Skip to content

Essential Docker Commands: The Complete Cheat Sheet

Essential Docker Commands: The Complete Cheat Sheet

Picture 2 AM on a Saturday. The checkout service returns 500s and kubectl logs shows no space left on device: months of CI/CD builds have filled overlay2 with dangling layers, and nobody runs docker system prune. The fix is one command. Finding it under pressure is the slow part.

TL;DR

Master container operations, image management, volumes, networking, and the disk cleanup command that prevents 3 AM outages. All 50 essential Docker commands, organized by task.

  • Use tables and quick command blocks for instant lookup and copy-paste
  • Always tag images with versions — never :latest in production
  • docker system prune -a --volumes prevents disk exhaustion before it kills production

Pick the Right Command for the Symptom

Most "I forgot the docker command for X" lookups boil down to a small decision tree. Route by symptom, not by concept:

graph LR
    Start[What do I need?] --> Type{Container,<br/>image, volume,<br/>or network?}
    Type -->|Container| Cstate{Running or<br/>stopped?}
    Cstate -->|Running| Crun[ps · exec · logs · stats]
    Cstate -->|Stopped| Cstop[ps -a · start · rm]
    Type -->|Image| Iaction{Build, list,<br/>or clean?}
    Iaction -->|Build| Ibuild[build · tag · push]
    Iaction -->|List| Ilist[images · history · inspect]
    Iaction -->|Clean| Iclean[image prune · system prune]
    Type -->|Volume| Vol[volume ls · volume inspect · volume rm]
    Type -->|Network| Net[network ls · network inspect · network connect]
    style Crun fill:#dfd
    style Ibuild fill:#dfd
    style Vol fill:#dfd
    style Net fill:#dfd

The diagram is the cheat-sheet within the cheat-sheet — every command in the rest of this file lives at a leaf of that tree.

The Quick Start

Source⁠[1]
TaskCommand
Rundocker run -d --name myapp -p 8080:8080 myapp:v1.0
List runningdocker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
List alldocker ps -a
Stopdocker stop myapp
Startdocker start myapp
Restartdocker restart myapp
Shell intodocker exec -it myapp /bin/bash
Run one commanddocker exec myapp curl http://localhost:8080/health
Copy from containerdocker cp myapp:/var/log/app.log ./debug/
Removedocker rm myapp

Images and Builds

Source⁠[1]
## Build from Dockerfile
docker build -t myapp:v1.0 .
 
## Build with build args
docker build --build-arg NODE_ENV=production -t myapp:prod .
 
## Pull specific version (always use tags, never :latest)
docker pull postgres:16-alpine
 
## Tag and push to registry
docker tag myapp:v1.0 registry.company.com/myapp:v1.0
docker push registry.company.com/myapp:v1.0
 
## Check image disk usage
docker system df
 
## Remove dangling images
docker image prune -f
 
## Remove all unused images
docker image prune -a -f

Registry login: docker login (Docker Hub), aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com (ECR), or gcloud auth configure-docker (GCP).

Dockerfile Basics

Source⁠[1]
## Multi-stage build: fast rebuilds, tiny final image
FROM golang:1.27-bookworm AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server
 
FROM scratch
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Layer caching rule: Put rarely-changing instructions (RUN apt-get install) first, frequently-changing ones (COPY . .) last. Docker reuses cached layers if nothing changed in that layer or before it⁠[2].

The image lifecycle in one picture — build → push → pull → run → exit:

graph TD
    DF[Dockerfile + source] -->|docker build| Img[Image<br/>layered SHA256<br/>local cache]
    Img -->|docker push| Reg[(Registry<br/>ECR / GCR / Hub)]
    Reg -->|docker pull| Node[Node-local cache<br/>per-host image store]
    Node -->|docker run<br/>or kubelet| Cont[Container<br/>writable top layer<br/>+ ephemeral fs]
    Cont -->|docker exec| Shell[Interactive shell<br/>or one-shot command]
    Cont -->|exit / SIGKILL| Stopped[Stopped container<br/>filesystem preserved]
    Stopped -->|docker rm| Gone[Gone — top layer deleted]
    Stopped -->|docker start| Cont
    Cont -.->|docker commit<br/>rare, manual| Img
    Cont -.->|volume mount| Vol[(Named volume<br/>survives container)]
    style Cont fill:#dfd
    style Reg fill:#ffd
    style Vol fill:#ffd

The diagram captures every command's place in the lifecycle: build produces an Image, run instantiates a Container, exec enters a running container, rm deletes the writable layer (the volume survives).

## Inspect layer history
docker image history myapp:v1.0
 
## Get full metadata
docker image inspect myapp:v1.0

Volumes and Storage

Source⁠[1]
TypeSyntaxUse Case
Nameddocker run -v mydata:/data myappDatabase storage, persistent app data
Bind mountdocker run -v /host/path:/container/path myappSource code (dev), config files
Read-onlydocker run -v myapp-config:/etc/myapp:ro myappImmutable config, certificates
tmpfsdocker run --tmpfs /tmp myappSecrets in memory, scratch space
## Create named volume
docker volume create myapp-data
 
## List and inspect
docker volume ls
docker volume inspect myapp-data
 
## Clean up unused volumes
docker volume prune -f
 
## Backup a volume
docker run --rm -v myapp-data:/source:ro -v $(pwd):/backup alpine \
  tar czf /backup/backup.tar.gz -C /source .

Networking

Source⁠[1]
DriverUse CaseDNS ResolutionMulti-Host
bridge (default)Single-host container-to-containerYes (user-defined networks)No
hostMaximum network performanceN/AN/A
overlaySwarm / multi-host networkingYesYes
noneComplete network isolationNoNo
## Create custom bridge network
docker network create myapp-network
 
## Containers on same network resolve by name
docker run -d --name api --network myapp-network myapp:v1.0
docker run -d --name db --network myapp-network postgres:16
## Inside api: ping db → resolves to postgres IP
 
## Port mapping variants
docker run -p 8080:80 nginx                   # host:container
docker run -p 127.0.0.1:8080:80 nginx         # localhost only
docker run -p 8080:80/udp nginx               # UDP port

Debugging and Logs

Source⁠[1]
## View logs with timestamps
docker logs --details --timestamps myapp
 
## Follow logs (like tail -f)
docker logs -f --tail=100 myapp
 
## Inspect container state and config
docker inspect myapp | jq '.[0] | {State, Env: .Config.Env, Memory: .HostConfig.Memory}'
 
## Shell into container
docker exec -it myapp /bin/bash
 
## Run command without interactive shell
docker exec myapp curl http://localhost:8080/health
 
## Monitor CPU/memory usage
docker stats --no-stream
 
## Watch events in real time
docker events --filter type=container

Production Cleanup

## Check disk usage (images, containers, volumes, build cache)
docker system df -v
 
## The lifesaver: stopped containers, unused images/networks, build cache older than 24h
docker system prune -a -f --filter "until=24h"
## Docker rejects "until" combined with --volumes, so prune anonymous volumes separately
docker volume prune -f
## Run both as a daily cron job on build servers (-f: cron has no TTY to confirm)

When to use what

Use CaseRecommendedWhyAvoid When
Multi-stage buildFROM golang AS builder ... FROM scratchFast rebuilds, tiny final images (distroless), no runtime bloatSingle-stage Dockerfiles with all deps in final image
Base image for Go/Rustdistroless or alpine:3.24Minimal CVE surface; Go/Rust are compiled so no runtime neededNode.js/Python — need runtime installed
Base image for Node/Pythonnode:22-alpine or python:3.12-slimSmall, has runtime, fewer CVEs than ubuntuUsing latest or bookworm when alpine works
Copy strategyCOPY for files, ADD only for .tar.gzCOPY is explicit; ADD auto-extracts but hides side effectsMixing COPY and ADD in same layer for clarity
Container startupENTRYPOINT ["app"] + CMD ["arg1"]ENTRYPOINT defines the executable; CMD overrides args onlyUsing CMD for the binary and args together
Port mapping rangedocker run -p 8080:80Accessible externally (0.0.0.0); standard in productiondocker run -p 127.0.0.1:8080:80 unless localhost-only intended
Persistent dataNamed volumes docker run -v mydata:/dataSurvives container deletion; easy backup/restoreBind mounts for app data (lose data on host deletion)
Config/secretsConfigMaps/Secrets in Kubernetes; env vars for DockerEnvironment decouples config from imageHardcoding in Dockerfile or .env files
Layer cachingPut stable deps first, code lastReuses layers; fast iterative rebuildsPutting COPY . . before RUN apt-get

