v282026
🐳 Engine 📦 Containers 🔧 Compose 🚀 Swarm 🛡 Hardened

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.

TL;DR

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

Virtual Machines
  • App A · App B · App C
  • Guest OS · Guest OS · Guest OS
  • Hypervisor
  • Host OS
  • Infrastructure
Hybrid (common)
  • App A · App B · App C
  • Container engine
  • Host OS
  • Infrastructure
Native (rootless / sysbox)
  • 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 environmentsdocker compose up and 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:

docker CLIdocker run, build, …
REST APIUNIX socket / TCP
dockerddaemon · manages images, containers, networks, volumes
containerd
  • Image pull / push
  • Container lifecycle
  • Image transfer
runc
  • OCI runtime reference
  • Creates the actual container process
Linux kernel
  • namespaces
  • cgroups
  • union filesystems

Linux kernel primitives Docker relies on

  • Namespaces — give each container its own view of pid, net, mnt, uts, ipc, and user.
  • cgroups — limit and account for CPU, memory, I/O, PIDs per container.
  • Union filesystemsoverlay2, 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

ComponentLocationPurpose
Images/var/lib/docker/overlay2/Read-only layers + writable container layer
Volumes/var/lib/docker/volumes/Persistent data bypassing the union FS
Networkskernel 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.

Image Atag: v1.2
container-1writable layer
container-2writable layer
container-3writable layer

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

bash
# 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

Pro tip

Use the official docker install one-liner rather than distro packages — it follows engine releases and configures the systemd unit correctly.

Linux (Debian / Ubuntu)

bash
# 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)

bash
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.
Heads up

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

bash
docker --version
docker compose version
docker info
docker run --rm hello-world

06 Quick Start

Hello, world

bash
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

bash
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

bash
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
Mental model

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

CommandWhat it does
docker psList running containers (add -a for all)
docker imagesList 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> shRun a command in a running container
docker inspect <obj>Low-level JSON metadata
docker statsLive resource usage per container
docker system dfDisk usage breakdown
docker system prune -aGarbage-collect everything unused

Flags you'll reach for daily

Flag (short)Long formEffect
-d--detachRun in the background, print container ID
-it--interactive --ttyKeep STDIN open and attach a TTY
--rmAuto-remove container on exit
--nameGive the container a name
-p--publishHOST:CONTAINER port mapping
-v--volumeMount a volume or host path
-e--envSet an environment variable
--env-fileLoad env vars from a file
--restartPolicy: no | on-failure | always | unless-stopped
--networkAttach to a specific network
--memory / --cpusResource limits
--read-onlyMake the rootfs read-only
--userRun as UID:GID (drop root)
--cap-drop / --cap-addFine-tune Linux capabilities

Shell-friendly tricks

bash
# 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

bash
# 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)

bash
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

bash
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

bash
# 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
Prune with care

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

created
running
paused
stopped
removed
StateHow you get there
createddocker create — image instantiated, no process started
runningdocker start / docker run
pauseddocker pause — SIGSTOP the whole cgroup
exitedmain process ended (graceful or crash)
restartingpolicy is always/unless-stopped and exit was non-zero
deadengine couldn't stop it — investigate docker inspect

Resource limits

bash
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

bash
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)
PID 1 matters

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

bash
# 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

bash
# 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 CSI driver set, not Docker volume drivers.

11 Networking

Driver overview

DriverScopeWhen to use
bridgeSingle hostDefault. Containers on the same bridge can resolve each other by name.
hostSingle hostNo namespace isolation. Best for high-throughput or port-heavy workloads.
noneSingle hostNo networking. Used for batch jobs.
overlayMulti-host (Swarm)Encrypted VXLAN between nodes. Default for Swarm services.
macvlanSingle hostContainer gets a MAC on the physical LAN. Useful for legacy apps that expect to be on the wire.
ipvlanSingle hostLike 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.

bash
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

bash
-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

bash
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

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

InstructionWhat it does
FROMBase image. Use a tag, not latest. --platform=$BUILDPLATFORM for cross-builds.
RUNExecute a command at build time. Prefer exec form [...].
CMDDefault command. Easily overridden. Prefer exec form.
ENTRYPOINTMain executable. CMD becomes its default args.
LABELMetadata. Used by scanners, registry search, org policies.
ENVPersistent env var for the running container.
ARGBuild-time variable. Use --build-arg to set it.
WORKDIRSet cwd. Prefer absolute paths.
COPYCopy local files in. Honors .dockerignore.
ADDLike COPY, plus auto-extract of local tarballs and remote URL fetch. Avoid for normal use.
VOLUMEDeclare a mount point. Mostly superseded by -v at run time.
EXPOSEDocument a port. Doesn't actually publish it.
USERSwitch UID. Use a high UID (e.g. 65532) — root inside a container is still root on the host namespace.
HEALTHCHECKBuilt-in liveness probe. --interval, --timeout, --retries.
SHELLChange the default shell for RUN (e.g. SHELL ["/bin/bash", "-o", "pipefail", "-c"]).
STOPSIGNALOverride SIGTERM (the default) for docker stop.
ONBUILDTrigger 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.

