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.
# ---------- 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.
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: 15sstop_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…