Docker multi-stage build til Python FastAPI — optimeret til produktion
Docker multi-stage build til FastAPI: builder stage med uv, slim runtime image, non-root bruger, health check, secrets og GitHub Actions CI integration.
Af M-Bus Gateway
Docker multi-stage builds reducerer image-størrelse fra ~1GB til under 200MB for en FastAPI server. Her er produktionsopsætningen til M-Bus Gateway.
Multi-stage Dockerfile
# server/Dockerfile
# ─── Stage 1: Dependencies ────────────────────────────────────────────────────
FROM python:3.11-slim AS builder
# uv: ultrafast Python package installer (10-100× hurtigere end pip)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Kopier kun dependency-filer først (caching)
COPY pyproject.toml uv.lock ./
# Installer dependencies i isoleret virtualenv
RUN uv sync --frozen --no-dev --no-editable
# ─── Stage 2: Runtime ─────────────────────────────────────────────────────────
FROM python:3.11-slim AS runtime
# Sikkerhed: non-root bruger
RUN groupadd --gid 1001 mbus && \
useradd --uid 1001 --gid mbus --shell /bin/bash --create-home mbus
WORKDIR /app
# Installer kun runtime system-deps (WeasyPrint kræver Cairo og Pango)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libcairo2 \
libgdk-pixbuf2.0-0 \
fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
# Kopier virtualenv fra builder (ikke pip, ikke build-tools)
COPY --from=builder /app/.venv /app/.venv
# Kopier applikationskode
COPY --chown=mbus:mbus server/src ./src
# Aktiver virtualenv
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"
USER mbus
EXPOSE 8000
CMD ["uvicorn", "src.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--workers", "2", \
"--log-config", "/app/src/logging.json"]
pyproject.toml med uv
# server/pyproject.toml
[project]
name = "mbus-server"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.111",
"uvicorn[standard]>=0.29",
"sqlmodel>=0.0.18",
"asyncpg>=0.29",
"pydantic>=2.7",
"python-jose[cryptography]>=3.3",
"passlib[bcrypt]>=1.7",
"celery[redis]>=5.3",
"structlog>=24.1",
"weasyprint>=62.3",
"httpx>=0.27",
"brevo-python>=3.0",
"stripe>=9.0",
"msgpack>=1.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"httpx>=0.27",
"mypy>=1.10",
"ruff>=0.4",
]
.dockerignore
# server/.dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.egg-info/
.eggs/
dist/
build/
.git/
.gitignore
.env*
*.env
.venv/
tests/
docs/
*.md
.mypy_cache/
.ruff_cache/
.pytest_cache/
Health check endpoint
# server/src/health.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
import redis.asyncio as aioredis
router = APIRouter()
@router.get("/health")
async def health(
session: AsyncSession = Depends(get_session),
redis: aioredis.Redis = Depends(get_redis),
):
checks = {}
# Database connectivity
try:
await session.execute(text("SELECT 1"))
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"error: {e}"
# Redis connectivity
try:
await redis.ping()
checks["redis"] = "ok"
except Exception as e:
checks["redis"] = f"error: {e}"
all_ok = all(v == "ok" for v in checks.values())
return {
"status": "healthy" if all_ok else "degraded",
"checks": checks,
}
Docker Compose med secrets
# docker-compose.prod.yml
services:
server:
image: ghcr.io/kirken20/mbus-gateway/server:${IMAGE_TAG}
build:
context: .
dockerfile: server/Dockerfile
target: runtime
secrets:
- db_password
- jwt_secret
- stripe_key
environment:
DATABASE_URL: postgresql+asyncpg://mbus:DOCKER-SECRET[db_password]@timescaledb:5432/mbus
JWT_SECRET_KEY: DOCKER-SECRET[jwt_secret]
STRIPE_SECRET_KEY: DOCKER-SECRET[stripe_key]
ENVIRONMENT: production
healthcheck:
test: ["CMD-SHELL", "python -c \"import httpx; httpx.get('http://localhost:8000/health').raise_for_status()\""]
interval: 30s
timeout: 10s
retries: 3
deploy:
replicas: 2
update_config:
order: start-first # Zero-downtime: start ny inden stop gammel
failure_action: rollback
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 3
secrets:
db_password:
external: true
jwt_secret:
external: true
stripe_key:
external: true
GitHub Actions: build og push
# .github/workflows/build-server.yml
name: Build server image
on:
push:
branches: [main]
paths: ["server/**", "server/Dockerfile"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: server/Dockerfile
target: runtime
push: true
tags: |
ghcr.io/kirken20/mbus-gateway/server:latest
ghcr.io/kirken20/mbus-gateway/server:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_SHA=${{ github.sha }}
Image størrelse sammenligning
Før multi-stage (python:3.11 + alle deps):
Image størrelse: ~1.4 GB
Layer count: 28
Build tid: 4m 30s
Efter multi-stage (python:3.11-slim + uv + selektive deps):
Image størrelse: 187 MB
Layer count: 12
Build tid: 1m 45s (cached)
Build tid (ingen cache): 2m 10s
Breakdown af 187 MB:
python:3.11-slim base: 130 MB
WeasyPrint system deps (Cairo/Pango): 28 MB
Python virtualenv (uv): 29 MB
Application code: under 1 MB
Konklusion
Docker multi-stage builds med python:3.11-slim og uv reducerer image-størrelse fra 1.4GB til 187MB — hurtigere deploy og mindre angrebsflade. uv sync --frozen --no-dev installerer kun produktionsdependencies. Non-root bruger, health checks og Docker secrets er obligatoriske for produktionssikkerhed. GitHub Container Registry med ghcr.io giver gratis opbevaring til open-source og private projekter.
Se Docker Compose produktion guide eller GitHub Actions CI guide.