Docker Engine Best Practices
A practical checklist for running Docker Engine on build hosts and CI runners with predictable, secure, and reproducible behavior.
How to Use This List
Treat these as defaults for any host that builds or runs containers outside a Kubernetes cluster.
Adopt them incrementally: version pinning and logging first, then security and resource hygiene.
Remember the boundary: these harden Docker Engine for build and dev. In production, pods run on containerd via CRI, configured separately.
A - Version and Reproducibility
Pin the Engine version on build hosts. Install a specific Docker Engine release rather than the rolling latest, so a surprise upgrade cannot change build behavior overnight.
Pin the Engine version in CI. Record the exact Engine and BuildKit versions your pipeline uses; verify with docker version at the start of a run.
Never build against latest base images. Reference a concrete tag or, better, a digest (image@sha256:...) so rebuilds are reproducible.
Standardize the config with automation. Template daemon.json with Ansible or cloud-init instead of hand-editing hosts, so the fleet stays identical.
Enable BuildKit consciously. BuildKit is the default backend; rely on its cache but pin builder versions when reproducibility matters.
B - Security
Prefer rootless Docker on shared hosts. Running the daemon in a user namespace shrinks the blast radius of a container escape.
Guard the Docker socket. Access to /var/run/docker.sock is effectively root on the host; never expose it to untrusted containers or over the network unauthenticated.
Use mTLS for any TCP endpoint. If you must expose the API over TCP, require mutual TLS on port 2376; an open TCP daemon is remote root.
Avoid --privileged and dropped isolation. Grant specific capabilities instead of blanket privilege, and keep no-new-privileges on.
Scan and sign images. Run Trivy or Grype in CI and sign releases with cosign so nodes can verify provenance before running them.
C - Resource and Log Hygiene
Bound container logs. Set the local log driver with max-size and max-file so a chatty container cannot fill the disk.
Watch /var/lib/docker capacity. Layers, writable container filesystems, and volumes all live here; monitor disk and inodes.
Prune on a schedule. Run docker system prune (carefully, with filters) to reclaim dangling images and stopped containers on build hosts.
Set default ulimits. Configure nofile and similar limits in daemon.json so builds and containers do not exhaust host descriptors.
Cap concurrency where needed. Tune max-concurrent-downloads/uploads on constrained networks to avoid saturating bandwidth.
D - Operations and Reliability
Turn on live restore. "live-restore": true keeps containers running across daemon upgrades on single-host deployments.
Manage cgroups via systemd. Use the systemd cgroup driver so Docker and init agree on resource ownership, especially under cgroup v2.
Use a registry mirror in CI. A pull-through cache cuts external bandwidth and speeds pipelines.
Validate daemon.json before reload. A malformed file stops the daemon from starting; lint the JSON and check journalctl -u docker.
Avoid subnet collisions. Set default-address-pools to ranges that do not clash with your VPN or corporate networks.
E - Boundaries and Portability
Do not treat Docker as the cluster runtime. dockershim is gone; nodes run containerd or CRI-O. Build with Docker, deploy to containerd.
Build multi-arch images. Use buildx to produce amd64 and arm64 manifests so laptops and nodes run the correct binary.
Keep config out of images. Inject configuration and secrets at runtime; bake only immutable application artifacts into the image.
Mirror local resource limits to manifests. The --memory/--cpus you use locally should inform the requests/limits you set in Kubernetes.
When You Are Done
You should have pinned Engine versions, bounded logs, a hardened socket, and automated config across every build host.
Your images should be digest-pinned, scanned, signed, and multi-arch, ready for containerd to run in the cluster.
Re-audit after each Engine upgrade, since defaults and features shift between releases.
FAQs
Why pin the Engine version at all? Build behavior, BuildKit cache semantics, and defaults can change across releases; pinning keeps CI reproducible.
Is rootless Docker production-ready? Yes for many build and dev uses, with some networking and cgroup caveats. Test your workload's needs.
Should I expose the Docker API over TCP? Only with mutual TLS. Prefer SSH-based Docker contexts, which need no open ports.
How do I stop logs from filling the disk? Use the local driver with max-size and max-file, then recreate long-lived containers.
Do these practices apply to Kubernetes nodes? No. Nodes run containerd via CRI. These target Docker build and dev hosts.
How often should I prune? On busy build hosts, a scheduled docker system prune with filters daily or weekly prevents disk exhaustion.
Related
- How the Docker Engine Works
- Docker Engine Basics
- Essential CLI Commands
- Docker Contexts
- Engine Configuration
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).