Base image & preparation
Use a minimal base image
Prefer the smallest base image that provides everything your application needs.
| Image | Approx. size | Use case |
|---|---|---|
| Alpine | ~5 MB | Small general-purpose runtime |
| Slim | ~50 MB | Smaller Debian-based runtime |
| Distroless | Minimal | Production runtime without shell/tools |
| Scratch | 0 MB | Statically linked binaries |
A smaller image has less surface area for something to go wrong.
Pin versions — never use :latest
The latest tag is mutable. It can point to a different image tomorrow, and your build stops being reproducible.
For maximum reproducibility, pin the image digest as well:
FROM python:3.11.4-slim@sha256:...Use a .dockerignore file
Exclude unnecessary and sensitive files from the build context: .git, node_modules, .env, logs, local configuration, build artifacts.
This reduces the build context and helps prevent sensitive files from accidentally being copied into the image.
.git
node_modules
.env
*.logBuild optimization
Optimize layer order & caching
Docker caches layers. If a layer changes, all subsequent layers need to be rebuilt.
Put rarely changing things first. Put frequently changing code last.
This allows Docker to reuse the dependency layer when only application code changes.
Combine RUN commands & clean caches
Each RUN instruction creates a new layer. Deleting a file in a later layer does not remove it from previous layers.
Create → use → clean up in the same layer.
Prefer COPY over ADD
ADD has additional behavior, such as extracting local tar archives and fetching remote URLs. COPY is simple and predictable.
If you need to download something, do it explicitly:
RUN curl -fsSL https://example.com/file.tar.gz \
| tar -xz -C /appUse ADD only when you specifically need its additional features.
Use multi-stage builds
Build the application in a full development image and run it in a minimal runtime image. Build environment ≠ runtime environment.
The final image contains only the application and its runtime dependencies.
Use BuildKit
BuildKit is Docker’s modern build engine — better caching, parallel builds, and support for cache and secret mounts.
Modern Docker versions use BuildKit by default. For older installations:
export DOCKER_BUILDKIT=1
docker build .Don’t run as root
Running an application as root inside a container increases the potential impact of a container compromise. Follow the principle of least privilege.
Use build secrets — not ARG / ENV
Do not pass secrets through ARG or ENV. Both end up baked into image metadata and build history, so anyone with access to the image can pull them back out.
The secret is mounted only for the duration of the build step and is not copied into the resulting image.
Stability
PID 1 & graceful shutdown
The first process inside a container becomes PID 1 and has special responsibilities, including signal handling. A shell used as the main process may not properly forward signals to the application.
Use exec in shell entrypoints:
exec node app.jsThis replaces the shell process with the application. Alternatively, use a minimal init system such as Tini:
ENTRYPOINT ["/sbin/tini", "--"]Tini handles signal forwarding and reaps zombie processes.
Use HEALTHCHECK
A running process does not necessarily mean that the application is healthy.
Add a health check:
HEALTHCHECK \
--interval=30s \
--timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1| Option | Meaning |
|---|---|
--interval=30s | Check every 30 seconds |
--timeout=3s | Fail after 3 seconds |
exit 1 | Report unhealthy |
Make sure curl or another health-check tool is available in the image.
Use Hadolint
Hadolint is a static analyzer for Dockerfiles. It catches the stuff from this guide automatically: bad layer order, missing cleanup, :latest tags, unnecessary packages.
Run locally:
hadolint DockerfileRun in CI:
docker run --rm -i hadolint/hadolint < DockerfileComplete production Dockerfile
Here’s a production-ready example for a Node.js + TypeScript app, putting everything above into one Dockerfile: multi-stage build, non-root user, health check, and dumb-init for signal handling.
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=22.14.0
# Stage 1: Dependencies
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Stage 2: Build
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Stage 3: Production dependencies
FROM node:${NODE_VERSION}-alpine AS prod-deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev \
&& npm cache clean --force
# Stage 4: Production
FROM node:${NODE_VERSION}-alpine AS production
WORKDIR /app
# Install a minimal init system for proper signal handling
RUN apk add --no-cache dumb-init
# Create a non-root user
RUN addgroup -S app \
&& adduser -S app -G app
# Copy only what is required at runtime
COPY --from=prod-deps --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --chown=app:app package*.json ./
USER app
ENV NODE_ENV=production
EXPOSE 3000
# Container health check
HEALTHCHECK \
--interval=30s \
--timeout=3s \
--start-period=10s \
--retries=3 \
CMD node -e \
"fetch('http://127.0.0.1:3000/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/main.js"].git
.gitignore
node_modules
npm-debug.log*
.env
.env.*
!.env.example
dist
coverage
Dockerfile
docker-compose*.yml
*.log
.DS_StoreBuild and run:
docker build -t my-app:1.0.0 .
docker run \
--rm \
-p 3000:3000 \
my-app:1.0.0What makes this production-oriented?
Key principle: build with everything you need, run with only what you need. Build environment ≠ production environment.
Quick reference
| Rule | Prefer |
|---|---|
| Base image | Minimal image |
| Image tags | Pinned versions / digests |
| Build context | .dockerignore |
| Layer order | Rarely changing → frequently changing |
| File copying | COPY |
| Production image | Multi-stage build |
| Build engine | BuildKit |
| User | Non-root |
| Secrets | BuildKit secrets |
| Entrypoint | exec / Tini / dumb-init |
| Container health | HEALTHCHECK |
| Dockerfile linting | Hadolint |
Further reading
For more container best practices, and skills built specifically for AI coding agents working on containerized applications, see the official Docker Skills repository.