../

Docker

Running, wiring and inspecting containers with the Docker CLI and Compose. Targets Docker Engine 29.x, Docker Desktop 4.9x and Compose v5 (the successor to Compose v2, same docker compose command). Writing images lives in Dockerfile.

Concepts

TermWhat it is
Imageread-only stack of layers plus config (entrypoint, env, user); addressed by name:tag or @sha256: digest
Layercontent-addressed filesystem diff; shared between images and cached by the builder
Containeran image plus a thin writable layer, running as isolated processes (namespaces, cgroups)
Registryserver that stores images: Docker Hub, GHCR, ECR, Artifact Registry, a private registry:3
Repositorynamed set of tags in a registry, e.g. ghcr.io/acme/api
VolumeDocker-managed storage that outlives containers
Bind mounta host path mounted into a container
Networkvirtual network with its own DNS; containers on it reach each other by name
Manifest list / indexone tag pointing at per-platform images (linux/amd64, linux/arm64)
Contextwhich daemon the CLI talks to (docker context ls, use)

Engine 29 uses the containerd image store by default on new installs, which is what lets a local daemon hold multi-platform images and attestations.

Running containers

docker run -d --name api -p 3000:3000 \
  -e NODE_ENV=production --restart unless-stopped \
  ghcr.io/acme/api:1.4.2
FlagEffect
-ddetach, print the container ID
-itinteractive + TTY, for shells and REPLs
--rmdelete the container when it exits
--name apifixed name, also its DNS name on user-defined networks
-p 8080:80publish container port 80 on host port 8080 (all interfaces)
-p 127.0.0.1:8080:80publish on loopback only
-Ppublish every EXPOSEd port on random host ports
-v vol:/datanamed volume
-v "$PWD":/appbind mount (use --mount for strict, explicit syntax)
-e KEY=val / -e KEYset / pass through an env var
--env-file .envload KEY=val lines
-w /appworking directory
-u 1000:1000run as UID:GID
--network appattach to a network
--entrypoint shoverride ENTRYPOINT (args after the image replace CMD)
--initrun a tiny init (tini) as PID 1 to forward signals and reap zombies
--platform linux/amd64pick a platform (emulated if it differs from the host)
--pull alwaysre-check the registry before starting
--add-host h:host-gatewaymap a hostname to the host's IP
--gpus allexpose GPUs (NVIDIA Container Toolkit)

Inspecting and interacting

CommandUse
docker ps / ps -arunning / all containers
docker ps -q -f status=exitedIDs of exited containers (filters: name=, label=, ancestor=)
docker logs -f --tail 100 apifollow the last 100 lines; --since 10m, -t timestamps
docker exec -it api shshell in a running container; -u root, -e, -w
docker inspect apifull JSON: mounts, networks, state, health
docker inspect -f '{{.State.Status}}' apione field via a Go template
docker stats --no-streamCPU, memory, net, block I/O snapshot
docker top apiprocesses inside
docker port apipublished port mappings
docker cp api:/app/out.log .copy out (or ./f api:/tmp/ to copy in)
docker diff apifiles changed in the writable layer
docker events --since 1hdaemon event stream
docker update --memory 1g apichange limits and restart policy live

Container lifecycle

create ──► created ──start──► running ──stop/kill──► exited ──rm──► gone
                               │  ▲                     │
                          pause│  │unpause        start │ (restart)
                               ▼  │                     ▼
                              paused                 running
