Flask Production Deployment

Why not to use the dev server in production, running Gunicorn, and a Dockerfile example.

Why not the dev server

Flask's built-in server (app.run() / flask run) is explicitly documented as unsuitable for production: it's single-threaded by default (one request at a time, unless you pass threaded=True, which still isn't the same as a real production server's process/worker model), has no protection against slow clients holding a worker hostage, and — critically — debug=True enables an interactive in-browser debugger that can execute arbitrary Python code, an outright security hole on a public server.

Gunicorn

Gunicorn ("Green Unicorn") is a production-grade WSGI server for Python — it's what actually runs a Flask app in production, in place of flask run:

Bash
pip install gunicorn
gunicorn app:app --bind 0.0.0.0:8000 --workers 4

app:app means "in the app module, use the object named app" — the same Flask(__name__) instance flask run would have used. --workers 4 runs four separate worker processes, letting Gunicorn handle several requests concurrently across CPU cores — a starting point of (2 × CPU cores) + 1 is Gunicorn's own documented rule of thumb.

If the app uses an application factory (create_app()) rather than a single module-level app object, Gunicorn calls it directly:

Bash
gunicorn "app:create_app()" --bind 0.0.0.0:8000 --workers 4

A Dockerfile

Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]
Text
# requirements.txt
flask
gunicorn
Bash
docker build -t my-flask-app .
docker run -p 8000:8000 -e FLASK_SECRET_KEY=some-real-secret my-flask-app

As with any framework's Dockerfile, requirements.txt is copied and installed before the rest of the application code — this lets Docker reuse the cached dependency-install layer on every subsequent build where only application code (not dependencies) changed, rather than reinstalling everything from scratch each time.

Common mistakes

  • Deploying with debug=True (or FLASK_DEBUG=1) still set — the interactive debugger it enables lets anyone who triggers an unhandled exception on the live site execute arbitrary Python code through the browser.
  • Running flask run (or app.run()) directly in production instead of Gunicorn — both are explicitly documented by Flask itself as development-only tools, not hardened for real traffic.
  • Baking a real SECRET_KEY (or database credentials) directly into the Docker image or source code instead of passing it in as an environment variable at container runtime.

Interview questions

Q: Why is Flask's built-in development server considered unsafe for production, beyond just performance? Performance is one factor — it's single-threaded by default and not built to handle real concurrent load — but the more serious issue is debug=True's interactive debugger, which lets anyone who can trigger an unhandled exception on the live site run arbitrary Python code directly from their browser. Both issues are avoided by running the app under Gunicorn (or another production WSGI server) with debug mode off.

Q: What does Gunicorn's --workers flag actually control, and how do you pick a value? It sets how many separate OS worker processes Gunicorn runs, each an independent copy of your Flask app handling requests concurrently — letting the app use more than one CPU core, which a single Python process can't do for CPU-bound work due to the GIL. Gunicorn's own documented rule of thumb is (2 × number of CPU cores) + 1 as a starting point, tuned from there based on actual load testing.