Running a Flask App in a Docker Container: Common Pitfalls and Production Best Practices
Running a Flask app in a Docker container requires binding to 0.0.0.0, using a production WSGI server instead of the built-in development server, and managing configuration through environment variables to avoid security vulnerabilities.
Containerizing Python web applications is standard practice for modern deployments, but running a Flask app in a Docker container introduces specific networking, security, and configuration challenges that differ from local development. The pallets/flask repository provides the development tools and documentation that inform these best practices, yet many developers inadvertently ship containers with the debug server enabled or bind to localhost, rendering their services unreachable.
Avoiding the Development Server in Production
The most critical mistake when running a Flask app in a Docker container is relying on app.run() or flask run in production. In src/flask/app.py, the run method is explicitly designed for development purposes only.
The built-in Werkzeug server is single-threaded, does not support graceful shutdown, and exposes the interactive debugger when FLASK_ENV=development or DEBUG=True. If an error occurs, the debugger allows arbitrary code execution, creating a severe security vulnerability in containerized environments.
Best practice: Treat app.run only for local testing. Deploy behind a production-grade WSGI server such as Gunicorn, uWSGI, or Waitress, and explicitly set FLASK_ENV=production.
Network Binding and Port Exposure
A common networking pitfall when running a Flask app in a Docker container is binding to 127.0.0.1 (localhost). Inside a container, 127.0.0.1 is reachable only from the container itself; the host machine and external networks cannot reach the service.
According to the Flask quickstart documentation (docs/quickstart.rst), you must bind to 0.0.0.0 to listen on all interfaces, or use the --host=0.0.0.0 flag when starting the application.
Best practice: Configure your WSGI server to bind to 0.0.0.0:8000 (or your chosen port), and use the EXPOSE directive in your Dockerfile for documentation purposes. Map the port when running the container using -p 8000:8000 or the ports key in Docker Compose.
Configuration Management and Secrets
Hard-coding configuration values such as SECRET_KEY, database URLs, or debug flags into your container image creates security risks and reduces portability. When these values are baked into the image, every environment (development, staging, production) requires a separate build, and secrets become visible in the image layers.
The Flask run method in src/flask/app.py automatically loads environment variables from a .env file if python-dotenv is installed, but for containerized deployments, you should inject configuration at runtime.
Best practice: Load configuration from environment variables. Set FLASK_ENV=production, SECRET_KEY, and database connection strings via the -e flag in docker run or an .env file with Docker Compose. Never commit secrets to version control or bake them into images.
Security Hardening and Image Optimization
Running containers as the root user violates the principle of least privilege. If your Flask process runs as root and is compromised, the attacker gains full container privileges and potentially host access.
Additionally, failing to use multi-stage builds results in bloated images containing development dependencies, build tools, and test files that increase the attack surface and deployment time.
Best practice: Create a non-root user in your Dockerfile using RUN adduser --disabled-password --gecos "" appuser and switch to it with USER appuser. Implement multi-stage builds: use a builder stage to compile dependencies and a runtime stage to copy only the necessary wheels and application code. Pin exact package versions in requirements.txt to ensure reproducible builds.
Static Files and Health Checks
Flask’s built-in static file handling (/static route) is designed for development convenience only. In production, serving static assets through a WSGI server like Gunicorn is inefficient and can block worker processes.
Additionally, container orchestrators like Kubernetes and Docker Swarm require health checks to determine if your application is ready to receive traffic. Without a dedicated endpoint, the orchestrator may restart healthy containers or fail to route traffic.
Best practice: Serve static files via a dedicated HTTP server like NGINX or Caddy, or configure your WSGI server’s static file handling. Implement a lightweight health-check endpoint in your Flask application:
from flask import Flask
app = Flask(__name__)
@app.route("/health")
def health():
return "", 200
Configure your orchestrator to poll this endpoint to verify container health.
Graceful Shutdown and Process Management
When Docker stops a container, it sends a SIGTERM signal to the main process. If your Flask application runs under Gunicorn without proper timeout configuration, workers may be killed abruptly, causing dropped requests and incomplete transactions.
Best practice: Use Gunicorn’s --graceful-timeout flag to allow workers to finish processing in-flight requests before shutting down. Configure your container to handle SIGTERM properly by ensuring Gunicorn runs as PID 1 or using a lightweight init system like tini.
Example Gunicorn configuration:
gunicorn -b 0.0.0.0:8000 --workers 4 --graceful-timeout 30 "hello:create_app()"
Summary
- Never use the development server (
app.run) in production; deploy behind Gunicorn or uWSGI as implemented insrc/flask/app.py. - Bind to
0.0.0.0instead of127.0.0.1to allow external access, as documented indocs/quickstart.rst. - Inject configuration via environment variables rather than hard-coding secrets; set
FLASK_ENV=productionat runtime. - Run as a non-root user and use multi-stage builds to minimize image size and attack surface.
- Serve static files through a dedicated web server and implement a
/healthendpoint for orchestrator compatibility. - Configure graceful shutdown with
--graceful-timeoutto prevent dropped requests during container stops.
Frequently Asked Questions
Why does my Flask app work locally but return connection refused in Docker?
This occurs because Flask’s development server defaults to binding on 127.0.0.1 (localhost), which is only accessible inside the container itself. According to docs/quickstart.rst, you must specify --host=0.0.0.0 or configure your WSGI server to bind to 0.0.0.0 to accept connections from outside the container.
Is it safe to use flask run inside a Docker container?
No. The flask run command and app.run() method in src/flask/app.py are designed specifically for development. They enable the interactive debugger and reloader, which expose arbitrary code execution vulnerabilities if enabled in production. Always use a production WSGI server like Gunicorn or uWSGI in containerized deployments.
How do I handle secrets and configuration in a Flask Docker container?
Never bake secrets into your Docker image. Instead, pass configuration via environment variables at runtime using the -e flag or Docker Compose environment keys. Flask automatically loads variables from a .env file during development, but in containers, set FLASK_ENV=production, SECRET_KEY, and database URLs through the orchestration layer to keep credentials out of image layers.
What is the correct way to shut down a Flask container without dropping requests?
Configure your WSGI server to handle SIGTERM signals gracefully. When Docker stops a container, it sends SIGTERM to PID 1. If using Gunicorn, set --graceful-timeout 30 to allow workers to complete in-flight requests before the container exits. Ensure Gunicorn runs as the main process or use an init system like tini to properly forward signals to the worker processes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →