# Docker CLI Cheat Sheet — Images & Containers

> **Tool:** Docker CLI
> **Category:** Containers & Orchestration
> **Verified against:** Docker 29.1.5, flags verified via `docker <cmd> --help` run locally, 2026-08-29
> **Official docs:** https://docs.docker.com/reference/cli/docker/

## What it is and where it fits 🎯

The Docker CLI talks to the Docker daemon (`dockerd`) over a local socket (or a remote one, via `docker
context`) to build images, run containers, and manage the resources around them — networks, volumes, images.
Everything the CLI does is really an API call to that daemon; `docker run` isn't magic, it's a client for a
REST API the same way `kubectl` is a client for the Kubernetes API server. This page covers building/tagging
images and running/inspecting/cleaning up containers, the day-to-day loop of local container development; the
companion page covers Compose, networking, and volumes.

## What actually happens on `docker run`

```mermaid
sequenceDiagram
    participant CLI as docker CLI
    participant Daemon as dockerd
    participant Reg as Registry

    CLI->>Daemon: docker run myapp:latest
    Daemon->>Daemon: Image present locally?
    alt not present
        Daemon->>Reg: Pull layers
        Reg-->>Daemon: Image layers
    end
    Daemon->>Daemon: Create container (writable layer + config)
    Daemon->>Daemon: Set up network namespace, mounts, cgroups
    Daemon->>Daemon: Start container process (PID 1 inside the namespace)
    Daemon-->>CLI: Container ID
```

The container's writable layer is thin — everything below it is the shared, read-only image layers, which is
why ten containers from the same image share disk space for everything except what each one actually writes.

## Building images

```bash
docker build -t myapp:latest .
docker build -t myapp:v1.2.0 -f Dockerfile.prod .
docker build --no-cache -t myapp:latest .           # ignore layer cache, force a full rebuild
docker build --build-arg NODE_ENV=production -t myapp:latest .
```

`docker build` is now backed by BuildKit (`docker buildx build` under the hood) — `--no-cache` invalidates every layer, while a targeted fix is usually cheaper: touching the file that changed and letting the layer cache do its job from that point forward.

## Tagging and pushing images

```bash
docker tag myapp:latest myregistry.io/myteam/myapp:v1.2.0
docker push myregistry.io/myteam/myapp:v1.2.0
docker pull myregistry.io/myteam/myapp:v1.2.0
docker images                                        # list local images
docker rmi myapp:latest                              # remove a local image
```

## Running containers

```bash
docker run -d --name my-app -p 8080:80 myapp:latest
docker run -d --name my-app -e LOG_LEVEL=info -v ./data:/app/data myapp:latest
docker run --rm -it myapp:latest /bin/bash            # interactive, auto-removed on exit
docker run -d --restart unless-stopped myapp:latest
```

`-p 8080:80` maps **host:container** — the host port comes first. Getting this backwards is a common cause of "it works when I exec in but not from the browser."

## Listing and inspecting containers

```bash
docker ps                                            # running containers only
docker ps -a                                         # include stopped containers
docker ps --filter status=exited
docker inspect my-app                                # full container config as JSON
docker stats                                          # live CPU/memory/network usage
```

## Logs and exec

```bash
docker logs my-app
docker logs -f my-app                                # follow/stream
docker logs --tail 100 --since 1h my-app
docker exec -it my-app /bin/bash                      # interactive shell in a running container
docker exec my-app env                                 # one-off command, no shell
```

## Stopping and removing containers

```bash
docker stop my-app
docker rm my-app
docker rm -f my-app                                   # stop and remove in one step
docker container prune                                 # remove all stopped containers
```

## Multi-stage builds

```dockerfile
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
```

```bash
docker build -t myapp:latest .                        # builds every stage, keeps only the final one
docker build --target build -t myapp:build-debug .    # stop at an intermediate stage, e.g. to debug it
```

Each `FROM` starts a new stage; `COPY --from=<stage>` pulls specific artifacts out of an earlier stage into the current one. Only the final stage ends up in the built image, so build tooling (compilers, `npm`/`node_modules`, test dependencies) never ships to production — this is the standard fix for "why is my image 1.2GB for a 20MB binary."

## Building for multiple platforms with buildx

```bash
docker buildx ls                                       # list builder instances
docker buildx create --name multiarch --use --bootstrap  # create + switch to a new builder, boot it
docker buildx build --platform linux/amd64,linux/arm64 -t myregistry.io/myapp:v1 --push .
docker buildx use default                               # switch back to the default builder
```

`docker build` on a single-arch host only ever produces an image for that host's architecture. `buildx` uses BuildKit (via QEMU emulation or a remote builder) to produce a multi-platform image in one invocation — `--push` is required for a multi-platform build's output, since a multi-arch manifest can't be loaded into the local `docker images` store the way `--load` loads a single-platform one.

## Health checks

```bash
docker run -d --name my-app \
  --health-cmd="curl -f http://localhost/health || exit 1" \
  --health-interval=30s \
  --health-timeout=5s \
  --health-retries=3 \
  --health-start-period=10s \
  myapp:latest
docker inspect --format='{{.State.Health.Status}}' my-app   # healthy / unhealthy / starting
docker run -d --no-healthcheck myapp:latest              # ignore a HEALTHCHECK baked into the image
```

