6.1 KiB
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.
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:
- Reduce worker count
- Use
--max-requeststo restart workers periodically - Increase container memory limits
gunicorn app:app --workers 2 --max-requests 1000 --max-requests-jitter 100
Connection Reset
If you see connection resets, ensure:
- Load balancer health checks match your
/healthendpoint - Graceful timeout is sufficient for in-flight requests
- Keepalive settings match between Gunicorn and upstream proxy