✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17 Docker Best Practices

A focused guide to Docker Best Practices, connecting core concepts with practical Docker and container operations.

Docker best practices are the accumulated, broadly applicable conventions for building images, configuring containers, and operating them in production that consistently reduce the most common categories of problem, oversized and insecure images, fragile deployments, lost data, invisible failures, encountered across real-world Docker usage, serving as a starting checklist rather than a substitute for understanding the specific reasoning behind each one.

Building smaller, more secure images

Choosing a minimal base image, using multi-stage builds to exclude build-only tooling from the final image, and running as a non-root user are foundational practices that reduce both image size and attack surface simultaneously:

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
RUN useradd --create-home appuser
COPY --from=build --chown=appuser:appuser /app/dist ./dist
USER appuser
CMD ["node", "dist/server.js"]

This pattern excludes the entire build toolchain, source maps, and development dependencies from the final image, while also ensuring the running process operates with restricted, non-root privileges rather than full root access by default.

Structuring instructions for effective layer caching

Ordering Dockerfile instructions from least to most frequently changing, dependency manifests before source code, source code before final build steps, maximizes how often a build can reuse cached layers rather than re-executing expensive steps unnecessarily:

COPY package*.json .
RUN npm ci
COPY . .

This ordering ensures a source code change, the most frequent kind of change, only invalidates the final COPY and whatever follows it, leaving the considerably more expensive dependency installation step cached and reused.

One process per container

Designing each container to run a single, well-defined process, rather than bundling multiple unrelated services into one container through a shell script or process supervisor, keeps logging, health checking, scaling, and restart behavior clean and independently manageable for each component:

CMD ["node", "server.js"]

This convention is not an absolute, universal rule, legitimate exceptions exist, but it should be the default assumption, with any deviation a deliberate, justified choice rather than an accidental consequence of convenience.

Explicit resource limits

Setting memory and CPU limits explicitly, rather than leaving containers unbounded, prevents any single container from consuming more of a host's shared resources than intended, protecting every other workload on the same host:

docker run -d --memory=512m --cpus=1 my-api

Sizing these limits based on actual, observed usage under realistic load, rather than guessing, avoids both unnecessary throttling from limits set too conservatively and insufficient protection from limits set too generously.

Meaningful health checks

A health check that actually exercises real dependencies, not just confirming the process is running, provides a genuinely useful signal for restart decisions, traffic routing, and deployment safety gates:

HEALTHCHECK --interval=15s --timeout=5s --retries=3 --start-period=30s \
  CMD curl -f http://localhost:3000/healthz || exit 1

Pairing this with deliberately chosen timing parameters, based on the application's actual measured startup time and response latency rather than copied defaults, makes the resulting health signal both fast to detect genuine problems and tolerant of normal, expected variance.

Proper persistence for anything that matters

Any data that genuinely needs to survive a container's replacement belongs on an explicitly named volume, never on the container's own disposable writable layer, and that volume needs an actual, tested backup process rather than an assumption that persistence alone is sufficient protection:

docker run -d -v pgdata:/var/lib/postgresql/data postgres
docker run --rm -v pgdata:/data:ro -v "$(pwd)":/backup alpine \
  tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /data .

Graceful shutdown handling

Applications should handle SIGTERM explicitly, finishing in-flight work and closing connections cleanly within the container's configured stop timeout, rather than relying on the eventual SIGKILL to terminate them forcibly:

process.on('SIGTERM', () => {
  server.close(() => process.exit(0));
});

Using an init process like tini (or Docker's own --init flag) ensures signals are correctly forwarded to the application even when an intermediate shell or wrapper process would otherwise interfere with that delivery.

Secrets handled correctly, never as plain environment variables

Credentials should be supplied through mounted secret files or a dedicated secrets manager, never as plain environment variables, which are visible through docker inspect and inherited by every child process:

services:
  api:
    secrets:
      - db_password

Centralized, structured logging with bounded retention

Logging to stdout and stderr, in structured (typically JSON) format, with rotation limits configured either per-container or as a daemon-wide default, prevents both unbounded local disk consumption and produces output that a centralized aggregation system can actually index and query effectively:

{
  "log-driver": "local",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Routine, scheduled maintenance

Pruning unused images, stopped containers, and stale build cache on a regular, scheduled basis prevents the gradual accumulation of disk bloat that would otherwise eventually escalate into a disruptive, reactive disk exhaustion incident:

0 3 * * 0 docker system prune -af --filter "until=168h"

Treating these as defaults, not absolutes

Every practice listed here has legitimate exceptions, and understanding the underlying reasoning behind each, rather than applying it mechanically without that understanding, is what allows a sound, deliberate decision about when a specific situation genuinely warrants deviating from the default.

Common mistakes

  • Running every container as root by default, rather than treating non-root execution as the expected, default configuration.
  • Bundling unrelated processes into a single container for convenience, rather than treating one-process-per-container as the default assumption.
  • Leaving containers without explicit resource limits, allowing any single one to consume more shared host capacity than intended.
  • Storing genuinely important data without an explicit, named volume and a separately tested backup process.
  • Treating these practices as a rigid checklist to be applied mechanically, rather than understanding the reasoning behind each one well enough to judge when a specific exception is actually warranted.

Docker best practices function best as a well-understood starting point, smaller and more secure images, deliberate caching structure, meaningful health checks, proper persistence and secret handling, and routine maintenance, applied with genuine understanding of the reasoning behind each, rather than as an unthinking checklist disconnected from the specific context a given application and deployment actually requires.

Content in this section