16. Cheat sheet
Docker · 16. Cheat sheetOne page. Everything from the previous fifteen chapters, arranged for looking things up rather than reading straight through.
The mental model
Section titled “The mental model” AN IMAGE a stack of content-addressed filesystem diffs + a config blob (CMD, ENV, USER, …)
A CONTAINER a Linux PROCESS with: namespaces what it can SEE cgroups what it can USE capabilities what it may DO a pivoted root filesystem
DOCKER CLI → dockerd → containerd → shim → runc → your processDockerfile
Section titled “Dockerfile”# syntax=docker/dockerfile:1
FROM node:20-slim AS buildWORKDIR /appCOPY package.json package-lock.json ./ # manifests FIRSTRUN --mount=type=cache,target=/root/.npm npm ciCOPY . . # source LASTRUN npm run build
FROM node:20-slimENV NODE_ENV=productionWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ci --omit=devCOPY --from=build --chown=node:node /app/dist ./distUSER nodeEXPOSE 3000ENTRYPOINT ["node", "dist/server.js"]Which instructions cost a layer
Section titled “Which instructions cost a layer” LAYER: RUN COPY ADD FREE: FROM ENV ARG WORKDIR USER EXPOSE LABEL CMD ENTRYPOINT VOLUME HEALTHCHECK STOPSIGNAL SHELLENTRYPOINT vs CMD
Section titled “ENTRYPOINT vs CMD” ENTRYPOINT = the command. CMD = its default arguments. `docker run img ARGS` replaces CMD only, never ENTRYPOINT.
ENTRYPOINT ["nginx"] CMD ["-g","daemon off;"] docker run img → nginx -g daemon off; docker run img -v → nginx -v docker run img bash → nginx bash ← NOT a shell docker run --entrypoint bash img → a shell
ALWAYS EXEC FORM: ["node","server.js"] Shell form makes /bin/sh PID 1, which does NOT forward SIGTERM.The rules
Section titled “The rules” ✓ order least-changing → most-changing ✓ COPY, not ADD ✓ apt-get update && install && rm -rf /var/lib/apt/lists — ONE RUN ✓ --no-install-recommends ✓ COPY --chown, never a following RUN chown -R ✓ USER non-root ✓ multi-stage; nothing from the build stage ships ✓ pin the base by digest for production ✗ secrets in ARG or ENV — use --mount=type=secret ✗ `rm` in a later layer (whiteouts delete nothing) ✗ VOLUME in a Dockerfile you control.dockerignore
Section titled “.dockerignore”.gitnode_modules__pycache__*.pyc.venvdisttargetcoverage*.log.envDockerfiledocker build -t app:v1 .docker build --no-cache --pull -t app:v1 . # the CI pathdocker build --target test . # one stage onlydocker build --build-arg NODE_VERSION=20 .docker build --secret id=npm_token,env=NPM_TOKEN .docker build --ssh default .docker buildx build --platform linux/amd64,linux/arm64 --push -t app:v1 .docker buildx build --progress=plain . # full output
docker buildx build \ --cache-from type=registry,ref=ghcr.io/org/app:cache \ --cache-to type=registry,ref=ghcr.io/org/app:cache,mode=max \ --push -t ghcr.io/org/app:$SHA .Cache mounts
Section titled “Cache mounts” npm --mount=type=cache,target=/root/.npm pip --mount=type=cache,target=/root/.cache/pip go --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cargo --mount=type=cache,target=/root/.cargo/registry --mount=type=cache,target=/app/target maven --mount=type=cache,target=/root/.m2 apt --mount=type=cache,target=/var/cache/apt,sharing=locked (+ remove /etc/apt/apt.conf.d/docker-clean first)Cache keys
Section titled “Cache keys” FROM the resolved base DIGEST RUN the COMMAND STRING only — never what it does COPY a hash of the file CONTENTS → the first miss invalidates everything below itdocker run -d --name api \ --restart unless-stopped \ --init \ -p 127.0.0.1:3000:3000 \ -v appdata:/data \ -e NODE_ENV=production \ --memory 512m --memory-swap 512m \ --pids-limit 200 \ --read-only --tmpfs /tmp \ --cap-drop ALL \ --security-opt no-new-privileges \ --user 1000:1000 \ myapp:v1 LIFECYCLE --rm --restart unless-stopped --init --stop-timeout 30 RESOURCES --memory --memory-swap --cpus --cpu-shares --pids-limit NETWORK -p IP:host:container --network name --add-host STORAGE -v name:/path -v /host:/path --tmpfs /tmp --read-only SECURITY --user --cap-drop ALL --cap-add X --security-opt no-new-privileges --read-onlyExit codes
Section titled “Exit codes” 0 clean 1 application error 125 DOCKER failed (bad flag) — the container never started 126 found but NOT EXECUTABLE (missing +x, or CRLF in a script) 127 NOT FOUND (typo — or a glibc binary on musl/Alpine) 137 SIGKILL → check .State.OOMKilled to tell OOM from "ignored SIGTERM" 139 SIGSEGV 143 SIGTERM → a clean, correct shutdown
exec format error → wrong architecture (arm64 vs amd64)Diagnostics, in order
Section titled “Diagnostics, in order”1 docker ps -a running? exit code?2 docker logs --tail 100 -f <c> what did it say?3 docker inspect <c> | jq '.[0].State' OOMKilled? RestartCount?4 docker exec <c> ps -eo pid,comm is PID 1 your app, or sh?5 docker exec <c> ss -lntp listening, and on what?6 docker system df is the disk the problem?7 docker events what keeps restarting it? no shell in the image (distroless/scratch): docker run --rm -it \ --network container:<id> --pid container:<id> \ --cap-add SYS_PTRACE nicolaka/netshootNetworking
Section titled “Networking” localhost inside a container = THAT CONTAINER. Never the host. EXPOSE publishes NOTHING. -p writes an iptables DNAT rule. -p 5432:5432 means 0.0.0.0 — and BYPASSES ufw/firewalld. → always -p 127.0.0.1:5432:5432
the default `bridge` network has NO DNS. → docker network create appnet, then names resolve
reaching the host: --add-host=host.docker.internal:host-gateway (and the host service must bind 0.0.0.0, not 127.0.0.1)
modes: bridge | host | none | container:<id> | macvlan | overlay container:<id> is exactly what a Kubernetes Pod isdocker network create appnetdocker network inspect appnetdocker port <c>docker exec <c> getent hosts dbdocker exec <c> nc -zv db 5432Storage
Section titled “Storage” THE WRITABLE LAYER IS NOT STORAGE — deleted with the container, slow (copy-up), and unbounded against the host disk.
-v name:/path named volume → data. The default choice. -v /host:/path bind mount → development. --tmpfs /path RAM → scratch, secrets.
an EMPTY NAMED VOLUME is seeded from the image ONCE, then masks it a BIND MOUNT never seeds — it just hides what was there the MORE SPECIFIC mount path wins: -v $(pwd):/app -v /app/node_modulesdocker volume create --label keep=true pgdatadocker volume ls / inspect / rmdocker volume prune --filter 'label!=keep=true'
# backupdocker run --rm -v pgdata:/from -v "$(pwd)":/to \ alpine tar czf /to/backup.tar.gz -C /from .# …but use pg_dump/mysqldump for a LIVE databasePermission denied on a mount: UIDs are numbers, not names.
-u "$(id -u):$(id -g)" for dev, chown on the host for a server, or a root entrypoint that chowns and exec gosus down.
Compose
Section titled “Compose”services: db: image: postgres:16 environment: { POSTGRES_PASSWORD: secret } volumes: [pgdata:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 10 start_period: 30s restart: unless-stopped
api: build: { context: ., target: dev } ports: ["127.0.0.1:3000:3000"] depends_on: db: { condition: service_healthy } init: true
volumes: pgdata:docker compose up -d --builddocker compose down # NOT -v unless you mean itdocker compose logs -f apidocker compose psdocker compose exec api sh # into the RUNNING containerdocker compose run --rm api npm test # a NEW throwaway containerdocker compose config # the merged, interpolated truthdocker compose watch # sync/rebuild on file change
depends_on alone = "start first", NOT "is ready" .env → interpolation INTO the yaml env_file → variables INTO the container merge: scalars replace, lists append, maps merge `version:` is obsolete. `docker-compose` v1 is end-of-life.Cleanup
Section titled “Cleanup”docker system df ← start here
docker image prune dangling only. safe.docker container prunedocker builder prune --filter "until=168h" --keep-storage=20gbdocker system prune --filter "until=168h"
docker system prune -a ← every unused image. Re-pulls everything.docker system prune -a --volumes ← ALSO DELETES DATABASES./etc/docker/daemon.json
Section titled “/etc/docker/daemon.json”{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "default-address-pools": [{ "base": "172.20.0.0/16", "size": 24 }], "live-restore": true, "builder": { "gc": { "enabled": true, "defaultKeepStorage": "20GB" } }, "default-ulimits": { "nofile": { "Name":"nofile","Hard":65536,"Soft":65536 } }, "registry-mirrors": ["https://mirror.internal.example.com"]}Logs are unbounded by default. default-address-pools prevents VPN/VPC collisions. live-restore keeps containers running across a daemon restart.
The failures, compressed
Section titled “The failures, compressed” stop always takes 10 s PID 1 is sh, or no SIGTERM handler zombies accumulate PID 1 does not reap → --init exits 0 immediately the process daemonized → foreground it 127 on a file that exists glibc binary on musl 126 missing +x, or CRLF line endings 137 OOM, or SIGTERM ignored (.State.OOMKilled) exec format error wrong architecture cache never hits COPY . . before the install step apt 404 on an unchanged file split apt-get update / install rebuilt image serves old a named volume masking it MODULE_NOT_FOUND after -v bind mount hid node_modules permission denied on a mount UID mismatch no space left images / containers / volumes / build cache / unrotated logs firewall says closed, isn't Docker DNAT runs before filter/INPUT internet works, one subnet no default-address-pools collision slow with no explanation CPU throttling OOM with headroom runtime read /proc/meminfo, not cgroupsSecurity minimum
Section titled “Security minimum” BUILD digest-pinned minimal base · USER non-root · multi-stage · --mount=type=secret · .dockerignore excludes .env · scan and gate on fixable HIGH/CRITICAL · scheduled --pull --no-cache rebuilds RUN --read-only --tmpfs /tmp · --cap-drop ALL · --security-opt no-new-privileges · memory/pids limits · -p bound to a specific interface · separate networks HOST the `docker` group IS the sudo group · the socket is NEVER mounted into a container · rootless or userns-remap where feasible · patch the kernel
--privileged = all caps + no seccomp + no AppArmor + all devices + writable /sys. Escape is a known short sequence.Where this connects
Section titled “Where this connects” KUBERNETES it runs these images, via containerd, not dockerd. A Pod is `--network container:<id>` plus scheduling. NGINX the canonical container workload — stateless, tiny, config from a mount. DATABASES fine in containers with a real volume and explicit memory settings, because none of them can see their own cgroup limit.- Kubernetes — what it is and why · the kubelet and the runtime · networking · storage and StatefulSets
- Nginx — what it is and why · when to use it
- PostgreSQL tuning · MySQL tuning · Redis memory · Kafka tuning · Elasticsearch tuning · MongoDB tuning · ClickHouse tuning · Cassandra tuning
The ten things worth remembering
Section titled “The ten things worth remembering”- A container is a process — namespaces, cgroups, capabilities, a pivoted root. Nothing more.
- Order the Dockerfile least-changing to most-changing. Manifests, install, then source.
- Exec form, always. Shell form makes
shPID 1 and every stop takes ten seconds. - Deleting a file in a later layer removes nothing. Secrets included.
- The writable layer is not storage. Volumes, or it is gone.
localhostinside a container is that container.-p 5432:5432is public and bypasses your firewall. Bind explicitly.- Memory limits kill; CPU limits throttle silently. Set the first; think about the second.
- The Docker socket is root on the host. No qualifier.
- Tags move. Digests do not.