dockerfile
# 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 mountsRUN --mount=type=cache,target=/root/.cache npm ci persists cache between builds.
  • Secret mountsRUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci; secrets never end up in a layer.
  • SSH mountsRUN --mount=type=ssh git clone ….
  • Build args from environmentARG BUILDKIT_INLINE_CACHE=1.
  • Parallel stages — BuildKit builds stages that don't depend on each other in parallel.
  • Multi-platformdocker buildx build --platform=linux/amd64,linux/arm64 ….

The .dockerignore file

gitignore
.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 install in one RUN; clean /var/lib/apt/lists/* after.
  • Use --no-install-recommends for Debian installs.
  • Use multi-stage builds to drop build tooling from the final image.
  • Set HEALTHCHECK for any long-running service.
  • Document the port with EXPOSE, even if you don't -p.
  • Add a one-line LABEL org.opencontainers.image.source=… and org.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

yaml
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

bash
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

yaml
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 in production?

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 Hubdocker.io — the default. Free public repos, paid private repos, autobuilds.
  • GitHub Container Registryghcr.io — tightly integrated with GitHub Actions.
  • GitLab Container Registryregistry.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:

bash
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

bash
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:

bash
# 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, or scratch — 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; only VOLUME dirs 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=secret at build, runtime secrets via the orchestrator.
  • Use user-defined networks — never put databases on the default bridge with public -p.
  • Limit resources. --memory, --cpus, --pids-limit prevent noisy neighbors and fork bombs.
  • Use rootless Docker for untrusted workloads and CI runners.

A hardened docker run for a web service

bash
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

bash
# 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)

bash
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 RUN steps; clean package manager caches in the same layer.
  • Use --mount=type=cache for package manager caches instead of stuffing them in /root.
  • Use .dockerignore to 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:

dockerfile
# 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

KnobEffect
Storage driveroverlay2 is the default and the fastest on modern kernels.
CPU shares vs cpuset--cpus for a soft pool share; --cpuset-cpus to pin.
Memory + swapSet --memory-swap equal to --memory to disable swap (recommended for latency-sensitive apps).
Logging driverjson-file with logrotate-friendly max-size; consider local for higher throughput.
Init process--init for clean signal handling; minimal overhead.
CPU featuresBuild for the target arch (--platform=linux/amd64); emulating arm64 on amd64 is slow.

Inspecting what an image actually contains

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

bash
# 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 → SIGKILL in <30 s.

Healthchecks

dockerfile
HEALTHCHECK --interval=10s --timeout=2s --start-period=20s --retries=3 \
  CMD curl -fsS http://localhost:8080/healthz || exit 1

Graceful shutdown

Your process should:

  1. Trap SIGTERM.
  2. Stop accepting new work.
  3. Finish in-flight requests.
  4. Close DB connections.
  5. 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:

bash
# 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 / syslog drivers.
  • 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

git push
CI build & scan
push to registry
sign image
deploy
yaml
# .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

SymptomFirst thing to try
Container exits immediatelydocker logs <name>, then docker inspect <name> → look at State.Error and Args.
App can't reach the databaseSame user-defined network? docker network inspect; docker exec api nslookup db.
"Permission denied" on a volumeUID mismatch. stat the file, run the container with the matching --user.
Build is super slow / fills the diskAdd .dockerignore; check for huge COPY . . lines; use --mount=type=cache.
Out of disk on the hostdocker 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% CPUdocker top <name>; docker exec -it <name> shapk add htop / apt install htop.
App in container sees stale timeBind-mount /etc/localtime from the host, or use tzdata in the image and set TZ.
Container can't write to bind mountHost 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

bash
# 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)

bash
# 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

bash
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

bash
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

bash
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)

bash
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

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

TermMeaning
OCIOpen Container Initiative — the standards body that defines image and runtime formats.
runcThe reference OCI runtime; what actually creates a container process.
containerdThe daemon-level component that pulls, stores, and starts containers.
BuildKitThe next-generation build engine, used by docker buildx.
CRI / OCI / CNIKubernetes interfaces — Container Runtime, Image, Network.
rootlessRunning the engine (or individual containers) without root on the host.
overlay2The default and recommended storage driver on modern Linux.
seccompA Linux facility for syscall filtering — the default Docker profile blocks ~50 dangerous syscalls.
AppArmor / SELinuxMandatory access control — Docker can label mounts so only the right container reads them.
That's a wrap

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.