do-dockerfile
When to use
You need a production Dockerfile, a bloated image slimmed down, a slow build made cache-friendly, or a docker-compose stack for local development and integration testing.
Not for: Kubernetes manifests or runtime orchestration (use do-kubernetes), CI wiring that builds the image (use do-ci-pipeline), or host provisioning (use do-terraform).
Method
- Pick the base. Decision: use distroless or a digest-pinned slim tag for compiled/static apps; use alpine or slim only when you need a shell/package manager. Never
latest. - Use multi-stage builds: a build stage with compilers and dev dependencies, a final stage that copies only the runtime artifact.
- Order layers by change frequency — copy the lockfile and install dependencies before copying source, so code edits do not bust the dependency cache.
- Harden. Decision: run as a non-root
USER; if the app writes at runtime, mount a writable volume rather than droppingreadOnlyRootFilesystem. Avoid embedding secrets (use build args or mounts), and add aHEALTHCHECK. - Trim: combine
RUNsteps, clean package caches in the same layer, and add a.dockerignoreto keep the build context lean. - For compose: declare health-gated
depends_on, named volumes, and an isolated network; keep dev-only overrides in a separate file.
Example
# build stage — has the toolchain
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci # cached until lockfile changes
COPY . .
RUN npm run build
# runtime stage — minimal, non-root
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER nonroot
HEALTHCHECK CMD ["node", "dist/healthcheck.js"]
CMD ["dist/server.js"]
Pitfalls
- Copying source before installing dependencies, so every code edit reinstalls everything from scratch.
- Running as root in the runtime image — a container escape becomes host root.
- Using
ADDwith a remote URL or leaving a build-arg secret in an intermediate layer;docker historyexposes it. - Cleaning apt/npm caches in a separate
RUN— the deleted files still live in the earlier layer, so the image never shrinks.
Output format
Dockerfile: multi-stage, commented.
.dockerignore: entries listed.
docker-compose.yml: when relevant (health-gated deps, named volumes).
Before/after:
- image size: <old> -> <new>
- layer cache: <what now stays cached on a code-only edit>
- security: <non-root? secrets removed? healthcheck added?>