A container image can bake in its own `HEALTHCHECK` instruction in the Dockerfile — the `--health-*` flags on `docker run` override it per-container without rebuilding. `--health-start-period` matters for slow-starting apps: failed checks during that window don't count toward `--health-retries`, so a container isn't marked unhealthy while it's still booting.

## Setting resource limits

```bash
docker run -d --memory=512m --memory-swap=512m myapp:latest   # hard cap RAM, disable swap (swap = memory limit)
docker run -d --memory=512m --memory-reservation=256m myapp:latest  # soft limit, enforced under host pressure
docker run -d --cpus=1.5 myapp:latest                    # cap at 1.5 CPU cores
docker run -d --cpuset-cpus="0,1" myapp:latest            # pin to specific CPU cores
docker stats my-app                                        # confirm actual usage against the limits
```

Setting `--memory-swap` equal to `--memory` disables swap for the container (the flag is swap *on top of* the memory limit, not swap in isolation) — the common pattern for stopping a leaking container from silently degrading into swap thrash instead of getting OOM-killed where you'd notice it.

## Working with docker context (managing multiple daemons)

```bash
docker context ls                                        # list contexts (local + remote daemons)
docker context create staging --docker "host=ssh://user@staging-host"
docker context use staging                                # switch the CLI's target daemon
docker context show                                        # print the currently active context
docker --context staging ps                                 # one-off command against a specific context without switching
docker context rm staging
```

A context bundles a daemon endpoint (local socket, SSH, or TCP+TLS) under a name — switching context is how you point the same `docker` CLI at a different host (dev laptop vs. a remote build box) without juggling `DOCKER_HOST` env vars by hand.

## Inspecting image layers and history

```bash
docker history myapp:latest                                # each layer, its size, and the command that created it
docker history --no-trunc myapp:latest                     # full (untruncated) command per layer
docker inspect myapp:latest                                 # full image metadata as JSON (env, entrypoint, layers, config)
docker inspect --format='{{.Config.Env}}' myapp:latest      # pull one field out with a Go template
```

`docker history` is usually the fastest way to find which instruction in a Dockerfile bloated an image — layers are listed newest-first with their individual size, so a surprisingly large layer points straight at the offending `RUN`/`COPY` line.

## Cleaning up unused resources

```bash
docker system prune                                          # remove stopped containers, dangling images, unused networks, build cache
docker system prune -a                                        # also remove ALL unused images, not just dangling ones
docker system prune --volumes                                  # also remove anonymous (unnamed) volumes
docker image prune -a --filter "until=24h"                    # only images untouched in the last 24h
docker buildx prune                                             # clear the BuildKit build cache specifically
```

`system prune` never touches named volumes or anything attached to a running container by default — `-a` is still safe for that reason, but always confirms what it's about to remove interactively unless you pass `-f`. `buildx prune` is separate because BuildKit's cache lives outside the regular image/container/volume/network bookkeeping `system prune` covers.

## Real-world scenario: shrinking a bloated production image

A team notices their production image is 1.4GB for what should be a small compiled binary — `docker history`
is the diagnostic starting point, multi-stage builds are almost always the fix:

```bash
docker history --no-trunc myapp:latest | head -20    # find which layer is huge, and the exact command that created it
```

> [!TIP]
> **The most common culprit is a single-stage Dockerfile that `RUN`s a full build toolchain (compilers,
> `npm`/`node_modules`, test dependencies) and never removes it.** The multi-stage pattern shown earlier on
> this page (`FROM ... AS build` then a fresh minimal `FROM` that only `COPY --from=build`s the compiled
> output) is the standard fix — build tooling never reaches the final image at all, rather than being
> installed and then `rm -rf`'d in a later `RUN` layer (which doesn't actually shrink the image, since
> earlier layers are immutable and still counted toward the total).

## Common pitfalls

- **Reversing `-p host:container`** — see the port-mapping note above; the most common "works locally, not
  from outside" report.
- **`--memory-swap` set independently of `--memory`** and expecting it to mean "this much swap" — it's the
  *combined* memory+swap ceiling, not swap alone; set them equal to disable swap for the container entirely.
- **Assuming a container image's own `HEALTHCHECK` disappears** when you don't pass `--health-*` flags — it
  doesn't; the Dockerfile-baked check still runs unless `--no-healthcheck` is passed explicitly.
- **Forgetting `--push` is mandatory for a multi-platform `buildx build`** — a multi-arch manifest list can't
  be loaded into the local single-arch `docker images` store the way `--load` loads a single-platform result.

## When to reach for something else

For anything beyond a single host — real multi-node orchestration, rolling updates, self-healing — reach for
Kubernetes (see the kubectl/helm/kubeadm cheat sheets in this same category) rather than Docker Swarm, which
has fallen out of mainstream use. Docker itself remains the standard for local development and image building
even in a Kubernetes-first shop; most teams build with `docker build`/`buildx` and deploy the resulting image
to a cluster that runs containerd directly, without Docker itself in the runtime path at all.
