A good Dockerfile is clear, repeatable, and produces a small, secure image. Here’s how to write one the right way.
Core syntax & instructions
- FROM base: Choose a minimal, trusted base (e.g.,
alpineor an official distro). Example:FROM python:3.12-slim. - WORKDIR /app: Set the working directory for subsequent instructions and runtime.
- COPY package*.json ./ : Copy only what you need; use
.dockerignoreto keep the context small. - RUN commands: Install deps, build assets. Chain related commands to reduce layers and clean caches (e.g.,
apk add --no-cache ...). - ENV VAR=value: Set environment variables for runtime defaults.
- EXPOSE 3000: Document the port (doesn’t publish it).
- USER appuser: Drop root; run as a non-root user.
- CMD ["node","server.js"]: Default runtime command (only one CMD is used—the last one).
- ENTRYPOINT ["myapp"]: The fixed executable; combine with CMD for default args.
- HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1: Define a container health probe.
Order matters (cache-friendly layout)
- FROM (base image)
- WORKDIR
- Copy dependency manifests first (e.g.,
package*.json,requirements.txt) - RUN install deps (leverages cache if manifests unchanged)
- COPY the rest of the source
- RUN build/compile steps
- ENV, EXPOSE, USER, HEALTHCHECK
- CMD/ENTRYPOINT
Multi-stage builds (preferred)
Use a builder stage to compile, then copy only the outputs to a slim runtime stage.
# Stage 1: build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: runtime
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node","dist/server.js"]
Benefits: smaller images, no build tools in production, faster pulls.
ENTRYPOINT vs CMD
- ENTRYPOINT sets the binary; CMD sets default args.
- Together:
ENTRYPOINT ["myapp"]+CMD ["--port","8080"]→ default commandmyapp --port 8080; users can override CMD args.
Security & hygiene
- Use minimal trusted bases; pin versions.
- Don’t bake secrets into images; use env/secret stores at runtime.
- Run as non-root; set USER.
- Keep
.dockerignoreupdated to avoid shipping build artifacts and secrets. - Add
HEALTHCHECKfor better orchestration behavior. - Label images (
LABEL org.opencontainers.image.source=…) for traceability.
Common pitfalls
- Using ADD when COPY is enough (avoid unintended URL fetch/extract).
- Reinstalling deps every build because COPY order changed—copy manifests first.
- Leaving build tools in the runtime image—use multi-stage.
- Running as root in production—set USER.
Quick checklist
- Minimal base + multi-stage
- Cached deps (copy manifests early)
- Non-root USER, HEALTHCHECK
- Clean build artifacts; small runtime image
- .dockerignore in place; no secrets in image
Follow this structure and you’ll get lean, secure images that build fast and run predictably in CI/CD.