Skip to content

16. Cheat sheet

One page. Everything from the previous fifteen chapters, arranged for looking things up rather than reading straight through.

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 process
# syntax=docker/dockerfile:1
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./ # manifests FIRST
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . . # source LAST
RUN npm run build
FROM node:20-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
ENTRYPOINT ["node", "dist/server.js"]
LAYER: RUN COPY ADD
FREE: FROM ENV ARG WORKDIR USER EXPOSE LABEL CMD ENTRYPOINT
VOLUME HEALTHCHECK STOPSIGNAL SHELL
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.
✓ 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
.git
node_modules
__pycache__
*.pyc
.venv
dist
target
coverage
*.log
.env
Dockerfile
docker build -t app:v1 .
docker build --no-cache --pull -t app:v1 . # the CI path
docker build --target test . # one stage only
docker 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 .
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)
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 it
docker 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-only
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)
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/netshoot
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 is
docker network create appnet
docker network inspect appnet
docker port <c>
docker exec <c> getent hosts db
docker exec <c> nc -zv db 5432
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_modules
docker volume create --label keep=true pgdata
docker volume ls / inspect / rm
docker volume prune --filter 'label!=keep=true'
# backup
docker 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 database

Permission 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.

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 --build
docker compose down # NOT -v unless you mean it
docker compose logs -f api
docker compose ps
docker compose exec api sh # into the RUNNING container
docker compose run --rm api npm test # a NEW throwaway container
docker compose config # the merged, interpolated truth
docker 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.
docker system df ← start here
docker image prune dangling only. safe.
docker container prune
docker builder prune --filter "until=168h" --keep-storage=20gb
docker system prune --filter "until=168h"
docker system prune -a ← every unused image. Re-pulls everything.
docker system prune -a --volumes ← ALSO DELETES DATABASES.
{
"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.

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 cgroups
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.
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.
  1. A container is a process — namespaces, cgroups, capabilities, a pivoted root. Nothing more.
  2. Order the Dockerfile least-changing to most-changing. Manifests, install, then source.
  3. Exec form, always. Shell form makes sh PID 1 and every stop takes ten seconds.
  4. Deleting a file in a later layer removes nothing. Secrets included.
  5. The writable layer is not storage. Volumes, or it is gone.
  6. localhost inside a container is that container.
  7. -p 5432:5432 is public and bypasses your firewall. Bind explicitly.
  8. Memory limits kill; CPU limits throttle silently. Set the first; think about the second.
  9. The Docker socket is root on the host. No qualifier.
  10. Tags move. Digests do not.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source