C.W.K.
Stream
Lesson 06 of 06 · published

Production-Ready Dockerfile — non-root, healthcheck, labels

~16 min · dockerfile, production, security

Level 0Container Curious
0 XP0/36 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

The five things a production Dockerfile gets right

  1. Multi-stage — small final image.
  2. Non-root user — drop privileges before CMD.
  3. HEALTHCHECK — Docker (and orchestrators) can tell when your app is actually ready.
  4. Exec-form CMD — JSON array, not a shell string. So PID 1 receives signals correctly.
  5. LABELs — version, source, maintainer. Searchable in registries.

About signals and graceful shutdown

If you write CMD python app.py (shell form), Docker actually runs /bin/sh -c "python app.py". PID 1 is the shell, not Python. SIGTERM goes to the shell, which doesn't forward it. Your Python app gets killed by SIGKILL after the 10-second grace period — no chance for cleanup.

Always write CMD ["python", "app.py"] (exec form). PID 1 is Python directly. SIGTERM reaches your handler. Graceful shutdown works.

Code

Production FastAPI Dockerfile·dockerfile
# Stage 1: install deps
FROM python:3.12-slim AS deps
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: runtime
FROM python:3.12-slim AS runtime
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

# Labels
LABEL org.opencontainers.image.source="https://github.com/cwk/myapi"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.licenses="MIT"

# Pull installed packages from deps stage
COPY --from=deps /usr/local/lib/python3.12/site-packages \
                 /usr/local/lib/python3.12/site-packages
COPY --from=deps /usr/local/bin /usr/local/bin

# App code
COPY . .

# Non-root user (create AND switch)
RUN addgroup --system app && adduser --system --group app
USER app

EXPOSE 8000

# Healthcheck — let Docker/k8s know when app is up
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

# Exec form so SIGTERM reaches Python directly
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

External links

Exercise

Take your earlier minimal Dockerfile from this track and harden it: convert to multi-stage, add a non-root user, add a HEALTHCHECK, switch CMD to exec form, add three OCI labels. Build it, run it, and verify docker ps shows (healthy) and that the running process inside the container is NOT root (docker exec ... whoami).

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.