439 lines
8.2 KiB
Markdown

# 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:
```bash
docker pull ghcr.io/benoitc/gunicorn:latest
```
### Quick Start
Mount your application directory and run:
```bash
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:
```bash
# 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
```bash
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)
```dockerfile
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
```yaml
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
```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:
```bash
docker build -t myapp .
docker run -p 8000:8000 myapp
```
## Production Configuration
### Environment Variables
Use environment variables for configuration:
```dockerfile
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`:
```dockerfile
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:
```python
# 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:
```bash
# At runtime
gunicorn app:app --workers $(( 2 * $(nproc) + 1 ))
```
### Non-Root User
Run as a non-root user for security:
```dockerfile
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:
```dockerfile
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:
```dockerfile
# 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`:
```yaml
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:
```yaml
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:
```dockerfile
CMD ["gunicorn", "app:app", \
"--bind", "0.0.0.0:8000", \
"--graceful-timeout", "30", \
"--timeout", "120"]
```
Match Docker's stop timeout:
```yaml
# docker-compose.yml
services:
web:
stop_grace_period: 30s
```
## Logging
Log to stdout/stderr for Docker log collection:
```python
# gunicorn.conf.py
accesslog = "-"
errorlog = "-"
loglevel = "info"
```
Use JSON logging for log aggregation:
```python
# 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:
```bash
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
```bash
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](../deploy.md) - General deployment patterns
- [Settings](../reference/settings.md) - All configuration options