Docker Wiki
A complete, opinionated reference for the world's most popular container platform — from docker run hello-world to multi-stage production builds, Compose stacks, and Swarm orchestration. Neo-brutalist, dark-mode aware, keyboard friendly.
01 Introduction
Docker is an open platform for developing, shipping, and running applications inside portable, lightweight containers. A container packages an application together with all of its dependencies — libraries, system tools, code, and runtime — into a single standardized unit that runs the same way on any host with a container engine installed.
Containers are conceptually similar to virtual machines, but they share the host operating system's kernel and run as isolated user-space processes. This makes them dramatically lighter: a typical container image is tens of megabytes and starts in milliseconds, while a full VM carries a multi-GB hypervisor payload and takes tens of seconds to boot.
Fast
Sub-second startup. No guest OS overhead. Layer-cached builds make iteration instant.
Portable
Build once, run anywhere — laptop, CI runner, bare metal, cloud VM, Kubernetes cluster.
Reproducible
The image is the build artifact. The same image produces the same container on every host.
Composable
Multi-service apps modeled as small cooperating containers linked by networks and volumes.
Ecosystem
Docker Hub hosts millions of ready-to-use images, and OCI compliance keeps the format open.
Tooling
First-class CLI, Compose for stacks, Scout for security, BuildKit for fast caching builds.
Docker turns the question "works on my machine" into "works on every machine" by packaging the runtime environment with the application.
02 Why Docker?
The problem Docker solves
Before containers, deploying a service meant provisioning a machine, installing an OS, configuring the runtime, installing dependencies, and hoping the application would behave the same way it did on the developer's laptop. Drift between environments — different glibc versions, missing shared libraries, conflicting system services — caused most production outages of the 2010s.
Docker collapses that gap. The image is the environment. If it works in CI, it will work in production.
Containers vs. virtual machines
- App A · App B · App C
- Guest OS · Guest OS · Guest OS
- Hypervisor
- Host OS
- Infrastructure
- App A · App B · App C
- Container engine
- Host OS
- Infrastructure
- App A · App B · App C
- Container engine (no privs)
- Host OS kernel
- Infrastructure
Common use cases
- Microservices — one container per service, independently scalable.
- CI/CD — ephemeral, identical runners; ship the same image from build to deploy.
- Dev environments —
docker compose upand the team has the same stack locally. - Legacy lift-and-shift — package an old app with its ancient OS libs and run it on modern hardware.
- ML / data science — ship a CUDA-pinned notebook image with exact dependency versions.
- Edge & IoT — tiny base images on constrained devices.
03 Architecture
Docker Engine components
The Docker Engine is a client–server application with three main pieces:
- Image pull / push
- Container lifecycle
- Image transfer
- OCI runtime reference
- Creates the actual container process
- namespaces
- cgroups
- union filesystems
Linux kernel primitives Docker relies on
- Namespaces — give each container its own view of
pid,net,mnt,uts,ipc, anduser. - cgroups — limit and account for CPU, memory, I/O, PIDs per container.
- Union filesystems —
overlay2,fuse-overlayfs; layer images on top of each other copy-on-write. - Capabilities & seccomp — drop the privileges a process doesn't need.
What lives where
| Component | Location | Purpose |
|---|---|---|
| Images | /var/lib/docker/overlay2/ | Read-only layers + writable container layer |
| Volumes | /var/lib/docker/volumes/ | Persistent data bypassing the union FS |
| Networks | kernel objects (bridge, veth, iptables) | Connect containers & the outside world |
| Build cache | /var/lib/docker/buildkit/ | Per-layer cache used by BuildKit |
| Logs | /var/lib/docker/containers/<id>/ | JSON log driver output |
04 Core Concepts
Image
An image is a read-only, layered filesystem bundle plus metadata (env, cmd, labels, exposed ports). Images are immutable; any change creates a new image. They are stored in registries and identified by a digest (sha256) or a tag (mutable pointer).
Container
A container is a running instance of an image — an isolated process tree on the host with its own namespaces, a thin writable layer on top of the image layers, and configured resources. Containers are disposable by design.
Layer
Each instruction in a Dockerfile produces a layer. Layers are content-addressed by hash and shared between images and containers, which is why pulling node:20 the second time is essentially instant. The union filesystem stitches them into the final rootfs.
Registry vs. repository vs. image
- Registry — the storage service (Docker Hub, GHCR, ECR, GCR, Harbor, …).
- Repository — a collection of image variants, e.g.
library/nginx. - Image — a specific tagged/digested variant, e.g.
nginx:1.27-alpine@sha256:….
Tag & digest
# A tag is a movable pointer
nginx:1.27
# A digest is immutable — always pulls the same bytes
nginx@sha256:42d6f9d6c2a5e8b9b4c0d5d3b5a4b1f9e0c8d7b6a5f4e3d2c1b0a9f8e7d6c5b4
Dockerfile
A text recipe that describes how to assemble an image. Each line is a layer; the order matters for cache reuse.
Compose
A declarative YAML file describing a multi-container application. Compose replaces running a dozen docker run commands by hand.
Volume & network
First-class objects managed by the engine. Volumes outlive containers and hold state; networks give containers stable DNS names and isolation.
05 Installation
Use the official docker install one-liner rather than distro packages — it follows engine releases and configures the systemd unit correctly.
Linux (Debian / Ubuntu)
# Add Docker's official GPG key
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# Install
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# (Optional) run docker as your user
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world
Linux (RHEL / Fedora / Amazon Linux)
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
docker run hello-world
macOS & Windows
- Docker Desktop — recommended for laptops. Bundles Engine, CLI, Compose, Kubernetes, and a managed VM (on macOS/Windows) plus a GUI dashboard. Free for personal use and small businesses; subscription required for larger orgs.
- Rancher Desktop — open-source alternative, also ships a managed VM + k3s.
- Colima — macOS-only, lightweight, no GUI.
- WSL 2 on Windows — install the Docker Engine directly inside the WSL distro for a near-native Linux experience.
Linux post-install: add yourself to the docker group to avoid sudo, but be aware this is roughly equivalent to passwordless root. Prefer rootless mode on shared hosts.
Verify the install
docker --version
docker compose version
docker info
docker run --rm hello-world
06 Quick Start
Hello, world
docker run --rm hello-world
# Docker pulls the image, starts a container, prints a greeting, and removes it.
# Behind the scenes: containerd downloads layers, runc creates namespaces,
# the binary inside the image runs, the container exits.
Run an interactive shell in Ubuntu
docker run --rm -it ubuntu:24.04 bash
# -i keep STDIN open
# -t allocate a pseudo-TTY
# --rm remove the container when it exits
Run a long-lived web server
docker run -d --name web -p 8080:80 nginx:1.27-alpine
# -d detached (background)
# --name human-readable name
# -p publish: host:container port
docker logs -f web
docker ps
curl -sI http://localhost:8080
docker stop web && docker rm web
Think of an image as a class and a container as an instance. Stopping a container doesn't delete the image, and removing a container doesn't touch the image.
07 CLI Reference
The Docker CLI follows a verb-noun pattern: docker <command> [options] [arguments]. Below is a curated tour of the most-used commands.
The essentials
| Command | What it does |
|---|---|
docker ps | List running containers (add -a for all) |
docker images | List local images (alias: docker image ls) |
docker pull <ref> | Download an image (or part of run) |
docker run [opts] <image> [cmd] | Create + start a new container |
docker start <name> | Start an existing stopped container |
docker stop <name> | SIGTERM, then SIGKILL after grace period |
docker kill <name> | SIGKILL immediately |
docker rm <name> | Delete a stopped container |
docker rmi <image> | Delete an image |
docker logs [-f] <name> | Show container logs |
docker exec -it <name> sh | Run a command in a running container |
docker inspect <obj> | Low-level JSON metadata |
docker stats | Live resource usage per container |
docker system df | Disk usage breakdown |
docker system prune -a | Garbage-collect everything unused |
Flags you'll reach for daily
| Flag (short) | Long form | Effect |
|---|---|---|
-d | --detach | Run in the background, print container ID |
-it | --interactive --tty | Keep STDIN open and attach a TTY |
--rm | — | Auto-remove container on exit |
--name | — | Give the container a name |
-p | --publish | HOST:CONTAINER port mapping |
-v | --volume | Mount a volume or host path |
-e | --env | Set an environment variable |
--env-file | — | Load env vars from a file |
--restart | — | Policy: no | on-failure | always | unless-stopped |
--network | — | Attach to a specific network |
--memory / --cpus | — | Resource limits |
--read-only | — | Make the rootfs read-only |
--user | — | Run as UID:GID (drop root) |
--cap-drop / --cap-add | — | Fine-tune Linux capabilities |
Shell-friendly tricks
# Get the IP of the last started container
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -q | head -1)
# Run a command in every running container
docker ps -q | xargs -I{} docker exec {} sh -c 'echo running in $(hostname)'
# Tail logs of every container that has a label
docker logs -f $(docker ps -q --filter label=app=web)
# Grep container image for a string (build context only)
grep -rIl "TODO" .
# Pretty-print running processes across all containers
for c in $(docker ps -q); do
echo "=== $c ==="
docker top $c
done
08 Image Workflow
Pull, tag, push
# 1) Pull an image
docker pull python:3.12-slim
# 2) Run a quick experiment
docker run --rm python:3.12-slim python -c 'print(2 ** 32)'
# 3) Tag for your registry
docker tag python:3.12-slim ghcr.io/yourorg/myapp:0.1.0
# 4) Push
docker push ghcr.io/yourorg/myapp:0.1.0
Save & load (offline transfer)
docker save -o myapp.tar ghcr.io/yourorg/myapp:0.1.0
# ship the tarball by sneakernet
docker load -i myapp.tar
Image history & inspection
docker history --no-trunc ghcr.io/yourorg/myapp:0.1.0
docker inspect ghcr.io/yourorg/myapp:0.1.0
dive ghcr.io/yourorg/myapp:0.1.0 # interactive layer explorer
Cleaning up
# Remove dangling images
docker image prune
# Remove every image not used by a container
docker image prune -a
# Remove stopped containers, unused networks, dangling images, build cache
docker system prune
# Same plus unused images
docker system prune -a --volumes
docker system prune -a --volumes is destructive. In CI runners, run it on schedule; on dev machines, save a tag you might want first.
09 Container Operations
Lifecycle
| State | How you get there |
|---|---|
| created | docker create — image instantiated, no process started |
| running | docker start / docker run |
| paused | docker pause — SIGSTOP the whole cgroup |
| exited | main process ended (graceful or crash) |
| restarting | policy is always/unless-stopped and exit was non-zero |
| dead | engine couldn't stop it — investigate docker inspect |
Resource limits
docker run -d --name api \
--cpus="1.5" \
--memory="512m" \
--memory-swap="512m" \
--pids-limit=200 \
--restart=unless-stopped \
myapi:1.0
Useful sub-commands
docker top <name> # processes inside the container
docker port <name> # port mappings
docker diff <name> # filesystem changes vs. the image
docker rename <old> <new> # rename
docker update <name> ... # change limits/restart policy live
docker wait <name> # block until exit, print code
docker attach <name> # attach stdio to PID 1
docker cp <name>:/path ./ # copy files in/out
docker commit <name> img # snapshot a container's writable layer (avoid in prod)
The first process inside a container is special: it receives signals, its exit code becomes the container's exit code, and zombie reaping depends on it. Use a tiny init (tini, dumb-init) or pass --init for clean shutdowns.
10 Data & Volumes
By default, data written inside a container lives in its thin writable layer and is lost when the container is removed. To persist data, mount it from outside.
The three mount types
Volumes
Managed by Docker under /var/lib/docker/volumes/. Best for databases, queues, and app state. Portable across hosts, backup-friendly, drivers for NFS/SSH/cloud.
Bind mounts
Map an arbitrary host path into the container. Best for dev — edit code on the host, see it instantly in the container. Production risk: leaks host layout into the app.
tmpfs
RAM-backed, never touches disk. Good for secrets, scratch space, fast caches that you don't want persisted.
Volume patterns
# Named volume — Docker manages location
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16
# Bind mount — host path into container
docker run -d --name dev -v $PWD/src:/app/src myapp:dev
# tmpfs
docker run -d --name cache --tmpfs /tmp:rw,size=64m myapi:1.0
# Anonymous volume (auto-created, hard to clean up — usually avoid)
docker run -d --name old -v /data alpine
# Read-only bind mount (config injection)
docker run -d --name web -v $PWD/nginx.conf:/etc/nginx/nginx.conf:ro nginx:1.27
# Inspect
docker volume ls
docker volume inspect pgdata
docker volume rm pgdata
Backup & restore a volume
# Backup
docker run --rm \
-v pgdata:/from:ro \
-v $PWD:/to \
alpine tar -czf /to/pgdata.tgz -C /from .
# Restore
docker run --rm \
-v pgdata:/to \
-v $PWD:/from \
alpine tar -xzf /from/pgdata.tgz -C /to
Volume drivers
For real workloads, use a driver that places data on networked, replicated storage:
local— default, host-local (single point of failure).rexray,convoy— EBS, ScaleIO, etc.azurefile,ebs,gce-pd— managed cloud block storage.- For Kubernetes, use the
CSIdriver set, not Docker volume drivers.
11 Networking
Driver overview
| Driver | Scope | When to use |
|---|---|---|
bridge | Single host | Default. Containers on the same bridge can resolve each other by name. |
host | Single host | No namespace isolation. Best for high-throughput or port-heavy workloads. |
none | Single host | No networking. Used for batch jobs. |
overlay | Multi-host (Swarm) | Encrypted VXLAN between nodes. Default for Swarm services. |
macvlan | Single host | Container gets a MAC on the physical LAN. Useful for legacy apps that expect to be on the wire. |
ipvlan | Single host | Like macvlan but shares the host's MAC. Lighter, more restricted. |
User-defined bridge networks give you DNS
The default bridge has a single shared IP range; containers reach each other only by IP. Create a user-defined network to get automatic service discovery by container name.
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name api --network appnet -p 8080:8080 myapi:1.0
docker run -d --name web --network appnet -p 80:80 nginx:1.27-alpine
# Inside the api container, 'db' resolves via embedded DNS
docker exec api getent hosts db
Port publishing reference
-p 8080:80 # host:container TCP
-p 8080:80/udp # UDP
-p 127.0.0.1:8080:80 # bind to a host IP only (safer)
-p 80 # ephemeral host port, container port 80
--expose 8080 # document a port without publishing
-P # publish every EXPOSE port on a random host port
Debugging
docker network ls
docker network inspect appnet
docker exec api cat /etc/resolv.conf
docker exec api ip route
docker exec api ss -tlnp
docker exec api tcpdump -i any -n port 5432 # if tcpdump is installed
12 Dockerfile
Anatomy of a Dockerfile
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12 AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/public ./public
USER nonroot
EXPOSE 3000
CMD ["dist/server.js"]
Every instruction, in one table
| Instruction | What it does |
|---|---|
FROM | Base image. Use a tag, not latest. --platform=$BUILDPLATFORM for cross-builds. |
RUN | Execute a command at build time. Prefer exec form [...]. |
CMD | Default command. Easily overridden. Prefer exec form. |
ENTRYPOINT | Main executable. CMD becomes its default args. |
LABEL | Metadata. Used by scanners, registry search, org policies. |
ENV | Persistent env var for the running container. |
ARG | Build-time variable. Use --build-arg to set it. |
WORKDIR | Set cwd. Prefer absolute paths. |
COPY | Copy local files in. Honors .dockerignore. |
ADD | Like COPY, plus auto-extract of local tarballs and remote URL fetch. Avoid for normal use. |
VOLUME | Declare a mount point. Mostly superseded by -v at run time. |
EXPOSE | Document a port. Doesn't actually publish it. |
USER | Switch UID. Use a high UID (e.g. 65532) — root inside a container is still root on the host namespace. |
HEALTHCHECK | Built-in liveness probe. --interval, --timeout, --retries. |
SHELL | Change the default shell for RUN (e.g. SHELL ["/bin/bash", "-o", "pipefail", "-c"]). |
STOPSIGNAL | Override SIGTERM (the default) for docker stop. |
ONBUILD | Trigger when this image is used as a base. Powerful, easy to misuse — prefer explicit. |
Multi-stage builds
Multi-stage builds let you compile in one stage and copy only the artifacts into a tiny runtime stage. The end image contains only what the app needs at runtime — no compilers, no headers, no source.
# syntax=docker/dockerfile:1.7
# ----- build -----
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app .
# ----- runtime -----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
BuildKit features worth knowing
- Cache mounts —
RUN --mount=type=cache,target=/root/.cache npm cipersists cache between builds. - Secret mounts —
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci; secrets never end up in a layer. - SSH mounts —
RUN --mount=type=ssh git clone …. - Build args from environment —
ARG BUILDKIT_INLINE_CACHE=1. - Parallel stages — BuildKit builds stages that don't depend on each other in parallel.
- Multi-platform —
docker buildx build --platform=linux/amd64,linux/arm64 ….
The .dockerignore file
.git
node_modules
dist
build
*.log
.env
.env.*
!.env.example
.DS_Store
coverage
.vscode
.idea
*.md
!README.md
docker-compose*.yml
Best practices cheat sheet
- Order instructions from least changing to most changing for cache reuse.
- Pin base images to a specific digest or versioned tag.
- Run as a non-root
USER. - Combine
apt-get update && apt-get installin oneRUN; clean/var/lib/apt/lists/*after. - Use
--no-install-recommendsfor Debian installs. - Use multi-stage builds to drop build tooling from the final image.
- Set
HEALTHCHECKfor any long-running service. - Document the port with
EXPOSE, even if you don't-p. - Add a one-line
LABEL org.opencontainers.image.source=…andorg.opencontainers.image.licenses=….
13 Docker Compose
Compose is a declarative spec for multi-container apps. A single docker-compose.yml describes services, networks, volumes, secrets, configs, and how they relate. The docker compose plugin replaces the old Python-based docker-compose.
A full-stack example
name: myapp
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-app}
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 10
secrets:
- db_password
api:
build:
context: .
dockerfile: Dockerfile
args:
BUILDKIT_INLINE_CACHE: 1
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://app:$(cat /run/secrets/db_password)@db:5432/app
ports:
- "127.0.0.1:8080:8080"
secrets:
- db_password
web:
image: nginx:1.27-alpine
restart: unless-stopped
depends_on:
- api
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
worker:
build: .
restart: unless-stopped
depends_on:
db:
condition: service_healthy
command: ["node", "dist/worker.js"]
environment:
DATABASE_URL: postgres://app:$(cat /run/secrets/db_password)@db:5432/app
secrets:
- db_password
volumes:
pgdata:
secrets:
db_password:
file: ./secrets/db_password.txt
Common Compose commands
docker compose up -d # create + start
docker compose down # stop + remove containers + network
docker compose down -v # also remove volumes
docker compose ps # list service status
docker compose logs -f api # follow logs
docker compose exec api sh # shell in service
docker compose run --rm migrate # one-off task
docker compose build --pull # rebuild, refreshing base images
docker compose pull # pull all service images
docker compose config # validate + render the merged config
docker compose profile # activate a profile (e.g. --profile debug)
Profiles, env files, and overrides
services:
api:
build: .
profiles: ["app"]
debug-tools:
image: nicolaka/netshoot
profiles: ["debug"]
network_mode: service:api
# Run only the debug profile:
# docker compose --profile debug up -d
# Use multiple compose files:
# docker compose -f compose.yml -f compose.override.yml -f compose.prod.yml up -d
Compose v2 on a single host is fine for small services. For multi-host, high-availability, or autoscaling, reach for Swarm or Kubernetes. The same compose.yml is a good starting point for a kompose convert migration.
14 Registries
Public registries
- Docker Hub —
docker.io— the default. Free public repos, paid private repos, autobuilds. - GitHub Container Registry —
ghcr.io— tightly integrated with GitHub Actions. - GitLab Container Registry —
registry.gitlab.com. - Quay.io — by Red Hat, with Clair vulnerability scanning.
- Amazon ECR, Azure ACR, Google Artifact Registry — cloud-native, IAM-integrated.
Run your own
The Distribution project (formerly Docker Registry) is the OCI-compliant reference implementation:
docker run -d \
-p 5000:5000 \
--name registry \
--restart=always \
-v registry-data:/var/lib/registry \
registry:3
Harbor — the enterprise option
If you need a UI, replication, vulnerability scanning, signed image policies, project-level RBAC, and OIDC, deploy Harbor on top of the registry.
Logging in
docker login # Docker Hub
docker login ghcr.io # GHCR
docker login registry.example.com # self-hosted
# Reads creds from $HOME/.docker/config.json (or OS keychain)
Working with image digests
Tags are mutable. Pin to a digest in CI and deployments for fully reproducible builds:
# After pushing myapp:1.0, ask the registry for the digest
docker buildx imagetools inspect myapp:1.0
# Pull / run by digest
docker pull myapp@sha256:4f3a…c0
docker run myapp@sha256:4f3a…c0
15 Security
Containers are not a security boundary by themselves. They give you a start at isolation, but the configuration matters as much as the kernel features underneath.
The hardening checklist
- Keep the engine up to date. Most container escapes in the wild are against known, patched vulnerabilities.
- Pin base images by digest.
FROM debian:12-slim@sha256:…so a registry republish can't sneak a backdoor into your build. - Use minimal bases.
alpine,distroless, orscratch— every package is attack surface. - Don't run as root.
USER 65532; drop all capabilities you don't need; turn on--security-opt=no-new-privileges. - Mount the FS read-only with
--read-only; onlyVOLUMEdirs are writable. - Use multi-stage builds to leave compilers out of the runtime image.
- Scan images.
docker scout cves, Trivy, Grype, Snyk, Clair. Block PRs on critical CVEs. - Sign images. Docker Content Trust / sigstore cosign — verify before deploy.
- Don't bake secrets into images. Use
--mount=type=secretat build, runtime secrets via the orchestrator. - Use user-defined networks — never put databases on the default
bridgewith public-p. - Limit resources.
--memory,--cpus,--pids-limitprevent noisy neighbors and fork bombs. - Use rootless Docker for untrusted workloads and CI runners.
A hardened docker run for a web service
docker run -d --name api \
--read-only \
--user 10001:10001 \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
--security-opt=seccomp=/etc/docker/seccomp/default.json \
--pids-limit=200 \
--memory=512m --cpus=1.0 \
--tmpfs /tmp:size=64m,mode=1777 \
--network appnet \
myorg/api@sha256:4f3a…c0
Scanning
# Built-in
docker scout cves myorg/api:1.0
docker scout recommendations myorg/api:1.0
# Trivy (third-party, very thorough)
trivy image --severity HIGH,CRITICAL myorg/api:1.0
# install: brew install trivy | apt install trivy | binary from github
# Grype (Anchore)
grype myorg/api:1.0
Docker Content Trust (image signing)
export DOCKER_CONTENT_TRUST=1
docker push myorg/api:1.0 # prompts for signing key passphrase
# On the consumer side, the same env var refuses any unsigned image
16 Performance
Image size
Smaller images pull faster, scan faster, and have less attack surface. Practical levers:
- Choose a slim or distroless base.
- Use multi-stage builds.
- Combine
RUNsteps; clean package manager caches in the same layer. - Use
--mount=type=cachefor package manager caches instead of stuffing them in/root. - Use
.dockerignoreto keep the build context small.
Layer caching
Docker (and BuildKit) cache each layer. The order of COPY and RUN lines determines how much gets invalidated on each build:
# BAD: code change invalidates npm ci
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
# GOOD: dependency layers survive code changes
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN --mount=type=cache,target=/app/.cache npm run build
Runtime performance
| Knob | Effect |
|---|---|
| Storage driver | overlay2 is the default and the fastest on modern kernels. |
| CPU shares vs cpuset | --cpus for a soft pool share; --cpuset-cpus to pin. |
| Memory + swap | Set --memory-swap equal to --memory to disable swap (recommended for latency-sensitive apps). |
| Logging driver | json-file with logrotate-friendly max-size; consider local for higher throughput. |
| Init process | --init for clean signal handling; minimal overhead. |
| CPU features | Build for the target arch (--platform=linux/amd64); emulating arm64 on amd64 is slow. |
Inspecting what an image actually contains
docker image inspect myorg/api:1.0 --format '{{.Size}}'
docker history myorg/api:1.0 --no-trunc
dive myorg/api:1.0 # TUI layer browser
docker save myorg/api:1.0 | tar -tv # list every file
docker run --rm myorg/api:1.0 ls -la /app
17 Swarm & Orchestration
Docker Swarm in 30 seconds
Swarm turns a pool of Docker hosts into a single virtual engine. Services (replicated or global) replace single containers. The routing mesh, secret store, and overlay network are built in.
# On the first manager
docker swarm init --advertise-addr <manager-ip>
# On each additional manager / worker
docker swarm join --token <token> <manager-ip>:2377
# Deploy a stack from compose
docker stack deploy -c compose.yml mystack
docker stack services mystack
docker stack ps mystack
docker service scale mystack_api=5
docker service rollback mystack_api
# Inspect / leave
docker node ls
docker service ls
docker swarm leave --force
Swarm features worth knowing
- Services — replicated (N copies) or global (one per node).
- Rolling updates —
--update-parallelism,--update-delay,--update-order. - Placement constraints —
--constraint node.labels.disk==ssd. - Secrets & configs — encrypted at rest, mounted in
/run/secrets/<name>. - Overlay networks — encrypted by default; service discovery baked in.
- Healthchecks + update failure action —
--update-failure-action=rollback.
When to graduate to Kubernetes
Swarm is great for small-to-mid fleets where you want zero-fuss HA without learning a new declarative model. Once you outgrow it — usually around: tens of nodes, complex stateful workloads, mixed cloud/on-prem, custom controllers — Kubernetes becomes the better choice. The Docker ecosystem now leans toward Kubernetes for orchestration; Swarm is in maintenance mode but still supported.
Kubernetes
The industry default. Massive ecosystem, but a steep learning curve. Use a managed flavor (EKS, GKE, AKS) unless you have a strong reason not to.
Swarm
Built into Docker. Dead simple. Great for a few hosts. No new DSL beyond compose.yml.
Nomad
HashiCorp's lightweight orchestrator. Single binary, language-agnostic, integrates with Consul and Vault.
ECS / Fargate
AWS-native. Tight IAM, no cluster to manage on Fargate. Less portable.
18 Production Patterns
12-factor, container-style
- One process per container. If you need a sidecar (log shipper, agent), run it as another container.
- Config from environment. Never bake URLs, creds, or feature flags into the image.
- Stateless services. Anything that needs to live across restarts goes into a volume, a database, or an object store.
- Logs to stdout/stderr. Let the engine driver ship them to your log pipeline.
- Fast start, fast stop.
SIGTERM→ drain →SIGKILLin <30 s.
Healthchecks
HEALTHCHECK --interval=10s --timeout=2s --start-period=20s --retries=3 \
CMD curl -fsS http://localhost:8080/healthz || exit 1
Graceful shutdown
Your process should:
- Trap
SIGTERM. - Stop accepting new work.
- Finish in-flight requests.
- Close DB connections.
- Exit 0 within the orchestrator's grace period (default 10 s, raise with
--stop-timeout).
Init systems and zombies
Container PID 1 must reap zombies. Two safe choices:
# Use the built-in tini (one flag, no image changes)
docker run --init myorg/api:1.0
# Or build a tiny init image
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /out/app /app
# distroless ships no shell, so tini must be PID 1 in another stage
Observability
- Expose Prometheus metrics on a separate
--expose'd port; or use the OpenTelemetry collector as a sidecar. - Centralize logs with the
fluentd/awslogs/gelf/syslogdrivers. - Emit a correlation ID; propagate it through every call.
- Set resource requests and limits — the absence of limits is the most common cause of node-level incidents.
CI/CD with Docker
# .github/workflows/build.yml (excerpt)
name: build
on: [push]
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: type=sha,format=long
- uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
19 Troubleshooting
The escape hatches
| Symptom | First thing to try |
|---|---|
| Container exits immediately | docker logs <name>, then docker inspect <name> → look at State.Error and Args. |
| App can't reach the database | Same user-defined network? docker network inspect; docker exec api nslookup db. |
| "Permission denied" on a volume | UID mismatch. stat the file, run the container with the matching --user. |
| Build is super slow / fills the disk | Add .dockerignore; check for huge COPY . . lines; use --mount=type=cache. |
| Out of disk on the host | docker system df and docker system prune -a --volumes (carefully). |
| DNS is broken in containers | --dns 1.1.1.1, or fix the host's resolv.conf / systemd-resolved. |
| Process consumes 100% CPU | docker top <name>; docker exec -it <name> sh → apk add htop / apt install htop. |
| App in container sees stale time | Bind-mount /etc/localtime from the host, or use tzdata in the image and set TZ. |
| Container can't write to bind mount | Host SELinux / AppArmor. :z / :Z on the mount. |
| "no space left on device" | docker system df; usually BuildKit cache. docker builder prune -af. |
Get a shell in a stuck container
# If the container is running
docker exec -it --user root <name> sh
# If it's not running, override the entrypoint
docker run -it --entrypoint sh <image>
# If you only have a stopped container
docker run -it --rm --volumes-from <name> alpine sh
# If the process is wedged and won't take signals
docker kill <name> # SIGKILL
# or, for a deeper look:
docker run --pid=container:<name> --net=container:<name> --ipc=container:<name> alpine sh -c 'apk add strace && strace -p 1'
Reset everything (last resort)
# Stop everything & remove all containers, networks, images, volumes, build cache
docker stop $(docker ps -aq) 2>/dev/null
docker system prune -a --volumes
# Nuclear option: remove the data root (Linux)
sudo systemctl stop docker
sudo rm -rf /var/lib/docker
sudo systemctl start docker
20 Cheat Sheet
Images
docker build -t app:1.0 . # build from Dockerfile in cwd
docker build -f Dockerfile.prod -t app:1.0 .
docker buildx build --platform=linux/amd64,linux/arm64 -t app:1.0 --push .
docker pull nginx:1.27-alpine
docker images
docker rmi app:1.0
docker image prune -a
docker tag app:1.0 ghcr.io/me/app:1.0
docker push ghcr.io/me/app:1.0
docker save -o app.tar app:1.0
docker load -i app.tar
docker history app:1.0
docker image inspect app:1.0
Containers
docker run -d --name api -p 8080:8080 -e NODE_ENV=production app:1.0
docker ps
docker ps -a
docker logs -f api
docker exec -it api sh
docker stop api
docker start api
docker restart api
docker kill api
docker rm api
docker rm -f api # SIGKILL + remove
docker top api
docker stats
docker inspect api
docker diff api
docker cp api:/etc/config.json .
docker update --memory=1g api
docker rename api api-v1
Volumes & networks
docker volume create data
docker volume ls
docker volume inspect data
docker volume rm data
docker network create appnet
docker network ls
docker network inspect appnet
docker network connect appnet <container>
docker network disconnect appnet <container>
docker network rm appnet
Compose (one-liners)
docker compose up -d
docker compose down -v
docker compose ps
docker compose logs -f
docker compose exec api sh
docker compose build --pull
docker compose pull
docker compose run --rm migrate
docker compose config | yq '.services.api.image'
21 Resources
Official
- docs.docker.com — the canonical reference, kept current.
- github.com/moby/moby — the engine source (read it for the deepest answers).
- BuildKit — the build engine;
docker buildxuses it. - compose-spec — the Compose file format, versioned.
- opencontainers.org — the OCI image & runtime specs.
Books
- Docker Deep Dive — Nigel Poulton. The classic zero-to-hero.
- Docker in Practice — Ian Miell & Aidan Hobson Sayers. Real recipes.
- Kubernetes Patterns — Bilgin Ibryam & Roland Huß. If you graduate from Compose.
Tools you should know
dive
Interactive image layer explorer. dive myorg/api:1.0.
Trivy / Grype
Vulnerability scanners. Wire into CI to fail builds on critical CVEs.
hadolint
Dockerfile linter. Catches common mistakes before you build.
docker-scout
Built-in CVE analysis and recommendations. docker scout cves.
ctop / lazydocker
Terminal UIs for monitoring and managing containers.
skopeo / crane
Inspect, copy, and sign images without needing a Docker daemon.
cosign (sigstore)
Sign and verify OCI images. The modern replacement for DCT.
kompose
Convert compose.yml to Kubernetes manifests. kompose convert.
Glossary
| Term | Meaning |
|---|---|
| OCI | Open Container Initiative — the standards body that defines image and runtime formats. |
| runc | The reference OCI runtime; what actually creates a container process. |
| containerd | The daemon-level component that pulls, stores, and starts containers. |
| BuildKit | The next-generation build engine, used by docker buildx. |
| CRI / OCI / CNI | Kubernetes interfaces — Container Runtime, Image, Network. |
| rootless | Running the engine (or individual containers) without root on the host. |
| overlay2 | The default and recommended storage driver on modern Linux. |
| seccomp | A Linux facility for syscall filtering — the default Docker profile blocks ~50 dangerous syscalls. |
| AppArmor / SELinux | Mandatory access control — Docker can label mounts so only the right container reads them. |
You now have the same mental model most platform engineers use day-to-day. The next time something breaks, run docker inspect before you run docker rm -f — and remember: the image is the artifact, the container is disposable, and the data lives in a volume.