Non-root user

The default user in most base images is root. If an attacker ever breaks out of your process, they are root inside the container, and one kernel vulnerability away from root on the host. Adding an unprivileged user is a single line and removes that entire class of attack.

Dockerfile
dockerfile
# ---------- Stage 2: runtime ----------
FROM python:3.11-slim AS runtime

WORKDIR /app

# curl is only needed for the HEALTHCHECK probe.
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# Copy the pre-built virtualenv from the builder stage.
COPY --from=builder /app/.venv /app/.venv

# Application code
COPY . .

# Create an unprivileged user and hand ownership over.
RUN groupadd -r app && useradd -r -g app app \
    && chown -R app:app /app
USER app

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/app/.venv/bin:$PATH"

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -fsS http://localhost:8000/deploy-patterns/health/live || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Drop privileges with a dedicated app user. The HEALTHCHECK directive hits the liveness endpoint and causes Docker to mark the container unhealthy if it fails. PYTHONUNBUFFERED=1 is essential for real-time JSON logs so you do not chase missing log lines in production.

docker-compose.yml
yaml
services:
  api:
    build: .
    container_name: fastapi-ai-deployment-patterns
    ports:
      - "8000:8000"
    env_file:
      - .env
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/deploy-patterns/health/live"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3
    stop_grace_period: 15s

stop_grace_period: 15s tells compose to wait up to 15 seconds after SIGTERM before forcing SIGKILL. That window is what gives your lifespan handler time to drain in-flight background work.

Quiz: Quiz

Loading practice…