# Patterns — Containerizing Services ## Contents - Node.js multi-stage Dockerfile - Python multi-stage Dockerfile - Go static binary + distroless - .dockerignore baseline - Build-time secrets (BuildKit) - Runtime hardening flags - Gotchas ## Node.js multi-stage Dockerfile ```dockerfile # syntax=docker/dockerfile:1 FROM node:22.4-slim AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build && npm prune --omit=dev FROM node:22.4-slim ENV NODE_ENV=production WORKDIR /app RUN useradd --uid 10001 --create-home app COPY --from=build --chown=app:app /app/node_modules ./node_modules COPY --from=build --chown=app:app /app/dist ./dist USER app EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:3000/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" CMD ["node", "dist/server.js"] ``` ## Python multi-stage Dockerfile ```dockerfile # syntax=docker/dockerfile:1 FROM python:3.12.4-slim AS build WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.12.4-slim WORKDIR /app RUN useradd --uid 10001 --create-home app COPY --from=build /install /usr/local COPY --chown=app:app . . USER app EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz').status==200 else 1)" CMD ["gunicorn", "-b", "0.0.0.0:8000", "app.wsgi:application"] ``` ## Go static binary + distroless ```dockerfile # syntax=docker/dockerfile:1 FROM golang:1.23.1 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/svc ./cmd/svc FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /out/svc /svc EXPOSE 8080 ENTRYPOINT ["/svc"] ``` Distroless has no shell — the orchestrator's HTTP probe replaces `HEALTHCHECK`. ## .dockerignore baseline ``` .git .gitignore .env* *.md node_modules __pycache__ *.pyc .venv dist coverage tests/fixtures Dockerfile ``` Remove `dist` from the ignore list if you COPY prebuilt artifacts instead of building in-image. ## Build-time secrets (BuildKit) ```dockerfile RUN --mount=type=secret,id=pip_index \ PIP_INDEX_URL=$(cat /run/secrets/pip_index) pip install -r requirements.txt ``` ```bash docker build --secret id=pip_index,src=.pip_index_url . ``` Never `ARG TOKEN` — args are recoverable via `docker history`. ## Runtime hardening flags ```bash docker run --rm \ --read-only --tmpfs /tmp \ --cap-drop ALL \ --security-opt no-new-privileges \ -p 8000:8000 svc:prod ``` Start from all-dropped and add back only what breaks. ## Gotchas - **`COPY --from` keeps root ownership** unless `--chown` is given — the classic "works as root, crashes as USER app" cause. - **alpine + Python wheels**: musl forces source builds of numpy/psycopg2 etc.; use `-slim` (glibc) unless you've verified alpine. - **`EXPOSE` documents, it does not publish** — publishing is `-p`/orchestrator config. - **apt cache bloat**: always `rm -rf /var/lib/apt/lists/*` in the same `RUN` as the install, or the cache lands in the layer anyway. - **CMD shell form (`CMD node server.js`) wraps in `/bin/sh`** → PID 1 is sh, signals (SIGTERM) never reach the app → 10s kill delay on every deploy. Use exec form (JSON array). - **`HEALTHCHECK` in Dockerfile is ignored by Kubernetes** — it uses its own probes; keep both consistent. - **Bind-mounting over image content in dev** hides image bugs; test the real image before shipping.