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:
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:
gunicorn "app:create_app()" --bind 0.0.0.0:8000 --workers 4
A 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"]
# requirements.txt
flask
gunicorn
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(orFLASK_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(orapp.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.