Flask + Docker Production Setup (Complete Guide)
If you're trying to run Flask in production with Docker, this guide shows you how to build, configure, and start the app step-by-step. It covers a minimal production-safe container setup, Gunicorn process configuration, environment handling, persistent storage decisions, and Nginx reverse proxy support.
Quick Fix / Quick Setup
Use this minimal production-style setup first.
Dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "wsgi:app", "--bind", "0.0.0.0:8000", "--workers", "3"]
.dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
venv/
.env
.git
Build and run
docker build -t flask-app .
docker run -d \
--name flask-app \
--restart unless-stopped \
-p 8000:8000 \
--env-file .env \
flask-app
Test
curl http://127.0.0.1:8000
This gets a Flask app running in a production-style container with Gunicorn. For public traffic, place Nginx in front of the container and terminate HTTPS there. If you need the reverse proxy layer, use Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide).
What’s Happening
In production, Flask should not run with the built-in development server inside Docker. Docker packages the app and dependencies into a repeatable image, and Gunicorn serves the application inside the container.
Most failures come from one of these: wrong Gunicorn app target, missing environment variables, binding to the wrong address, port mismatch, or assuming files written inside the container will persist after redeploy.
Step-by-Step Guide
1. Create a production WSGI entrypoint
Create a wsgi.py file at the project root.
If your app uses an application factory:
from yourapp import create_app
app = create_app()
If your app already exposes app directly:
from yourapp import app
Gunicorn must be able to import the target used in the command:
gunicorn wsgi:app --bind 0.0.0.0:8000 --workers 3
2. Pin production dependencies
Create or update requirements.txt.
Flask==3.0.3
gunicorn==22.0.0
psycopg2-binary==2.9.9
Add only the packages your app actually needs. If you use database migrations, include the migration package as well.
3. Create a production Dockerfile
Use a slim Python image, install dependencies, copy the app, and start Gunicorn.
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "wsgi:app", "--bind", "0.0.0.0:8000", "--workers", "3"]
If your app needs extra OS libraries, install them in the same apt-get install line.
4. Exclude local and secret files from the image
Create .dockerignore.
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
venv/
.env
.git
.gitignore
Do not bake .env into the image.
5. Configure Flask for production via environment variables
Use runtime environment variables instead of hardcoded values.
Example .env:
SECRET_KEY=change-me
DATABASE_URL=postgresql://user:password@db-host:5432/appdb
FLASK_ENV=production
Example config usage in Flask:
import os
class Config:
SECRET_KEY = os.environ["SECRET_KEY"]
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
DEBUG = False
For a full environment-variable setup, use Flask Environment Variables and Secrets Setup.
6. Build the image
docker build -t flask-app .
Confirm the build completes without copying secrets or local virtualenv files.
7. Run the container with restart policy and environment file
docker run -d \
--name flask-app \
--restart unless-stopped \
-p 8000:8000 \
--env-file .env \
flask-app
This publishes container port 8000 to host port 8000.
8. Verify the container is running and reachable
Check container state:
docker ps -a
Test the app:
curl http://127.0.0.1:8000
If the container exits immediately, inspect logs:
docker logs flask-app --tail 100
9. Mount persistent storage if the app writes files
Do not rely on the container filesystem for uploads or generated files that must survive container replacement.
Example with a host-mounted directory:
docker run -d \
--name flask-app \
--restart unless-stopped \
-p 8000:8000 \
--env-file .env \
-v /srv/flask/uploads:/app/uploads \
flask-app
Example with a named volume:
docker volume create flask_uploads
docker run -d \
--name flask-app \
--restart unless-stopped \
-p 8000:8000 \
--env-file .env \
-v flask_uploads:/app/uploads \
flask-app
Use external storage for long-term or multi-instance deployments.
10. Put Nginx in front of the container
For public production traffic, use Nginx as the reverse proxy and terminate HTTPS there.
Basic host-level Nginx upstream example:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Then test and reload:
sudo nginx -t
sudo systemctl reload nginx
If Nginx returns a 502, use Fix Flask 502 Bad Gateway (Step-by-Step Guide).
11. Add a health endpoint
Create a lightweight health route.
@app.get("/health")
def health():
return {"status": "ok"}, 200
Validate it:
curl http://127.0.0.1:8000/health
Use this during deploy validation and reverse-proxy checks.
12. Run database migrations explicitly
Do not assume schema changes apply automatically.
Example:
docker exec -it flask-app flask db upgrade
If migrations are part of deployment, run them as a one-off task before shifting production traffic.
13. Validate startup logs and runtime behavior
Check startup logs:
docker logs flask-app --tail 100
Look for:
- successful Gunicorn worker boot
- correct app import path
- no missing environment variables
- no database connection failures
- no permission errors on mounted paths
14. Use Compose when the stack grows
A single-container setup is fine for a simple app. If you add Nginx, PostgreSQL, Redis, workers, or scheduled jobs, move to Compose for service orchestration.
See Flask + Docker Compose Production Setup.
Common Causes
- Wrong Gunicorn target → Gunicorn cannot import the Flask app object → fix the module path, such as
wsgi:apporpackage.wsgi:app - Binding to
127.0.0.1inside the container → app is unreachable from outside the container → bind Gunicorn to0.0.0.0:8000 - Using
flask runin production → unstable or unsafe behavior → replace it with Gunicorn - Missing environment variables → app crashes at startup or loads wrong config → pass values with
--env-fileor deployment environment injection - Copying
.envinto the image → secrets leak into image layers → exclude.envwith.dockerignore - Using the container filesystem for uploads → files disappear after redeploy → use named volumes or external object storage
- Port mismatch → Docker publishes one port but Gunicorn listens on another → align
EXPOSE,--bind, and-p - Missing build dependencies →
pip installfails for compiled packages → install required OS packages during image build - Database host set to
localhostincorrectly → the container cannot reach the real database → use the actual external host or service name - Nginx upstream points to the wrong port → proxy returns 502 → verify container listener and proxy target
Debugging Section
Check container state:
docker ps -a
Inspect logs:
docker logs flask-app --tail 100
Open a shell inside the container:
docker exec -it flask-app sh
Inspect environment variables:
docker exec flask-app env | sort
Inspect container metadata:
docker inspect flask-app
Inspect image metadata:
docker image inspect flask-app
Test the app directly on the published port:
curl http://127.0.0.1:8000
Check a specific environment variable:
docker exec -it flask-app python -c "import os; print(os.getenv('FLASK_ENV'))"
If the app exits immediately, start a shell instead of the default command:
docker run --rm -it --entrypoint sh flask-app
What to look for:
ModuleNotFoundErrororImportError→ wrong app import path or missing packageKeyErrorfor config values → missing environment variables- bind errors → wrong port or process already using the port
- connection refused to the database → wrong DB host, port, credentials, or firewall
- missing files on mounted paths → incorrect volume target or permissions
If Nginx is involved, test both layers separately:
- Curl the container port directly
- Curl through Nginx
This isolates whether the failure is in the app container or the reverse proxy.
Checklist
- Docker image builds successfully without copying secrets into the image
- Gunicorn starts the correct Flask app target
- The container listens on
0.0.0.0:8000 - Required environment variables are provided at runtime
- Debug mode is disabled in production
- Static and media storage paths are handled correctly for production
- Persistent data is stored in volumes or external services
- The app responds successfully on the published port
- Nginx can proxy to the container if used
- Logs show clean startup with no import, permission, or database errors
For final production validation, use Flask Production Checklist (Everything You Must Do).
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask + Docker Compose Production Setup
- Flask Environment Variables and Secrets Setup
- Flask Production Checklist (Everything You Must Do)
FAQ
Can I deploy Flask with only Docker and no Docker Compose?
Yes. A single-container setup works. Compose becomes useful when you add Nginx, PostgreSQL, Redis, or worker services.
Should I expose Flask directly to the internet from the container?
Not recommended. Put Nginx or another reverse proxy in front for TLS and request handling.
What is the correct Gunicorn bind address in Docker?
Use 0.0.0.0 with the internal port, such as 0.0.0.0:8000.
Where should secrets go in a Docker-based Flask deployment?
Inject them at runtime through environment variables, secret managers, or deployment platform configuration. Do not bake them into the image.
Why does my app work locally but fail in the container?
Usually due to an incorrect import path, missing dependencies, missing environment variables, or assumptions about local files and services.
Final Takeaway
A production Flask Docker setup is mainly about using Gunicorn correctly, injecting runtime configuration safely, and avoiding ephemeral-container assumptions. If the app target, bind address, environment variables, reverse proxy wiring, and storage strategy are correct, the setup is usually stable and repeatable.