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

### As Base Image (Recommended for Production)

    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