Verified15 commandsAI-assisted

Images & Containers

.md

Verified against Docker 29.1.5, flags verified via `docker <cmd> --help` run locally, 2026-08-29 · official docs

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#

Diagram

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#

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#

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#

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#

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#

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#

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#

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

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#

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#

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

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#

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#

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:

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 RUNs 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=builds 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.