Docker Deployment Guide¶
This guide covers Docker-specific deployment configurations, best practices, and optimization techniques for the FastAPI HTTP/WebSocket application.
Table of Contents¶
- Dockerfile Best Practices
- Multi-Stage Builds
- Docker Compose Production
- Image Optimization
- Security Hardening
- Health Checks
- Resource Limits
- Networking
Dockerfile Best Practices¶
Production Dockerfile¶
Create docker/Dockerfile.production:
# ============================================
# Stage 1: Builder - Install dependencies
# ============================================
FROM python:3.11-slim as builder
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create wheel directory
WORKDIR /wheels
# Copy requirements and build wheels
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ============================================
# Stage 2: Runtime - Minimal production image
# ============================================
FROM python:3.11-slim
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/home/appuser/.local/bin:$PATH"
# Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -u 1000 -s /bin/bash appuser
# Set working directory
WORKDIR /app
# Copy wheels from builder
COPY --from=builder /wheels /wheels
# Install Python packages from wheels
RUN pip install --no-cache --no-index --find-links=/wheels /wheels/* \
&& rm -rf /wheels
# Copy application code
COPY --chown=appuser:appuser . .
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run application
CMD ["uvicorn", "app:application", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--workers", "4", \
"--log-config", "/app/uvicorn_logging.json"]
Key Features¶
- Multi-stage build: Separates build and runtime dependencies (smaller image)
- Non-root user: Runs as
appuser(UID 1000) for security - Minimal base: Uses
slimvariant to reduce attack surface - Health check: Built-in Docker health monitoring
- Optimized layers: Leverages Docker layer caching
Multi-Stage Builds¶
Why Multi-Stage?¶
- Smaller images: Build dependencies (gcc, build-essential) not in final image
- Faster deployment: Less data to push/pull
- Better security: Fewer packages = smaller attack surface
Build Process¶
# Build production image
docker build -f docker/Dockerfile.production -t fastapi-app:1.0.0 .
# Check image size
docker images fastapi-app:1.0.0
# Expected: ~300-400MB (vs ~800MB+ without multi-stage)
Layer Optimization¶
# ❌ BAD: Changes to code trigger full rebuild
COPY . .
RUN pip install -r requirements.txt
# ✅ GOOD: Dependencies cached separately
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
Docker Compose Production¶
Production Compose File¶
Create docker/docker-compose.prod.yml:
version: '3.8'
services:
hw-server:
image: fastapi-app:${VERSION:-latest}
container_name: hw-server-prod
hostname: hw-server
networks:
- hw-network
# No exposed ports - only accessible via Traefik
expose:
- "8000"
# Production volume (code built into image)
volumes:
- /var/log/fastapi:/app/logs
# Run as non-root user
user: "1000:1000"
env_file:
- ../.env.production
- .srv_env.production
# Resource limits
deploy:
replicas: 3
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
hw-db:
condition: service_healthy
hw-redis:
condition: service_healthy
hw-keycloak:
condition: service_healthy
traefik:
condition: service_healthy
restart: unless-stopped
# Security options
security_opt:
- no-new-privileges:true
# Read-only root filesystem (logs volume is writable)
read_only: true
tmpfs:
- /tmp
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
hw-db:
image: postgres:13
container_name: hw-db-prod
networks:
- hw-network
# Only expose to internal network
expose:
- "5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./backups:/backups
env_file:
- .pg_env.production
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
# PostgreSQL tuning
command: >
postgres
-c shared_buffers=2GB
-c effective_cache_size=6GB
-c maintenance_work_mem=512MB
-c checkpoint_completion_target=0.9
-c wal_buffers=16MB
-c default_statistics_target=100
-c random_page_cost=1.1
-c effective_io_concurrency=200
-c work_mem=16MB
-c min_wal_size=1GB
-c max_wal_size=4GB
-c max_connections=100
hw-redis:
image: redis:7-alpine
container_name: hw-redis-prod
networks:
- hw-network
expose:
- "6379"
volumes:
- redis-data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
deploy:
resources:
limits:
cpus: '1'
memory: 2G
reservations:
cpus: '0.5'
memory: 1G
command: redis-server /usr/local/etc/redis/redis.conf
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
restart: unless-stopped
networks:
hw-network:
name: hw-network-prod
driver: bridge
ipam:
config:
- subnet: 172.25.0.0/16
volumes:
postgres-data:
name: postgres-prod-data
redis-data:
name: redis-prod-data
prometheus-data:
name: prometheus-prod-data
grafana-data:
name: grafana-prod-data
loki-data:
name: loki-prod-data
traefik-certificates:
name: traefik-prod-certs
Image Optimization¶
Size Reduction Techniques¶
-
Use Alpine base images (where possible):
-
Multi-stage builds (as shown above)
-
Remove build artifacts:
-
Minimize layers:
-
Use .dockerignore:
Build Cache Optimization¶
# Use BuildKit for better caching
DOCKER_BUILDKIT=1 docker build -t fastapi-app:latest .
# Use cache from registry
docker build --cache-from fastapi-app:latest -t fastapi-app:1.0.1 .
Security Hardening¶
1. Non-Root User¶
# Create user with specific UID
RUN useradd -m -u 1000 -s /bin/bash appuser
# Set ownership
COPY --chown=appuser:appuser . .
# Switch to user
USER appuser
2. Read-Only Root Filesystem¶
3. Drop Capabilities¶
4. Security Options¶
services:
app:
security_opt:
- no-new-privileges:true
- apparmor:docker-default
- seccomp:unconfined # Only if needed
5. Secrets Management¶
# Use Docker secrets (Swarm mode)
services:
app:
secrets:
- db_password
- api_key
secrets:
db_password:
external: true
api_key:
external: true
# Read secrets in app
with open('/run/secrets/db_password', 'r') as f:
db_password = f.read().strip()
Health Checks¶
Application Health Check¶
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
FastAPI Health Endpoint¶
# app/api/http/health.py
from fastapi import APIRouter, Response, status
from app.storage.db import async_session
from app.storage.redis import RRedis
router = APIRouter()
@router.get("/health")
async def health_check():
"""
Health check endpoint.
Checks:
- Application is running
- Database connection
- Redis connection
"""
checks = {
"status": "healthy",
"checks": {}
}
# Database check
try:
async with async_session() as session:
await session.execute("SELECT 1")
checks["checks"]["database"] = "healthy"
except Exception as e:
checks["status"] = "unhealthy"
checks["checks"]["database"] = f"unhealthy: {str(e)}"
# Redis check
try:
redis = RRedis()
await redis.ping()
checks["checks"]["redis"] = "healthy"
except Exception as e:
checks["status"] = "unhealthy"
checks["checks"]["redis"] = f"unhealthy: {str(e)}"
status_code = status.HTTP_200_OK if checks["status"] == "healthy" else status.HTTP_503_SERVICE_UNAVAILABLE
return Response(
content=json.dumps(checks),
status_code=status_code,
media_type="application/json"
)
Monitoring Health Status¶
# Check container health
docker ps --filter health=healthy
docker ps --filter health=unhealthy
# Inspect health status
docker inspect --format='{{json .State.Health}}' hw-server | jq
Resource Limits¶
Memory Limits¶
services:
app:
deploy:
resources:
limits:
memory: 2G # Hard limit
reservations:
memory: 1G # Minimum guaranteed
CPU Limits¶
services:
app:
deploy:
resources:
limits:
cpus: '2.0' # Max 2 CPUs
reservations:
cpus: '1.0' # Min 1 CPU
Monitoring Resource Usage¶
# Real-time stats
docker stats
# Specific container
docker stats hw-server
# Export stats
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" > stats.txt
Networking¶
Bridge Network (Default)¶
Custom Network Settings¶
Network Isolation¶
# Public network (Traefik)
networks:
public:
external: true
# Private network (backend services)
networks:
private:
internal: true # No external access
services:
traefik:
networks:
- public
app:
networks:
- public
- private
db:
networks:
- private # Only accessible from app
Logging¶
JSON Logging Driver¶
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels: "production,fastapi"
Centralized Logging¶
# Use Loki driver (requires plugin)
services:
app:
logging:
driver: loki
options:
loki-url: "http://loki:3100/loki/api/v1/push"
loki-external-labels: "job=fastapi,environment=production"
Build and Deploy Workflow¶
Automated Image Builds¶
This project includes automated Docker image builds via GitHub Actions. Every push to main branch triggers a build and push to GitHub Container Registry (ghcr.io).
Workflow file: .github/workflows/docker-build.yml
Features: - ✅ Multi-platform builds (linux/amd64, linux/arm64) - ✅ Automatic tagging (latest, commit SHA, branch name) - ✅ Layer caching for faster builds - ✅ Push to GitHub Container Registry (ghcr.io)
Generated image tags:
ghcr.io/acikabubo/fastapi-http-websocket:latest
ghcr.io/acikabubo/fastapi-http-websocket:main-abc1234
ghcr.io/acikabubo/fastapi-http-websocket:main
Pulling Images¶
# Pull latest image
docker pull ghcr.io/acikabubo/fastapi-http-websocket:latest
# Pull specific commit
docker pull ghcr.io/acikabubo/fastapi-http-websocket:main-abc1234
# Run pulled image
docker run -d -p 8000:8000 \
--name fastapi-app \
ghcr.io/acikabubo/fastapi-http-websocket:latest
Manual Workflow Trigger¶
The workflow can also be triggered manually from GitHub Actions UI:
- Go to Actions tab in GitHub
- Select Build and Push Docker Image workflow
- Click Run workflow
- Choose branch and click Run
CI/CD Pipeline Example (with Deployment)¶
If you want to add automatic deployment after image build:
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -f docker/Dockerfile \
-t ghcr.io/user/fastapi-app:${{ github.sha }} \
-t ghcr.io/user/fastapi-app:latest .
- name: Push to registry
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/user/fastapi-app:${{ github.sha }}
docker push ghcr.io/user/fastapi-app:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: |
ssh user@prod-server "cd /app && \
export VERSION=${{ github.sha }} && \
docker-compose -f docker-compose.prod.yml pull && \
docker-compose -f docker-compose.prod.yml up -d"
Troubleshooting¶
Common Issues¶
Issue: Container exits immediately
# Check logs
docker logs hw-server
# Check exit code
docker inspect hw-server --format='{{.State.ExitCode}}'
Issue: Permission denied
Issue: Out of memory
Issue: Cannot connect to service
Best Practices Summary¶
✅ DO: - Use multi-stage builds - Run as non-root user - Set resource limits - Implement health checks - Use .dockerignore - Pin base image versions - Use BuildKit - Scan images for vulnerabilities
❌ DON'T: - Run as root - Store secrets in images - Use latest tag in production - Ignore security updates - Over-allocate resources - Skip health checks