Reference Guide Detailed deployment notes with production context and concrete examples.

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.

bash
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:

text
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.

text
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:

python
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.

dockerfile
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:

dockerfile
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "wsgi:app"]

3. Define environment variables

Add application settings to .env.

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.

yaml
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 expose for the Flask app container
  • Use ports only 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.

nginx
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:app if your Flask object is app in app.py
  • wsgi:app if your WSGI entrypoint is wsgi.py
  • A dedicated WSGI file for app factories

Test import correctness inside the container later with:

bash
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:

yaml
volumes:
  uploads:

Mounted into the app container:

yaml
services:
  web:
    volumes:
      - uploads:/app/uploads

Mounted into Nginx for direct file serving:

yaml
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.

bash
docker compose up -d --build

Check service state:

bash
docker compose ps

Expected output: both web and nginx are running and not restarting.

9. Validate HTTP routing through Nginx

Test from the host:

bash
curl -I http://localhost

Or from a remote machine:

bash
curl -I http://SERVER_IP

Expected result:

text
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.

bash
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_pass may 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.

bash
docker exec -it $(docker compose ps -q web) env | sort

Look for:

  • SECRET_KEY
  • DATABASE_URL
  • REDIS_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.

bash
docker exec -it $(docker compose ps -q nginx) nginx -t

Expected result:

text
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:

yaml
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:

bash
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.1 instead of 0.0.0.0 → Nginx cannot reach the app container → Bind Gunicorn to 0.0.0.0:8000
  • Nginx proxy_pass points to the wrong host or port → Requests return 502 or connection refused → Use the Compose service name, such as web:8000
  • Missing environment variables → App crashes on startup or database connections fail → Define env_file or environment values and verify them inside the container
  • Using ports on the app container unnecessarily → Gunicorn becomes publicly reachable → Remove the public mapping and keep the app internal with expose
  • Static or media paths are not mounted or served by Nginx → CSS, JS, or uploads fail in production → Add the correct volumes and Nginx location blocks
  • 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 COPY instructions and rebuild
  • Nginx config syntax error → Proxy container fails to start → Run nginx -t inside 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

bash
docker compose ps

Look for:

  • containers stuck in restarting
  • exited containers
  • incorrect published ports on Nginx

Check Flask/Gunicorn logs

bash
docker compose logs -f web

Look for:

  • ModuleNotFoundError
  • import failures
  • missing dependencies
  • bad Gunicorn app path
  • database connection exceptions during startup

Check Nginx logs

bash
docker compose logs -f nginx

Look for:

  • connect() failed
  • host not found in upstream
  • upstream prematurely closed connection
  • syntax errors or startup failures

Inspect the web container

bash
docker exec -it $(docker compose ps -q web) sh

Useful checks inside the container:

bash
pwd
ls -la
env | sort
ps aux

Test Python import manually

bash
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

bash
docker exec -it $(docker compose ps -q nginx) nginx -t

Test Nginx-to-web connectivity

bash
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:8000 inside the web container
  • Nginx proxies to the correct Compose service name and port
  • Only Nginx exposes public ports
  • Required environment variables are present in the running web container
  • Persistent data paths use volumes and are not stored only in the container filesystem
  • docker compose ps shows stable running containers with no crash loop
  • curl to the server returns the expected response through Nginx

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.