Gotchas that bite in production

  1. docker system prune -a removes base layers you'll immediately rebuild

    • Friday-evening prune, Monday-morning builds 5 minutes slower.
    • Fix: docker image prune (no -a) for dangling only; on build servers docker system prune -a --filter "until=72h" off-peak.
  2. No healthcheck → a crashed -d container fails silently

    • Immediate crash (OOM, missing binary) just marks the container exited; 503s for hours.
    • Fix: HEALTHCHECK in the Dockerfile (or --health-cmd) + --restart=on-failure:3.
  3. COPY . . before RUN npm install busts the layer cache on every code change

    • One edited line rebuilds 500MB of node_modules — 3 minutes instead of 5 seconds.
    • Fix: dependency layer before code layer:
      COPY package.json package-lock.json ./
      RUN npm ci
      COPY . .
      RUN npm run build
  4. docker exec on a crashed container errors — reach for docker logs

    • You can't exec into something that isn't running; the crash evidence is in captured stdout/stderr.
    • Fix: docker logs myapp (works on stopped containers) + docker inspect myapp --format '{{.State.ExitCode}} {{.State.Error}}'; to browse the dead filesystem: docker commit myapp debug-myapp && docker run -it --entrypoint sh debug-myapp.
  5. Image pulls time out at peak → pending replicas → cascading OOM

    • Fix: pre-pull to all nodes before deploy (warm cache); multi-registry fallback; pull_policy: always for freshness.

Common Gotchas

  1. Always specify image versions — never :latest in production. docker pull postgres:latest gets a different image next month.

  2. Build args are not secrets — ARG GITHUB_TOKEN=... is visible in docker image history. Use BuildKit secret mounts instead: RUN --mount=type=secret,id=token npm ci.

  3. --privileged is root access — never use in production. It gives the container full access to the host's devices. Debug locally only.

  4. docker rm doesn't remove images — use docker rmi myapp:v1.0. Removing images doesn't clean the build cache.

  5. Dangling layers kill disk space — docker system prune -a --volumes removes untagged images and build cache. Set up as daily cron job on build servers.

  6. Port mapping to localhost doesn't broadcast — docker run -p 127.0.0.1:8080:80 nginx listens on localhost only. docker run -p 8080:80 nginx listens on 0.0.0.0 (externally accessible).

Docker image security: the SBOM workflow

The provenance pipeline is build → SBOM → scan → sign → verify, one-line tool per step: an SBOM inventories every package in the image, the scanner cross-references CVEs, the signature proves the image came from your pipeline. Syft → Grype → cosign, same workflow in CI (block the push on critical CVEs) and at admission time in Kubernetes (reject unsigned images via Kyverno or Connaisseur).

## 1. Generate SBOM in CycloneDX format (also supports SPDX, syft-json)
syft myapp:v1.0 -o cyclonedx-json=sbom.json
 
## 2. Scan the SBOM, fail the build on Critical / High CVEs
grype sbom:sbom.json --fail-on high --only-fixed
 
## 3. Sign the image keyless via Sigstore (uses GitHub OIDC token in CI)
cosign sign --yes registry.company.com/myapp:v1.0
 
## 4. Attach the SBOM as an attestation to the image
cosign attest --yes --predicate sbom.json \
  --type cyclonedx registry.company.com/myapp:v1.0
 
## 5. At deploy time, verify signature + SBOM attestation
cosign verify \
  --certificate-identity-regexp "https://github.com/company/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  registry.company.com/myapp:v1.0

Keyless signing kills the vaulted long-lived key: the OIDC token from CI is exchanged for a short-lived certificate at Sigstore's Fulcio, the signature is recorded in Rekor's transparency log, and verifiers check both. If an attacker pushes a malicious image without the matching OIDC identity, cosign verify fails and the deployment is rejected.

Pin the base image by digest, not tag: FROM gcr.io/distroless/base@sha256:abcd... instead of FROM gcr.io/distroless/base:latest. Tags are mutable; digests are not. Combined with cosign verification, this closes the registry-poisoning attack vector entirely.

Production Dockerfile patterns

Three patterns separate hobbyist Dockerfiles from production ones: distroless final stages, BuildKit cache mounts, and --mount=type=secret for build-time credentials. Each cuts image size, build time, or attack surface — and they compose.

# syntax=docker/dockerfile:1.7
## Rust example: cache mount keeps cargo registry warm across builds
FROM rust:1.98-bookworm AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release --locked && \
    cp target/release/server /server
 
## Distroless final stage — no shell, no package manager, minimal CVE surface
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

