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
text
.git
.gitignore
.env*
*.md
node_modules
__pycache__
*.pyc
.venv
dist
coverage
tests/fixtures
DockerfileRemove 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.txtbash
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:prodStart from all-dropped and add back only what breaks.
Gotchas
COPY --fromkeeps root ownership unless--chownis 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. EXPOSEdocuments, it does not publish — publishing is-p/orchestrator config.- apt cache bloat: always
rm -rf /var/lib/apt/lists/*in the sameRUNas 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). HEALTHCHECKin 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.