CommandWhat happens
docker createbuild the container, don't start it
docker start / -astart (and attach)
docker stop -t 30 apiSIGTERM (or the image's STOPSIGNAL), SIGKILL after the timeout (default 10 s)
docker kill -s HUP apisend a signal now (default SIGKILL)
docker restart apistop + start
docker pause / unpausefreeze with the cgroup freezer
docker wait apiblock until exit, print the exit code
docker rm -f apikill and remove; -v also drops anonymous volumes
Exit codeMeaning
0clean exit
1app error
125docker run itself failed (bad flag, name taken)
126 / 127command not executable / not found
137SIGKILL: OOM kill (State.OOMKilled) or stop timeout
143SIGTERM honored

Images, builds & registries

CommandUse
docker imageslocal images; --filter dangling=true
docker pull node:24-slimfetch; --platform linux/arm64
docker tag api ghcr.io/acme/api:1.4.2add a name to an existing image
docker push ghcr.io/acme/api:1.4.2upload (needs docker login)
docker rmi api:olduntag / delete; -f if in use
docker history apilayers and the instruction that made each
docker save api -o api.tar / load -imove images without a registry
docker image inspect apiconfig, digest, platform
docker login ghcr.iostore registry credentials (--password-stdin)
Build commandUse
docker build -t api .build from ./Dockerfile with BuildKit
-f docker/Dockerfile.prodother Dockerfile
--target buildstop at a stage
--build-arg VERSION=1.4.2set an ARG
--secret id=npmrc,src=.npmrcmount a secret into RUN steps
--ssh defaultforward the SSH agent
--no-cache / --pullignore cache / refresh base images
--progress=plainfull log output
--checkrun build checks only (lint)
--output type=local,dest=outexport files instead of an image
--cache-to type=registry,ref=…,mode=maxshare cache via a registry (type=gha in GitHub Actions)
docker buildx ls / create --uselist / add builders
docker buildx bakebuild targets from docker-bake.hcl or a compose file
docker buildx imagetools inspect imgshow a remote manifest list
docker buildx du / prunebuild cache usage / cleanup

Tags and names

ReferenceResolves to
nodedocker.io/library/node:latest
acme/api:1.4docker.io/acme/api:1.4
ghcr.io/acme/api:1.4.2GitHub Container Registry
123456789012.dkr.ecr.eu-west-1.amazonaws.com/apiAWS ECR
europe-docker.pkg.dev/proj/repo/apiGoogle Artifact Registry
api@sha256:…one immutable image; tags can move, digests can't

Push an exact version plus moving aliases (1.4.2, 1.4, 1) and a commit tag (sha-3f9c2e1). latest is only the default tag name, not "newest"; don't deploy it. Log in to Docker Hub even for public images, because anonymous pulls are rate-limited.

echo "$GITHUB_TOKEN" | docker login ghcr.io \
  -u "$GITHUB_ACTOR" --password-stdin

Volumes & mounts

KindSyntaxLivesUse for
Named volume-v pgdata:/var/lib/postgresqlDocker-manageddatabases, anything that must persist
Anonymous volume-v /data or image VOLUMEuntil rm -v / prunethrowaway state
Bind mount-v ./src:/app/srchost pathdev source code, config files
tmpfs--tmpfs /tmp:size=64mmemoryscratch space with --read-only
Image mount--mount type=image,source=img,target=/xanother imageread-only data from an image
docker volume create pgdata
docker volume ls
docker volume inspect pgdata
docker run --rm -v pgdata:/v alpine du -sh /v
 
# explicit form; fails if the host path is missing
docker run --mount \
  type=bind,src="$PWD"/config,dst=/etc/app,readonly \
  api
GotchaFix
Bind mount hides files baked into the image at that pathmount a subfolder, or use a named volume
Named volume is seeded from the image only when emptydelete the volume to re-seed
-v ./x:/y with a missing ./x creates a root-owned diruse --mount, which errors instead
Permission denied as non-rootmatch UIDs (-u "$(id -u):$(id -g)") or chown in the image
Slow bind mounts on macOSkeep node_modules in a volume, or sync with Compose watch
Suffixes:ro read-only; :z / :Z SELinux relabel

Networking

DriverBehavior
bridge (default network)NAT'd private network; no DNS between containers
user-defined bridgesame, plus embedded DNS (127.0.0.11): containers resolve each other by name and alias
hostshare the host's network stack; no port mapping (Linux; opt-in setting on Docker Desktop)
noneloopback only
overlaymulti-host networks (Swarm)
macvlan / ipvlancontainer gets its own address on the LAN
docker network create app
docker run -d --name db --network app postgres:18
docker run --rm --network app postgres:18 \
  pg_isready -h db            # "db" resolves via DNS
docker network connect app api    # add a 2nd network
docker network inspect app
NeedHow
Container → containersame user-defined network, use the container or service name
Extra DNS names--network-alias search (Compose: aliases:)
Container → hosthost.docker.internal (Desktop); on Linux add --add-host host.docker.internal:host-gateway
Host → containerpublish with -p; EXPOSE alone publishes nothing
Share another container's network--network container:api (debug sidecars)
Keep a DB off the LAN-p 127.0.0.1:5432:5432, or don't publish at all

Environment & secrets

MechanismScopeNotes
-e / environment:container runtimeshows in docker inspect, not for secrets
--env-file / env_file:container runtimeplain KEY=value lines, no shell expansion
.env next to compose.yamlCompose interpolation onlyfills ${VAR} in the YAML, not the container
Compose secrets:file at /run/secrets/<name>from a file: or host environment:
Build --secretone RUN stepnever lands in a layer (Dockerfile)
ARG / ENV in a Dockerfilebaked into the imagevisible in docker history: never secrets
compose.yaml
services:
  api:
    image: ghcr.io/acme/api:1.4.2
    environment:
      LOG_LEVEL: ${LOG_LEVEL:-info}   # default
      API_URL: ${API_URL:?set API_URL} # required
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
 
secrets:
  db_password:
    file: ./secrets/db_password.txt

Compose resolves variables from the shell first, then .env (or --env-file). Write $$ for a literal dollar sign. Many official images (Postgres, MySQL) read *_FILE variants of their secrets.

Resources, restarts & health

docker runComposeEffect
-m 512mmem_limit: 512m / deploy.resources.limits.memoryhard memory cap; exceeding it means OOM kill (137)
--memory-reservation 256mmem_reservationsoft floor under memory pressure
--cpus 1.5cpus: 1.5 / deploy.resources.limits.cpusCPU quota
--cpuset-cpus 0,1cpusetpin to cores
--pids-limit 200pids_limitstop fork bombs
--ulimit nofile=65536:65536ulimits:file descriptors
--shm-size 1gshm_size/dev/shm (Chromium, Playwright, Postgres)
Restart policyRestarts when
no (default)never
on-failure[:5]non-zero exit, up to N times
alwaysany exit, and when the daemon starts, even after a manual stop
unless-stoppedlike always, except after docker stop

Healthchecks

docker run -d --name api \
  --health-cmd 'curl -fsS localhost:3000/health || exit 1' \
  --health-interval 30s --health-timeout 3s \
  --health-retries 3 --health-start-period 20s \
  --health-start-interval 2s \
  api
docker inspect -f '{{.State.Health.Status}}' api

Status goes starting → healthy / unhealthy. Plain Docker only reports it: an unhealthy container is not restarted. Compose uses it for depends_on: condition: service_healthy; Swarm and Kubernetes (with their own probes) act on it. The command runs inside the container, so the tool (curl, wget, pg_isready) must exist in the image.

Compose

compose project
my-app/compose.yaml            # base definitioncompose.override.yaml   # dev tweaks, auto-mergedcompose.prod.yaml       # applied via a second -f.env                    # interpolation vars, gitignored.env.examplesecrets/db_password.txt     # at /run/secrets/db_passwordapi/Dockerfile.dockerignoresrc/db/init/001-schema.sql  # run once on an empty data dir
Top-level keyHolds
nameproject name (default: directory name; -p / COMPOSE_PROJECT_NAME override)
servicescontainers to run
volumes / networksnamed volumes and networks (a default network is created for you)
secrets / configsfiles mounted into services
includepull in other compose files, each resolved relative to itself
modelsAI models served by Docker Model Runner
versionobsolete, ignored with a warning; delete it
Service keyPurpose
image / buildrun an image / build one (context, dockerfile, target, args, secrets, platforms)
command / entrypointoverride CMD / ENTRYPOINT
ports / exposepublish to the host / document for other services
volumesname:/path, ./host:/path:ro, or long form
environment / env_filecontainer env (environment wins)
depends_onstart order and readiness conditions
healthchecktest, interval, timeout, retries, start_period, start_interval
restartrestart policy
profilesonly start when that profile is active
develop.watchfile sync / rebuild rules for watch
deploy.resourcesCPU and memory limits and reservations
init, user, read_only, cap_drop, tmpfsruntime hardening
extra_hostse.g. host.docker.internal:host-gateway
post_start / pre_stoplifecycle hook commands in the container
pre_startinit containers that must exit 0 before the service starts (Compose 5.3+)
pull_policyalways, missing, never, build, daily, weekly

depends_on conditions

services:
  api:
    depends_on:
      db:
        condition: service_healthy   # waits for healthcheck
        restart: true   # restart api when db is recreated
      migrate:
        condition: service_completed_successfully
      cache:
        condition: service_started   # the short-form default
        required: false  # only warn if it isn't running

Profiles

services:
  api: { build: ./api }          # no profile: always starts
  adminer:
    image: adminer
    profiles: [debug]
docker compose --profile debug up -d
COMPOSE_PROFILES=debug,tools docker compose up -d
docker compose run --rm adminer   # run starts it anyway

Overrides and merging

MechanismBehavior
compose.override.yamlmerged over compose.yaml automatically
-f a.yaml -f b.yamlexplicit list, later files win; disables auto-override
COMPOSE_FILE=a.yaml:b.yamlsame, from the environment
Merge rulessingle values replace; ports, volumes, environment merge by key
!reset [] / !overrideYAML tags to clear or replace a value instead of merging
extendsinherit one service from another (same or another file)
docker compose configprint the fully merged, interpolated model

Watch (dev sync)

actionOn change
synccopy files into the running container (target)
rebuildrebuild the image and recreate the container
restartrestart the container
sync+restartcopy, then restart (config files)
sync+execcopy, then run exec.command inside

Rules take path, target, ignore and include patterns (.dockerignore syntax), plus initial_sync. Start it with docker compose up --watch, or docker compose watch to keep sync events out of the app logs.

Compose CLI

CommandUse
up -dcreate and start everything in the background
up -d --buildrebuild images first
up --waitblock until services are running/healthy (CI)
up --watchstart plus file watching
up -d --force-recreate --remove-orphansfresh containers, drop services no longer in the file
up -d --scale worker=3run N replicas (no fixed container_name or host port)
downstop and remove containers and networks
down -v --rmi localalso named volumes and locally built images
ps / ps -aproject containers
logs -f --tail 50 apifollow one service
exec api shshell into a running service (-T without a TTY in CI)
run --rm api bun run migrateone-off container; --no-deps, --service-ports
build --no-cache apirebuild one service
pull / pushfetch / publish service images
restart api / stop / startlifecycle without recreating
configvalidated, merged YAML; --services, --quiet
watchfile sync / rebuild only
cp api:/app/out.txt .copy files
top / stats / eventsprocesses / usage / event stream
lsall Compose projects on this daemon
-p name / -f file / --env-fileglobal: project name, file, env file

Multi-platform builds

ApproachNotes
QEMU emulationdefault on Docker Desktop; simple but slow for heavy compiles
Cross-compilationFROM --platform=$BUILDPLATFORM + TARGETOS/TARGETARCH args; native speed (Go, Rust, Bun --compile)
Native nodesdocker buildx create --append with an arm64 and an amd64 machine, or a cloud builder
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/acme/api:1.4.2 --push .

With the containerd image store (default in Engine 29), --load can keep a multi-platform image locally. On the classic store, create a docker-container builder and --push instead. Apple silicon builds linux/arm64 by default, which won't run on most x86 servers, so always set --platform for images you ship.

Security

PracticeHow
Non-root processUSER in the image, or -u 10001:10001 / Compose user:
Read-only root fs--read-only --tmpfs /tmp (Compose read_only: true, tmpfs:)
Drop capabilities--cap-drop ALL --cap-add NET_BIND_SERVICE
No privilege escalation--security-opt no-new-privileges
Avoid --privilegedit disables almost every isolation feature
Never mount /var/run/docker.socksocket access equals root on the host
Rootless daemondockerd-rootless-setuptool.sh install: the daemon itself runs unprivileged
User namespacesuserns-remap in daemon.json maps container root to a high host UID
Minimal base imagesslim, distroless or Docker Hardened Images (dhi.io)
Pin and scanpin digests, scan in CI, rebuild often for patched bases
Resource limitsmemory, CPU and PID limits stop one container starving the host
docker scout quickview api:1.4.2   # summary + base image
docker scout cves api:1.4.2 \
  --only-severity critical,high --exit-code
docker scout recommendations api:1.4.2  # better bases

Debugging

SymptomTry
Exits immediatelydocker logs api; docker inspect -f '{{.State.ExitCode}}' api
Need a shell in a bad imagedocker run --rm -it --entrypoint sh img
Image has no shell (distroless)docker debug api (Docker Desktop): brings its own toolbox, works on images and stopped containers
Network problemdocker run --rm -it --network container:api nicolaka/netshoot
Killed at randomdocker inspect -f '{{.State.OOMKilled}}' api, raise -m
Slow or stuck stopapp ignores SIGTERM as PID 1: add --init or a signal handler
"works in the build, not at runtime"docker history --no-trunc img, docker diff api
Build step failsdocker build --progress=plain --no-cache .
Compose service won't startdocker compose config, then docker compose logs api
Which daemon am I on?docker context ls, docker info
docker debug api          # shell with vim, curl, htop…
docker debug --command 'ls -la /app' api
docker exec -u root -it api sh   # when it has a shell
docker logs --since 5m -t api 2>&1 | grep -i error

Disk cleanup

CommandRemoves
docker system df -vnothing: shows usage by images, containers, volumes, cache
docker container prunestopped containers
docker image prunedangling (untagged) images
docker image prune -a --filter until=168hall unused images older than a week
docker volume pruneunused anonymous volumes; -a includes named ones
docker network pruneunused networks
docker builder prunebuild cache; --keep-storage 10GB
docker system prunestopped containers, unused networks, dangling images, build cache
docker system prune -a --volumeseverything unused, including volumes: the nuclear option

Alternatives

ToolPlatformNotes
Docker DesktopmacOS, Windows, LinuxGUI, Scout, Debug, Model Runner; paid license for larger companies
OrbStackmacOSfast, light drop-in Docker engine plus Linux VMs and local Kubernetes; paid for commercial use
ColimamacOS, Linuxfree, Lima VM running dockerd or containerd: colima start --cpu 4 --memory 8
PodmanLinux, macOS, Windowsdaemonless and rootless by default; alias docker=podman, podman compose, pods
Rancher DesktopmacOS, Windows, Linuxfree, Kubernetes-first, dockerd or containerd (nerdctl)

All of them speak the same image format and registries, so images and Dockerfiles carry over. Switch the CLI's target with docker context use colima (or orbstack).

Recipes

Postgres + app with a healthcheck

The API starts only after Postgres accepts connections, and restarts when the database is recreated.

compose.yaml
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    volumes: [pgdata:/var/lib/postgresql]
    ports: ["127.0.0.1:5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
  api:
    build: ./api
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
    ports: ["3000:3000"]
    depends_on:
      db: { condition: service_healthy, restart: true }
volumes:
  pgdata:

Postgres 18+ images keep data under /var/lib/postgresql/18/docker, so mount the volume at /var/lib/postgresql, not the old .../data. More in Postgres.

One-off container for a CLI

Run a tool without installing it, against the current directory, as your own user.

docker run --rm -it -v "$PWD":/work -w /work \
  -u "$(id -u):$(id -g)" oven/bun:1 bun --version
 
docker run --rm -i hadolint/hadolint < Dockerfile
 
# join a Compose project's network: <project>_default
docker run --rm -it --network my-app_default \
  postgres:18 psql postgres://app:app@db/app
 
alias jq='docker run --rm -i ghcr.io/jqlang/jq'

Cleanup

Reclaim disk from a laptop that has been building images for months.

docker system df
docker container prune -f
docker image prune -a -f --filter until=168h
docker builder prune -f --keep-storage 10GB
docker volume ls -f dangling=true   # review first!
docker volume prune -f
# one project, including its volumes
docker compose down -v --remove-orphans --rmi local

Copy files out of a container or image

Grab logs, build artifacts or a config file.

docker cp api:/app/logs/app.log ./app.log
docker cp api:/app/logs - | tar -t   # stream as tar
 
# from an image without running it
id=$(docker create ghcr.io/acme/api:1.4.2)
docker cp "$id":/app/dist ./dist
docker rm "$id"
 
# straight from a build stage, no image at all
docker build --target build \
  --output type=local,dest=./out .

Multi-arch build and push

Ship one tag that runs on x86 servers and ARM (Graviton, Apple silicon).

docker buildx create --name multi \
  --driver docker-container --use --bootstrap
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/acme/api:1.4.2 \
  -t ghcr.io/acme/api:1.4 \
  --cache-to type=registry,ref=ghcr.io/acme/api:cache \
  --cache-from type=registry,ref=ghcr.io/acme/api:cache \
  --push .
docker buildx imagetools inspect ghcr.io/acme/api:1.4.2

Compose watch for a Bun dev server

Edit on the host; files sync into the container and bun --watch reloads. Lockfile changes rebuild.

compose.yaml
services:
  api:
    build: { context: ., target: dev }
    command: bun --watch src/index.ts
    ports: ["3000:3000"]
    env_file: [.env]
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json
        - action: rebuild
          path: bun.lock
docker compose up --watch

The dev stage installs all dependencies; see the Bun Dockerfile recipe and Bun.

References