Flask + Docker Compose Production Setup
If you're trying to run Flask in production with Docker Compose, this guide shows you how to set up the app, Gunicorn, and Nginx step-by-step. The goal is a repeatable deployment that starts cleanly, serves traffic correctly, and is easy to debug.
Quick Fix / Quick Setup
Use this minimal production-style stack first. It verifies container startup, Gunicorn binding, Docker networking, and Nginx reverse proxy behavior before you add your full application, database, TLS, or persistent storage.
mkdir -p flask-compose-prod && cd flask-compose-prod
cat > app.py <<'PY'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Flask Compose production OK\n'
PY
cat > requirements.txt <<'TXT'
flask==3.0.3
gunicorn==22.0.0
TXT
cat > Dockerfile <<'DOCKER'
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "-w", "3", "-b", "0.0.0.0:8000", "app:app"]
DOCKER
cat > docker-compose.yml <<'YAML'
services:
web:
build: .
restart: unless-stopped
expose:
- "8000"
nginx:
image: nginx:stable-alpine
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- web
YAML
cat > nginx.conf <<'NGINX'
server {
listen 80;
server_name _;
location / {
proxy_pass http://web: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;
}
}
NGINX
docker compose up -d --build
curl -I http://localhost
Expected result:
HTTP/1.1 200 OK
What’s Happening
Docker Compose defines multiple services on a shared internal network. Gunicorn serves the Flask WSGI application inside the web container, and Nginx handles public HTTP traffic and proxies requests to web:8000.
Most production failures come from one of these conditions:
- Wrong Gunicorn entrypoint
- Gunicorn binding to the wrong interface
- Nginx proxying to the wrong service name or port
- Missing environment variables
- Missing persistent volumes for uploads or durable files
Step-by-Step Guide
1. Create a production project layout
Use a structure that separates app code, proxy config, and environment files.
flask-compose-prod/
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .env
└── nginx.conf
If your Flask app uses a factory pattern, add a wsgi.py file instead of pointing Gunicorn directly at your package internals.
Example wsgi.py:
from myapp import create_app
app = create_app()
2. Build a production Dockerfile for Flask + Gunicorn
Use a slim Python image, install only required dependencies, and bind Gunicorn to 0.0.0.0.
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "app:app"]
If your entrypoint is wsgi.py, change the final argument:
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "wsgi:app"]
3. Define environment variables
Add application settings to .env.
SECRET_KEY=change-this
FLASK_ENV=production
DATABASE_URL=postgresql://user:password@db:5432/appdb
REDIS_URL=redis://redis:6379/0
Use .env for Compose variable injection and service environment loading where appropriate. Do not hardcode secrets in the image.
4. Configure docker-compose.yml
Expose the app internally and publish only Nginx to the public network.
services:
web:
build: .
restart: unless-stopped
env_file:
- .env
expose:
- "8000"
volumes:
- uploads:/app/uploads
nginx:
image: nginx:stable-alpine
restart: unless-stopped
depends_on:
- web
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- uploads:/app/uploads:ro
volumes:
uploads:
Key points:
- Use
exposefor the Flask app container - Use
portsonly on Nginx - Mount volumes for any files that must survive rebuilds
5. Configure Nginx as the reverse proxy
Create nginx.conf and point proxy_pass to the Compose service name.
server {
listen 80;
server_name _;
client_max_body_size 20M;
location / {
proxy_pass http://web: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;
proxy_redirect off;
}
location /uploads/ {
alias /app/uploads/;
}
}
If you also serve static files through Nginx, add a dedicated location /static/ block and mount the static directory into the Nginx container. See Flask Static Files Not Loading in Production if CSS, JS, or image assets fail after deployment.
6. Confirm the Flask/Gunicorn entrypoint
This is one of the most common failure points.
Use the correct Gunicorn module path:
app:appif your Flask object isappinapp.pywsgi:appif your WSGI entrypoint iswsgi.py- A dedicated WSGI file for app factories
Test import correctness inside the container later with:
docker exec -it $(docker compose ps -q web) python -c "import app; print('import-ok')"
7. Add persistent storage for uploads or durable files
Do not rely on the container filesystem for long-term storage. Container replacement removes in-container data.
Example named volume in Compose:
volumes:
uploads:
Mounted into the app container:
services:
web:
volumes:
- uploads:/app/uploads
Mounted into Nginx for direct file serving:
services:
nginx:
volumes:
- uploads:/app/uploads:ro
For larger deployments, use object storage instead of container volumes for user-generated files.
8. Build and start the stack
Build images and launch services in detached mode.
docker compose up -d --build
Check service state:
docker compose ps
Expected output: both web and nginx are running and not restarting.
9. Validate HTTP routing through Nginx
Test from the host:
curl -I http://localhost
Or from a remote machine:
curl -I http://SERVER_IP
Expected result:
HTTP/1.1 200 OK
If you get 502 Bad Gateway, inspect the proxy and app logs immediately. Also see Flask Docker Container Not Starting (Fix Guide) for container boot failures.
10. Validate service-to-service networking
From inside the Nginx container, verify the upstream app responds on the Docker network.
docker exec -it $(docker compose ps -q nginx) wget -qO- http://web:8000
Expected output should include the Flask response body.
If this fails:
- Gunicorn may not be listening on
0.0.0.0:8000 proxy_passmay reference the wrong service- The app container may be crashing
11. Validate environment variables inside the app container
Check that required values exist at runtime.
docker exec -it $(docker compose ps -q web) env | sort
Look for:
SECRET_KEYDATABASE_URLREDIS_URL- any external API credentials your app requires
Missing environment variables are a common reason deployments work locally but fail on the server.
12. Validate Nginx configuration syntax
Before troubleshooting application code, confirm the proxy config is valid.
docker exec -it $(docker compose ps -q nginx) nginx -t
Expected result:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
13. Add optional database or Redis services only when needed
If you need a local PostgreSQL or Redis service, add them explicitly.
Example:
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: strongpassword
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
uploads:
postgres_data:
If the app starts before the database is ready, implement retry logic or a wait strategy in your app startup path.
14. Rebuild and roll out changes safely
For updates:
docker compose up -d --build
docker compose ps
docker compose logs --tail=50 web
docker compose logs --tail=50 nginx
curl -I http://localhost
Treat the deployment as incomplete until logs are clean and HTTP validation succeeds.
Common Causes
- Wrong Gunicorn app path or module name → Gunicorn exits or throws import errors → Fix the command to match the real WSGI entrypoint, such as
wsgi:app - Gunicorn bound to
127.0.0.1instead of0.0.0.0→ Nginx cannot reach the app container → Bind Gunicorn to0.0.0.0:8000 - Nginx
proxy_passpoints to the wrong host or port → Requests return502orconnection refused→ Use the Compose service name, such asweb:8000 - Missing environment variables → App crashes on startup or database connections fail → Define
env_fileorenvironmentvalues and verify them inside the container - Using
portson the app container unnecessarily → Gunicorn becomes publicly reachable → Remove the public mapping and keep the app internal withexpose - Static or media paths are not mounted or served by Nginx → CSS, JS, or uploads fail in production → Add the correct volumes and Nginx
locationblocks - Container filesystem used for persistent uploads → Files disappear after rebuild or replacement → Use named volumes or external storage
- Build context or Dockerfile copy paths are wrong → Code or dependency files are missing in the image → Fix
COPYinstructions and rebuild - Nginx config syntax error → Proxy container fails to start → Run
nginx -tinside the container and correct the config - Database starts but app connects too early → App fails boot with connection errors → Add retry logic or startup handling for dependent services
Debugging Section
Run these commands in order.
Check service state
docker compose ps
Look for:
- containers stuck in
restarting - exited containers
- incorrect published ports on Nginx
Check Flask/Gunicorn logs
docker compose logs -f web
Look for:
ModuleNotFoundError- import failures
- missing dependencies
- bad Gunicorn app path
- database connection exceptions during startup
Check Nginx logs
docker compose logs -f nginx
Look for:
connect() failedhost not found in upstreamupstream prematurely closed connection- syntax errors or startup failures
Inspect the web container
docker exec -it $(docker compose ps -q web) sh
Useful checks inside the container:
pwd
ls -la
env | sort
ps aux
Test Python import manually
docker exec -it $(docker compose ps -q web) python -c "import app; print('import-ok')"
If this fails, your image contents or Gunicorn entrypoint are wrong.
Validate Nginx config
docker exec -it $(docker compose ps -q nginx) nginx -t
Test Nginx-to-web connectivity
docker exec -it $(docker compose ps -q nginx) wget -qO- http://web:8000
If this fails but the web container is running, verify:
- Gunicorn bind address
- service name in
docker-compose.yml - correct internal port
Checklist
- Flask app starts under Gunicorn without import or dependency errors
- Gunicorn binds to
0.0.0.0:8000inside thewebcontainer - Nginx proxies to the correct Compose service name and port
- Only Nginx exposes public ports
- Required environment variables are present in the running
webcontainer - Persistent data paths use volumes and are not stored only in the container filesystem
-
docker compose psshows stable running containers with no crash loop -
curlto the server returns the expected response through Nginx
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Flask + Docker Production Setup (Complete Guide)
- Flask Docker Container Not Starting (Fix Guide)
- Flask Production Checklist (Everything You Must Do)
FAQ
Should Flask run with the built-in development server in Docker production?
No. Use Gunicorn or another production WSGI server behind Nginx.
Can I skip Nginx and publish Gunicorn directly?
You can, but Nginx is preferred for reverse proxying, static files, buffering, and TLS termination.
How do I persist uploaded files?
Mount a named volume or use external storage, then configure your app and Nginx to use that path.
Where should HTTPS be configured?
Usually at Nginx, with certificates mounted into the proxy container or terminated at the host or load balancer.
Why does docker compose up work locally but fail on the server?
Typical causes are missing environment variables, wrong paths, port conflicts, DNS differences, firewall rules, or server-specific filesystem assumptions.
Final Takeaway
A reliable Flask Docker Compose deployment depends on four things: the correct Gunicorn entrypoint, the correct Nginx upstream target, the correct environment variables, and the correct volume and network configuration. Validate those first before investigating deeper application issues.