Skip to content

Docker Deployment

Running Gunicorn in Docker containers is the most common deployment pattern for modern Python applications. This guide covers best practices for containerizing Gunicorn applications.

Official Docker Image

Gunicorn provides an official Docker image on GitHub Container Registry:

docker pull ghcr.io/benoitc/gunicorn:latest

Quick Start

Mount your application directory and run:

docker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app

Running in Background

Use -d (detached mode) to run the container in the background:

# Start in background
docker run -d --name myapp -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app

# View logs
docker logs myapp

# Follow logs in real-time
docker logs -f myapp

# Stop the container
docker stop myapp

# Start it again
docker start myapp

# Remove the container
docker rm myapp

Environment Variables

Variable Description Default
GUNICORN_BIND Full bind address 0.0.0.0:8000
GUNICORN_HOST Bind host 0.0.0.0
GUNICORN_PORT Bind port 8000
GUNICORN_WORKERS Number of workers (2 * CPU) + 1
GUNICORN_ARGS Additional arguments (none)

With Configuration

docker run -p 9000:9000 -v $(pwd):/app \
  -e GUNICORN_PORT=9000 \
  -e GUNICORN_WORKERS=4 \
  -e GUNICORN_ARGS="--timeout 120 --access-logfile -" \
  ghcr.io/benoitc/gunicorn app:app
FROM ghcr.io/benoitc/gunicorn:24.1.0

# Install app dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY --chown=gunicorn:gunicorn . .

CMD ["myapp:app", "--workers", "4"]

With Docker Compose

services:
  web:
    image: ghcr.io/benoitc/gunicorn:latest
    ports:
      - "8000:8000"
    volumes:
      - ./app:/app
    command: ["myapp:app", "--workers", "4"]

Available Tags

  • ghcr.io/benoitc/gunicorn:latest - Latest release
  • ghcr.io/benoitc/gunicorn:24.1.0 - Specific version
  • ghcr.io/benoitc/gunicorn:24.1 - Minor version
  • ghcr.io/benoitc/gunicorn:24 - Major version

Building Your Own Image

For more control, build a custom image using the patterns below.

Basic Dockerfile

FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY . .

# Run gunicorn
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Build and run:

docker build -t myapp .
docker run -p 8000:8000 myapp

Production Configuration

Environment Variables

Use environment variables for configuration:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Configuration via environment
ENV GUNICORN_WORKERS=4
ENV GUNICORN_BIND=0.0.0.0:8000

CMD gunicorn app:app \
    --workers ${GUNICORN_WORKERS} \
    --bind ${GUNICORN_BIND}

Or use GUNICORN_CMD_ARGS:

ENV GUNICORN_CMD_ARGS="--workers=4 --bind=0.0.0.0:8000"
CMD ["gunicorn", "app:app"]

Worker Count

In containers, determine workers based on available CPU:

# gunicorn.conf.py
import multiprocessing

workers = multiprocessing.cpu_count() * 2 + 1
bind = "0.0.0.0:8000"

Or let Kubernetes/Docker limit CPU and calculate accordingly:

# At runtime
gunicorn app:app --workers $(( 2 * $(nproc) + 1 ))

Non-Root User

Run as a non-root user for security:

FROM python:3.12-slim

# Create non-root user
RUN useradd --create-home appuser
WORKDIR /home/appuser/app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .

USER appuser

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Health Checks

Add a health check endpoint and Docker health check:

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

Multi-Stage Build

Reduce image size with multi-stage builds:

# Build stage
FROM python:3.12 AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# Runtime stage
FROM python:3.12-slim

WORKDIR /app

# Copy wheels and install
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels

COPY . .

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]

Docker Compose

Example docker-compose.yml:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://db:5432/myapp
    depends_on:
      - db
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 512M

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_PASSWORD=secret
    volumes:
      - postgres_data:/var/lib/postgresql/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - web

volumes:
  postgres_data:

Kubernetes Deployment

Example Kubernetes deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8000
        env:
        - name: GUNICORN_WORKERS
          value: "4"
        resources:
          limits:
            cpu: "1"
            memory: "512Mi"
          requests:
            cpu: "500m"
            memory: "256Mi"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8000

Graceful Shutdown

Gunicorn handles SIGTERM gracefully by default. Configure the timeout:

CMD ["gunicorn", "app:app", \
     "--bind", "0.0.0.0:8000", \
     "--graceful-timeout", "30", \
     "--timeout", "120"]

Match Docker's stop timeout:

# docker-compose.yml
services:
  web:
    stop_grace_period: 30s

Logging

Log to stdout/stderr for Docker log collection:

# gunicorn.conf.py
accesslog = "-"
errorlog = "-"
loglevel = "info"

Use JSON logging for log aggregation:

# gunicorn.conf.py
import json
import datetime

class JsonFormatter:
    def format(self, record):
        return json.dumps({
            "timestamp": datetime.datetime.utcnow().isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
        })

logconfig_dict = {
    "version": 1,
    "formatters": {
        "json": {"()": JsonFormatter}
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "json",
            "stream": "ext://sys.stdout"
        }
    },
    "root": {
        "handlers": ["console"],
        "level": "INFO"
    }
}

Troubleshooting

Worker Timeout

If workers are killed with [CRITICAL] WORKER TIMEOUT, increase the timeout:

gunicorn app:app --timeout 120

Or investigate slow requests in your application.

Out of Memory

If containers are OOM-killed:

  1. Reduce worker count
  2. Use --max-requests to restart workers periodically
  3. Increase container memory limits
gunicorn app:app --workers 2 --max-requests 1000 --max-requests-jitter 100

Connection Reset

If you see connection resets, ensure:

  1. Load balancer health checks match your /health endpoint
  2. Graceful timeout is sufficient for in-flight requests
  3. Keepalive settings match between Gunicorn and upstream proxy

See Also

  • Deploy - General deployment patterns
  • Settings - All configuration options