Docker
#3 5 pagesDocker Reference
A production-focused Docker reference covering architecture, image layers, essential commands, Dockerfile best practices, Compose, and runtime flags.
1. Architecture
graph TD
CLI["Docker CLI<br/>(docker build/run/push)"]
DAEMON["dockerd<br/>(Docker Daemon — REST API)"]
CONTAINERD["containerd<br/>(image pull · lifecycle)"]
SHIM["containerd-shim<br/>(per-container process)"]
RUNC["runc<br/>(OCI runtime)"]
KERNEL["Linux Kernel<br/>(namespaces · cgroups)"]
CLI -->|"UNIX socket /var/run/docker.sock"| DAEMON
DAEMON -->|"gRPC"| CONTAINERD
CONTAINERD -->|"spawns"| SHIM
SHIM -->|"exec"| RUNC
RUNC -->|"creates"| KERNEL
| Layer | Responsibility |
|---|---|
| Docker CLI | User-facing commands, sends requests to daemon |
| dockerd | Orchestrates builds, networks, volumes; delegates to containerd |
| containerd | Image distribution (OCI), snapshot management, container lifecycle |
| containerd-shim | Keeps container running if containerd restarts; reports exit codes |
| runc | Low-level OCI runtime — calls clone(2) to create namespaces |
| Kernel | pid/net/mnt/uts/ipc namespaces + cgroups v2 for resource limits |
2. Image Layers & Copy-on-Write
graph BT
BASE["Layer 0: base image<br/>(debian:slim — read-only)"]
APT["Layer 1: apt install<br/>(ca-certs, tzdata — read-only)"]
APP["Layer 2: COPY binary<br/>(app binary — read-only)"]
WRITE["Container layer<br/>(writable — ephemeral)"]
BASE --> APT --> APP --> WRITE
How COW works:
- All image layers are read-only and stored once on disk (content-addressed by SHA256).
- When a container starts, a thin writable layer is stacked on top via OverlayFS (
overlay2driver). - Reading a file: OverlayFS finds it in the topmost layer that contains it — no copy needed.
- Writing/modifying a file: the file is copied up from the read-only layer into the writable layer on first write, then modified there. The original layer is untouched.
- Deleting a file: a whiteout file is created in the writable layer masking the lower layer entry.
- When the container is removed, the writable layer is discarded. Image layers remain cached.
Practical consequence: Multiple containers from the same image share all read-only layers — only the writable layer is per-container.
3. Essential Commands
Images
# Pull an image (latest tag if omitted)
docker pull golang:1.22-alpine
# Build from Dockerfile in current directory
docker build -t myapp:1.0 .
# Tag an existing image
docker tag myapp:1.0 registry.example.com/myapp:1.0
# Push to a registry
docker push registry.example.com/myapp:1.0
# List local images
docker images
docker images --filter "dangling=true" # untagged layers
# Remove an image
docker rmi myapp:1.0
docker rmi $(docker images -q --filter "dangling=true") # remove all dangling
# Show layer history + sizes
docker history myapp:1.0
# Full image metadata (JSON)
docker inspect myapp:1.0
Containers
# Run a container (detached, named, port-mapped)
docker run -d --name api -p 8080:8080 myapp:1.0
# Execute a command in a running container
docker exec -it api sh
# Stream logs (follow + timestamps)
docker logs -f --timestamps api
# List running containers; -a includes stopped
docker ps
docker ps -a
# Graceful stop (SIGTERM → waits → SIGKILL after 10s)
docker stop api
# Immediate kill (SIGKILL)
docker kill api
# Remove a stopped container
docker rm api
docker rm -f api # force-remove running container
# Low-level container metadata
docker inspect api
# Live resource usage (CPU, mem, net, block I/O)
docker stats api
# Processes inside a container
docker top api
System
# Disk usage by images / containers / volumes / build cache
docker system df
docker system df -v # verbose breakdown
# Remove stopped containers
docker container prune
# Remove unused images (dangling only)
docker image prune
docker image prune -a # all images not used by a container
# Remove everything unused (containers, networks, images, cache)
docker system prune
docker system prune -a --volumes # also volumes — destructive!
# Stream real-time Docker events
docker events
docker events --filter "type=container" --filter "event=die"
Build
# Build without cache (force fresh layer resolution)
docker build --no-cache -t myapp:1.0 .
# Build a specific stage from a multi-stage Dockerfile
docker build --target builder -t myapp:builder .
# Pass build-time variables
docker build --build-arg GO_VERSION=1.22 -t myapp:1.0 .
# Multi-platform build with buildx (requires buildx plugin)
docker buildx create --use --name multi
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-t registry.example.com/myapp:1.0 .
4. Dockerfile Best Practices (Production Go)
# syntax=docker/dockerfile:1
# ---------- build stage ----------
# Pin exact base image digest for reproducibility.
# Use alpine for small footprint; golang image has the full toolchain.
FROM golang:1.22-alpine AS builder
# Install only what the build needs. Combine RUN commands to reduce layers.
# ca-certificates needed for TLS in scratch image; git for go modules with VCS deps.
RUN apk add --no-cache ca-certificates git tzdata
WORKDIR /src
# Copy go.mod and go.sum BEFORE source code.
# This layer is cached as long as dependencies don't change — avoids re-downloading
# modules on every code change.
COPY go.mod go.sum ./
RUN go mod download
# Now copy source. Cache miss here only rebuilds the binary, not module download.
COPY . .
# CGO_ENABLED=0 → fully static binary (no libc dependency).
# -ldflags "-s -w" strips debug info and DWARF → smaller binary.
# -trimpath removes local filesystem paths from the binary.
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-trimpath \
-o /out/api \
./cmd/api
# ---------- final stage ----------
# scratch = zero-byte base. The only files are what we COPY in.
# Alternatives: gcr.io/distroless/static-debian12 (adds ca-certs + tz data).
FROM scratch AS final
# Copy trusted CA bundle so the binary can validate TLS certs.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy timezone data so time.LoadLocation works.
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy the compiled binary from the builder stage.
# COPY is preferred over ADD; ADD has implicit tar-extraction and URL-fetch behaviour
# which is surprising and rarely needed.
COPY --from=builder /out/api /api
# Run as non-root. scratch has no useradd, so use numeric UID directly.
# Principle of least privilege: a compromised process can't write to the filesystem.
USER 65532:65532
EXPOSE 8080
# ENTRYPOINT sets the executable; CMD provides default arguments.
# Using exec form (JSON array) avoids a shell wrapper — signals go directly to the
# process, which means SIGTERM is handled properly for graceful shutdown.
ENTRYPOINT ["/api"]
CMD ["--addr", ":8080"]
Key decisions explained:
| Decision | Reason |
|---|---|
| Two-stage build | Builder stage has compiler + tools; final stage is minimal, no attack surface |
| Copy go.mod first | Dependency layer cached independently from source changes |
CGO_ENABLED=0 |
Static binary works in scratch/distroless without libc |
-ldflags="-s -w" |
Strips symbol table + DWARF → 30–40% size reduction |
FROM scratch |
Zero OS — no shell, no package manager, minimal CVE surface |
| Non-root UID 65532 | Matches nonroot user in distroless; principle of least privilege |
| ENTRYPOINT + CMD | ENTRYPOINT = fixed binary; CMD = overridable defaults at docker run |
Follow-up: CMD vs ENTRYPOINT — exact behaviour matrix
| No ENTRYPOINT | ENTRYPOINT ["/bin/app"] |
|
|---|---|---|
| No CMD | Error — nothing to run | /bin/app |
CMD ["--port","8080"] |
/bin/sh -c --port 8080 (shell form) or runs cmd directly |
/bin/app --port 8080 |
docker run img --port 9090 |
Overrides CMD → runs --port 9090 |
/bin/app --port 9090 (CMD replaced) |
Key rules:
ENTRYPOINT(exec form) = the process that receives SIGTERM. Always use exec form["binary"]not shell form"binary"— shell form wraps in/bin/sh -cwhich doesn't forward signals → container ignores SIGTERM → gets SIGKILL after grace period.CMD= default arguments, overridden by anything after the image name indocker runENTRYPOINT= fixed command, overridden only with--entrypointflag
# Correct — exec form, signals go directly to /api
ENTRYPOINT ["/api"]
CMD ["--port", "8080"]
# Wrong — shell form, SIGTERM goes to /bin/sh not /api
ENTRYPOINT /api --port 8080
Follow-up: COPY vs ADD — which to use?
Always use COPY unless you specifically need ADD's extra behaviours:
COPY |
ADD |
|
|---|---|---|
| Local file → image | ✅ | ✅ |
Auto-extract .tar.gz |
❌ | ✅ |
| Fetch from URL | ❌ | ✅ (but don't — use RUN curl for layer caching) |
| Predictability | Always copies as-is | Surprising: .tar gets extracted silently |
COPY app.tar.gz /tmp/ # copies the .tar.gz file as-is
ADD app.tar.gz /tmp/ # silently extracts to /tmp/ ← surprising
Best practice: use COPY for everything. Use RUN curl | tar if you need URL-fetched archives (gives you explicit control and a single layer).
5. Docker Compose
# compose.yml
services:
api:
build:
context: .
target: final
image: myapp:local
env_file: .env # keeps secrets out of compose file
ports:
- "8080:8080"
networks:
- backend
depends_on:
postgres:
condition: service_healthy # wait for health check, not just start
redis:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.50"
memory: 256M
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
postgres:
image: postgres:16-alpine
env_file: .env # POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
volumes:
- pg_data:/var/lib/postgresql/data # named volume — survives container removal
networks:
- backend
deploy:
resources:
limits:
cpus: "1.00"
memory: 512M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
redis:
image: redis:7-alpine
command: redis-server --save 60 1 --loglevel warning
volumes:
- redis_data:/data
networks:
- backend
deploy:
resources:
limits:
cpus: "0.25"
memory: 128M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
volumes:
pg_data: # Docker manages the path; data persists across `docker compose down`
redis_data:
networks:
backend:
driver: bridge # default; containers on this network reach each other by service name
Notable patterns:
depends_onwithcondition: service_healthy— container starts only after dependency passes its health check, not just when it starts.env_file: .env— credentials never appear incompose.yml; add.envto.gitignore.- Named volumes (
pg_data,redis_data) — data survivesdocker compose down; onlydocker compose down -vremoves them. - Service discovery — containers reach each other by service name (
postgres:5432,redis:6379) via thebackendnetwork. - Resource limits prevent a runaway service from starving others on the same host.
6. Container Runtime Flags
docker run [FLAGS] IMAGE [COMMAND]
Volume & Storage
| Flag | Purpose | Example |
|---|---|---|
-v src:dst[:opts] |
Bind mount or named volume | -v /data:/app/data:ro |
--mount |
Explicit mount syntax (preferred) | --mount type=bind,src=/data,dst=/app/data,readonly |
--mount is preferred over -v for clarity — it separates type, src, dst, and options explicitly.
# Named volume
docker run --mount type=volume,src=pg_data,dst=/var/lib/postgresql/data postgres:16
# Read-only bind mount
docker run --mount type=bind,src=$(pwd)/config,dst=/config,readonly myapp
Networking
| Flag | Purpose | Example |
|---|---|---|
-p host:container |
Publish a port | -p 8080:8080 |
--network |
Attach to a network | --network backend |
--network host |
Share host network namespace | --network host |
--network none |
No network access | --network none |
Security
| Flag | Purpose | Example |
|---|---|---|
--cap-drop ALL |
Drop all Linux capabilities | --cap-drop ALL |
--cap-add NET_BIND_SERVICE |
Add back only what's needed | --cap-add NET_BIND_SERVICE |
--security-opt no-new-privileges |
Prevent privilege escalation via setuid | --security-opt no-new-privileges:true |
--security-opt seccomp=profile.json |
Apply custom seccomp filter | --security-opt seccomp=/path/to/profile.json |
--read-only |
Mount root filesystem read-only | --read-only |
-u uid:gid |
Run as specific user | -u 65532:65532 |
# Hardened container: no capabilities, no privilege escalation, read-only rootfs
docker run \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--read-only \
--tmpfs /tmp \ # writable tmpfs only where needed
-u 65532:65532 \
myapp:1.0
Resource Limits
| Flag | Purpose | Example |
|---|---|---|
--memory |
Hard memory limit | --memory 256m |
--memory-swap |
Total memory+swap limit | --memory-swap 256m (= no swap) |
--cpus |
CPU quota (fractional cores) | --cpus 0.5 |
--cpu-shares |
Relative CPU weight | --cpu-shares 512 |
--pids-limit |
Max number of processes | --pids-limit 100 |
Restart Policy
| Flag | Behavior |
|---|---|
--restart no |
Never restart (default) |
--restart on-failure[:n] |
Restart on non-zero exit; optional max retries |
--restart unless-stopped |
Always restart unless manually stopped |
--restart always |
Always restart, even after daemon restart |
# Production-like run combining several flags
docker run -d \
--name api \
-p 8080:8080 \
--network backend \
--memory 256m \
--cpus 0.5 \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--read-only \
--tmpfs /tmp \
--restart unless-stopped \
myapp:1.0
Quick Reference Card
Build cycle: docker build → docker tag → docker push
Run cycle: docker run → docker exec → docker logs → docker stop → docker rm
Debug: docker inspect · docker stats · docker top · docker events
Cleanup: docker system prune -a --volumes
Multi-platform: docker buildx build --platform linux/amd64,linux/arm64 --push
7. Storage Drivers
The storage driver controls how image layers are stacked and how COW works on disk.
| Driver | Kernel feature | Status | Use when |
|---|---|---|---|
overlay2 |
OverlayFS | Default, recommended | All modern Linux kernels (4.0+) |
devicemapper |
Device Mapper | Deprecated | Old RHEL/CentOS kernels |
btrfs |
Btrfs | Niche | Already on Btrfs filesystem |
zfs |
ZFS | Niche | Already on ZFS |
vfs |
None (full copy) | Testing only | No COW — each layer is a full copy |
overlay2 directory structure:
/var/lib/docker/overlay2/
<layer-id>/
diff/ ← actual files in this layer
link ← short ID symlink
lower ← IDs of all lower layers (colon-separated)
work/ ← OverlayFS internal use
merged/ ← union mount (only exists while container is running)
# See driver in use
docker info | grep "Storage Driver"
# Inspect a container's overlay2 layers
docker inspect api --format '{{.GraphDriver.Data}}'
# Shows: LowerDir, UpperDir, WorkDir, MergedDir
8. Build Cache Mechanics
Understanding cache invalidation prevents slow builds.
Dockerfile instruction → cache key = (parent layer hash + instruction string + context)
COPY/ADD additionally hash the file contents of what they copy.
Cache invalidation rules:
FROM golang:1.22-alpine # ← cache key: image digest
RUN apk add --no-cache git # ← only invalidated if instruction text changes
# or parent layer changes
COPY go.mod go.sum ./ # ← invalidated if go.mod OR go.sum changes
RUN go mod download # ← only reruns if COPY above was a cache miss
COPY . . # ← invalidated if ANY source file changes
RUN go build -o /app ./cmd/api # ← reruns only if COPY above was a cache miss
Why order matters: place instructions that change frequently later in the Dockerfile.
# Force no cache (e.g. to pick up security patches in base image)
docker build --no-cache .
# Use a specific cache source (BuildKit)
docker build --cache-from=registry.example.com/myapp:latest .
# BuildKit cache mounts — persist cache between builds without layer bloat
RUN --mount=type=cache,target=/root/.cache/go/pkg/mod \
go mod download
9. Health Checks
Health checks let Docker (and K8s) know if a container is actually working, not just running.
# In Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget -qO- http://localhost:8080/healthz || exit 1
# In docker-compose
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 30s # check every 30s
timeout: 5s # fail if no response in 5s
retries: 3 # mark unhealthy after 3 consecutive failures
start_period: 15s # grace period after start before failures count
States: starting → healthy / unhealthy
# Check health status
docker inspect api --format='{{.State.Health.Status}}'
docker inspect api --format='{{json .State.Health}}' | jq
# In Compose: depends_on with condition: service_healthy
# waits for health check to pass before starting dependent service
Health check in scratch/distroless images: no wget/curl available. Use a tiny Go binary:
// cmd/healthcheck/main.go
func main() {
resp, err := http.Get("http://localhost:8080/healthz")
if err != nil || resp.StatusCode != 200 { os.Exit(1) }
}
COPY --from=builder /out/healthcheck /healthcheck
HEALTHCHECK CMD ["/healthcheck"]