The cache mount survives across builds without bloating the image (it lives in BuildKit's cache, not in any layer). For a Rust monorepo, compiled dependencies persist between builds, so only changed crates recompile.

For Java services, the analogous pattern uses Maven's repository cache plus a JLink-trimmed JRE in the runtime stage:

# syntax=docker/dockerfile:1.7
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B -DskipTests package && \
    jlink --add-modules java.base,java.logging,java.sql,java.naming \
          --strip-debug --no-header-files --no-man-pages \
          --output /jre
 
FROM gcr.io/distroless/java-base-debian12:nonroot
COPY --from=builder /jre /jre
COPY --from=builder /app/target/app.jar /app.jar
USER nonroot:nonroot
ENTRYPOINT ["/jre/bin/java", "-jar", "/app.jar"]

Build-time secrets: never ARG GITHUB_TOKEN — it lands in image history. Use --mount=type=secret:

DOCKER_BUILDKIT=1 docker build \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t myapp:v1.0 .

Inside the Dockerfile: RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci. The secret is mounted only for that RUN step and never written to a layer.

Container debugging in production

When a container misbehaves in production, the diagnostic flow is roughly: check liveness, then resource pressure, then in-process state, then kernel-level signals. Each step has one or two commands and rules out a class of failure before you escalate.

## 1. Is the container actually alive? Did it OOM-kill recently?
docker inspect --format \
  '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} pid={{.State.Pid}}' \
  myapp
 
## 2. Resource pressure — CPU throttling and memory headroom
docker stats --no-stream --format \
  "table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
 
## 3. What is the container actually doing? PID + threads inside namespace
docker top myapp -o pid,ppid,pcpu,pmem,etime,stat,wchan,cmd
 
## 4. Real-time event stream — restarts, OOMs, health-check transitions
docker events --filter container=myapp \
  --filter event=die --filter event=oom --filter event=health_status
 
## 5. When exec fails because the container has no shell (distroless)
docker run -it --rm --pid=container:myapp --net=container:myapp \
  --cap-add=SYS_PTRACE nicolaka/netshoot

The last command is the distroless workhorse: it joins the target's PID and network namespaces via a debug image that has tcpdump, strace, dig, curl, nc, and lsof. From inside it, strace -p 1 attaches to the application's PID 1 to see what syscalls it is blocked on, and ss -tnlp lists every listening socket without modifying the production image.

"Intermittent 503s" flow: docker stats rules out CPU throttling, docker top confirms the worker count matches expectations, run docker exec myapp curl -s localhost:8080/metrics | grep http_requests to read in-process counters, and finally docker events --since 10m to spot whether the orchestrator killed and restarted a replica during the window. Most production debugging never needs to leave these four commands — the discipline is running them in order rather than guessing.

Frequently Asked Questions

How do I run a container with environment variables?

Use -e KEY=value (or -e KEY to read from the host environment). Example: docker run -e DATABASE_URL=postgres://db:5432/myapp -e DEBUG=true myapp:v1.0.

How do I mount a volume at a specific path?

docker run -v /host/path:/container/path myapp for bind mounts. Use -v mydata:/data for named volumes. Append :ro for read-only: docker run -v config:/etc/app:ro myapp.

How do I see if a container is using too much memory?

Run docker stats --no-stream for live CPU/memory across all containers. To inspect a single container's limit: docker inspect --format '{{.HostConfig.Memory}}' myapp (returns bytes; 0 means unlimited).

What's the difference between docker stop and docker kill?

docker stop sends SIGTERM, waits 10 seconds (Linux default), then escalates to SIGKILL⁠[3]. docker kill sends SIGKILL immediately. Always use stop in production so in-flight requests can drain.

How do I copy a file out of a crashed container?

docker cp myapp:/var/log/app.log ./debug/ works on stopped containers. If you need to explore the filesystem, create a debug image: docker commit myapp debug-myapp && docker run -it --entrypoint sh debug-myapp.

Can I run commands inside a container without a shell?

Yes. docker exec myapp curl http://localhost:8080/health runs curl directly without spawning a shell. Reserve docker exec -it for cases where you actually need interactive input.

Production Checklist

  • Images tagged with specific versions, never :latest
  • docker system prune -a -f --filter "until=24h" plus docker volume prune -f run daily on build servers
  • Containers run as non-root USER in Dockerfile
  • Health checks configured in compose files or run commands
  • Memory limits set: docker run --memory="512m" myapp
  • Volumes for persistent data (database, logs, caches)
  • Private registry configured for sensitive images
  • .dockerignore excludes .git, node_modules, *.md, .env files
  • Multi-stage Dockerfile with build stage and minimal final stage
  • Build process scanned for CVEs before pushing: docker scout cves myapp:v1.0 --exit-code

Keep Reading

Sources

  1. 1.Docker Documentation — docs.docker.com, 2026
  2. 2.Build cache invalidation — docs.docker.com, 2026
  3. 3.docker container stop — docs.docker.com, 2026
BackendBytes Engineering Team
BackendBytes

Engineering Team

An independent engineering publication covering distributed systems, databases, and production infrastructure. Every factual claim is cited to a primary source or removed.

Read Next