11 · Deploying a web app
Ship the app the same way the MLOps track ships models: containerize it. A Flask app runs behind `gunicorn` (the dev server isn't production-grade); a Streamlit app runs with `streamlit run` on port 8501. A `Dockerfile` + `requirements.txt` makes either reproducible, then the MLOps cloud options take it live.
Deploy by CONTAINERIZING, exactly like the MLOps track: a `requirements.txt` pins deps, a `Dockerfile` packages the app. A Flask app's `CMD` runs `gunicorn` (a real WSGI server — the built-in dev server is NOT production-grade); a Streamlit app's `CMD` runs `streamlit run` bound to `0.0.0.0:8501`. The image then deploys through the SAME cloud options the MLOps track covered (Docker registry → cloud run/host).
Without this:
Running the Flask dev server in production can't handle real load and exposes the debugger as an attack surface; and without a container the app's exact Python/dependency environment can't be reproduced on the cloud host.
Your app works locally — now you need it live, on a server anyone can reach. The good news: you've already learned the how in the MLOps track. Deploying a web app is the same playbook as deploying a model — containerize it, then push it to the cloud — with one app-specific twist: the start command.
Step 1 — pin dependencies with requirements.txt. List the exact packages the app needs (flask, gunicorn, scikit-learn, joblib — or streamlit, pandas, ...). This is what makes the environment reproducible: the cloud host installs precisely these, so it runs the same code you ran locally.
Step 2 — write a Dockerfile. Just like the MLOps track (Ch 2, Docker): start from a Python base image, copy in the code and requirements.txt, pip install, and set the CMD that launches the server. The CMD is the one line that differs between a Flask and a Streamlit app:
Flask in production → gunicorn, NOT the dev server. Recall lesson 2's warning: app.run(debug=True) is for development only. The built-in server is single-threaded, slow under load, and (with debug) a security hole. In production you run Flask behind a real WSGI server — gunicorn — which handles many concurrent requests across worker processes:
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"] — app:app means "the app object inside app.py", and -b 0.0.0.0:8000 binds all interfaces (so the container is reachable) on port 8000.
Streamlit in production → streamlit run, port 8501. Streamlit ships its own production-capable server, so there's no gunicorn equivalent — you run the same streamlit run command, but bind it for a container:
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] — port 8501 is Streamlit's conventional port, and --server.address=0.0.0.0 makes it reachable from outside the container (not just localhost).
Step 3 — deploy via the MLOps cloud options. Once the image is built, it's just a container — so it ships exactly like a model image: push it to a registry and run it on a cloud platform (the MLOps track, Ch 7, cloud deployment covers the options — cloud run services, container hosts, etc.). Expose the right port (8000 for the Flask example, 8501 for Streamlit), and the app is live.
The key principle: a web app is just another container. Everything the MLOps track taught about Docker and cloud deployment applies unchanged; the only web-specific decisions are the CMD (gunicorn vs streamlit run) and the exposed port.
The read-along cells show both Dockerfiles side by side and the build/run commands.
A Flask production Dockerfile: pin deps, copy code, and run behind `gunicorn` (`gunicorn -b 0.0.0.0:8000 app:app`) — a real WSGI server, never the development `app.run()`.
A Streamlit production Dockerfile: same base, but the `CMD` runs `streamlit run app.py --server.port=8501 --server.address=0.0.0.0` — Streamlit's own server on port 8501, bound for external access.
Build and run each image (`docker build`/`docker run`, ports 8000 for Flask and 8501 for Streamlit), then deploy by pushing the container to a registry and running it on the cloud — the same flow as the MLOps track (Ch 2 Docker, Ch 7 cloud).
Which statement correctly pairs each framework with its production serving setup?
- Deploy a web app the MLOps way: a `requirements.txt` pins deps and a `Dockerfile` packages it — then ship it to the cloud like any container (MLOps Ch 2 Docker, Ch 7 cloud).
- Flask in production runs behind `gunicorn` (`gunicorn -b 0.0.0.0:8000 app:app`) — the built-in dev server isn't production-grade and `debug=True` is a security risk.
- Streamlit ships its own server: run `streamlit run app.py --server.port=8501 --server.address=0.0.0.0` on port 8501 — no gunicorn needed.
A deployed model front end is a container like any other — the same Docker + cloud pipeline the MLOps track built for model images carries a Flask API or Streamlit dashboard to production unchanged.
If you remove it: Without containerizing and using a real server, the app either won't reproduce on the host or will fall over under load — and a debug-enabled Flask server is a live security hole.