Containers cheat sheet

Docker cheat sheet

Every Docker command worth keeping close, grouped by what you are trying to do: run a container, build an image, mount a volume, and clear up the mess.

Last updated

Docker packages an application together with everything it needs to run, so the same image behaves the same way on your laptop, in CI and on a server. Nearly every command below is doing one of three things: building an image, running a container from an image, or clearing up what earlier containers left behind.

The reference is grouped by what you are trying to achieve rather than alphabetically, and the filter box searches all of it at once — type volume and every volume command on the page comes to you.

Anything in angle brackets is a placeholder you replace. Commands are given in their current form, so docker compose rather than docker-compose, and a few need a recent release — check with docker --version.

Image, container, volume

Most confusing Docker behaviour makes sense once these three are separate in your head. Almost every "where did my data go" question is the second row.

ThingWhat it isLives until
ImageA read-only stack of layers, plus the metadata saying what to runYou run docker rmi
ContainerOne instance of an image, with a writable layer of its ownYou run docker rm, and the writable layer goes with it
VolumeStorage that no container ownsYou run docker volume rm

A bind mount is the fourth thing worth naming: not a volume, just a folder on your machine grafted onto a path inside the container. Volumes are for data you want Docker to look after, bind mounts are for files you are editing.

The everyday loop

Nine commands out of ten, this is all you need.

TaskCommand
List running containersdocker ps
List every container, stopped includeddocker ps -a
Run a container in the backgrounddocker run -d <image>
Run one interactively and delete it afterdocker run --rm -it <image> sh
Follow a container's logsdocker logs -f <container>
Get a shell inside a running containerdocker exec -it <container> sh
Stop a containerdocker stop <container>
Start a stopped container againdocker start <container>
Delete a stopped containerdocker rm <container>
Stop and delete in one godocker rm -f <container>
List imagesdocker images
Download an imagedocker pull <image>:<tag>
Build an image from the Dockerfile heredocker build -t <name>:<tag> .

docker ps, docker images and docker rm are the short forms. Each one also has a longer management form: docker container ls, docker image ls, docker container rm. Neither is deprecated. The short forms are what people type, the long forms are what the help output and the documentation use, so both are worth recognising.

Running a container

docker run does two things at once: it creates a container from an image and starts it. Almost every option has to be set here, because most of them cannot be changed once the container exists.

TaskCommand
Run a command and leavedocker run --rm <image> <command>
Run interactively with a terminaldocker run --rm -it <image> sh
Run in the backgrounddocker run -d <image>
Give it a namedocker run -d --name <name> <image>
Publish a portdocker run -d -p 8080:80 <image>
Publish every exposed port to a random onedocker run -d -P <image>
Mount the folder you are indocker run -v "$(pwd)":/app <image>
Mount a named volumedocker run -v <volume>:/data <image>
Pass an environment variabledocker run -e KEY=value <image>
Load variables from a filedocker run --env-file .env <image>
Restart it automatically after a rebootdocker run -d --restart unless-stopped <image>
Override the image's default commanddocker run <image> <command>
Run as your own user, not rootdocker run -u "$(id -u):$(id -g)" <image>
Join a network so other containers can reach itdocker run -d --network <network> --name <name> <image>

Port mapping is host first, container second, so -p 8080:80 means localhost:8080 reaches port 80 inside. Getting it the wrong way round is the usual reason a container looks like it is running and nothing answers. And --rm deletes the container when it exits, which is what you want for anything one-off: without it every run leaves a stopped container behind for docker ps -a to find.

Flags for docker run

What you wantFlag
Delete the container when it exits--rm
Run in the background-d
Interactive terminal-it
Name the container--name <name>
Publish a port-p <host>:<container>
Bind mount a host folder-v <host-path>:<container-path>
Mount a named volume-v <volume>:<container-path>
Make a mount read-only-v <volume>:<container-path>:ro
Set an environment variable-e KEY=value
Read variables from a file--env-file <file>
Set the working directory-w <path>
Run as a specific user-u <uid>:<gid>
Cap the memory--memory 512m
Cap the CPU--cpus 1.5
Join a network--network <network>
Restart policy--restart unless-stopped
Reach a service on your own machine--add-host=host.docker.internal:host-gateway
Replace the entrypoint--entrypoint <command>
Ask for a specific architecture--platform linux/amd64

--memory and --cpus are worth knowing before you need them: a container with no limit can take every core and all the RAM on the machine, which is how one runaway build takes a laptop with it. host-gateway is the portable way to reach something running outside Docker. On Docker Desktop the name host.docker.internal already resolves; on Linux that flag is what creates it.

Looking inside a container

TaskCommand
Follow the logsdocker logs -f <container>
Last 100 lines onlydocker logs --tail 100 <container>
Logs from the last ten minutesdocker logs --since 10m <container>
Live CPU and memory, every containerdocker stats
Processes running insidedocker top <container>
Everything Docker knows about itdocker inspect <container>
One field, with a Go templatedocker inspect -f '{{.State.Status}}' <container>
Why it exiteddocker inspect -f '{{.State.ExitCode}}' <container>
Which ports are publisheddocker port <container>
Files changed since the imagedocker diff <container>
Run a one-off command insidedocker exec <container> <command>
Copy a file outdocker cp <container>:/path/file ./file
Copy a file indocker cp ./file <container>:/path/file
Attach to the main processdocker attach <container>

docker exec and docker attach are not the same thing, and mixing them up is unpleasant. exec starts a new process alongside the one already running; attach connects your terminal to PID 1 itself, so Ctrl-C stops the container. Use Ctrl-P then Ctrl-Q to detach without stopping it, or just use exec. An exit code of 137 means the process was killed rather than finishing, and the usual culprit is the memory limit.

Images

TaskCommand
List imagesdocker images
List dangling imagesdocker images --filter dangling=true
Download onedocker pull <image>:<tag>
Add a tagdocker tag <image> <registry>/<name>:<tag>
Log in to a registrydocker login <registry>
Push itdocker push <registry>/<name>:<tag>
Delete an imagedocker rmi <image>
See how each layer was builtdocker history <image>
Read an image's metadatadocker image inspect <image>
Save an image to a tar filedocker save -o <file>.tar <image>
Load one backdocker load -i <file>.tar
Check an image for known CVEsdocker scout cves <image>

The tag is part of an image's identity, not a version label you can move for free. Pulling an image with no tag means the latest tag, and latest is only whatever was pushed with that name last, so pin a real version in anything you deploy. Also note that docker scout replaced docker scan, which was Snyk-based and has been removed.

Building images

TaskCommand
Build from the Dockerfile in this folderdocker build -t <name>:<tag> .
Build from a Dockerfile somewhere elsedocker build -f <path>/Dockerfile -t <name> .
Build ignoring the cachedocker build --no-cache -t <name> .
Pass a build argumentdocker build --build-arg KEY=value -t <name> .
Stop at one stage of a multi-stage builddocker build --target <stage> -t <name> .
Build for another architecturedocker build --platform linux/amd64 -t <name> .
Build and push several architectures at oncedocker buildx build --platform linux/amd64,linux/arm64 -t <name> --push .
See the full, unfolded build logdocker build --progress=plain -t <name> .
List buildersdocker buildx ls
Clear the build cachedocker builder prune

The trailing dot is the build context: the folder Docker sends to the builder, and the root that every COPY path is resolved against. Put a .dockerignore next to the Dockerfile or that upload includes node_modules and .git. BuildKit has been the default builder since Docker Engine 23, which is why --progress=plain is worth remembering — the default output collapses each step, and plain is what you want when a build fails and you need to read the log.

Volumes and data

A container's own filesystem is thrown away with the container. Anything that has to survive lives in a volume or a bind mount.

TaskCommand
Create a named volumedocker volume create <name>
List volumesdocker volume ls
Where a volume lives on diskdocker volume inspect <name>
Delete a volumedocker volume rm <name>
Delete every unused volumedocker volume prune
Mount a named volumedocker run -v <name>:/data <image>
Mount a folder from your machinedocker run -v /host/path:/app <image>
Mount it read-onlydocker run -v /host/path:/app:ro <image>
The same thing, spelled outdocker run --mount type=bind,src=/host/path,dst=/app <image>
Delete a container and its anonymous volumesdocker rm -v <container>
Back a volume up to a tar filedocker run --rm -v <name>:/data -v "$(pwd)":/backup alpine tar czf /backup/volume.tar.gz -C /data .

A named volume is managed by Docker and is the right default for a database. A bind mount points at a real folder on your machine and is the right default for source code you are editing. The difference that catches people out: mounting over a folder the image already populated hides the image's contents rather than merging with them, which is why bind mounting a project over /app can make node_modules disappear.

Networks

TaskCommand
List networksdocker network ls
Create a networkdocker network create <name>
Run a container on itdocker run -d --network <name> <image>
Attach a container that is already runningdocker network connect <name> <container>
Detach onedocker network disconnect <name> <container>
See what is on a networkdocker network inspect <name>
Delete a networkdocker network rm <name>
Delete every unused networkdocker network prune

Containers on the same user-defined network find each other by container name, and that is the whole reason to create one: a web container can reach postgres://db:5432 with no IP addresses written down anywhere. The default bridge network does not do this. Compose creates a user-defined network for you, which is why service names simply work there.

Docker Compose

Compose describes a set of containers in a compose.yaml file so the whole stack starts with one command. It is docker compose, a subcommand of Docker, and not the older docker-compose binary.

TaskCommand
Start everything in the backgrounddocker compose up -d
Start, rebuilding changed imagesdocker compose up -d --build
Stop and remove the containersdocker compose down
Also remove the named volumesdocker compose down -v
What is runningdocker compose ps
Follow the logs of everythingdocker compose logs -f
Follow one servicedocker compose logs -f <service>
Shell into a servicedocker compose exec <service> sh
Run a one-off command in a new containerdocker compose run --rm <service> <command>
Restart one servicedocker compose restart <service>
Rebuild without the cachedocker compose build --no-cache
Pull newer imagesdocker compose pull
Print the merged, resolved configdocker compose config
Rebuild and sync as files changedocker compose watch

docker compose config is the debugging command nobody reaches for: it prints the file after variable substitution and after every override file has been merged, which is how you find out that the value in your .env is not the one being used. docker compose watch needs a develop.watch block in the file and Compose 2.22 or newer. The hyphenated docker-compose was Compose V1, which reached end of life in July 2023.

Cleaning up

Docker does not reclaim anything on its own. Images, stopped containers, volumes and build cache accumulate until you say otherwise.

TaskCommandAlso removes
See what is using the diskdocker system dfNothing. Start here
The same, itemiseddocker system df -vNothing
Delete stopped containersdocker container pruneTheir writable layers
Delete dangling imagesdocker image pruneOnly untagged layers
Delete every image no container usesdocker image prune -aImages you will have to pull again
Delete unused volumesdocker volume pruneThe data in them, permanently
Delete unused networksdocker network prune
Clear the build cachedocker builder pruneNothing, but the next build is slow
The usual sweepdocker system pruneStopped containers, unused networks, dangling images, build cache
The aggressive sweepdocker system prune -aAll of the above plus every unused image
Everything, volumes includeddocker system prune -a --volumesVolume data. Not recoverable

Run docker system df first, so you know what you are about to free. Unused means no container references the thing, and a stopped container counts as a reference, so clearing containers is what unlocks most of the image reclaim. The one to be careful with is prune -a --volumes: volume data is gone for good, and it is easy to forget which project owned which volume.

When it has gone properly wrong

SituationWay out
Port is already allocateddocker ps to find the container, or lsof -i :<port> for a process outside Docker, then stop it or publish a different host port
Container exits straight awaydocker logs <container> first, then docker run --rm -it --entrypoint sh <image> and run the command by hand
Container exits and logs nothingdocker inspect -f '{{.State.ExitCode}}' <container>. 137 means killed, usually the memory limit
The image has no shellscratch and distroless images have none. Rebuild from an -alpine or -slim tag to poke at it, or use docker debug if your subscription includes it
Build fails with no useful outputdocker build --progress=plain --no-cache
Build ignores the change you just madeCheck .dockerignore is not excluding the file, then rebuild with --no-cache
Cannot delete an image, still in usedocker ps -a to find the container holding it, docker rm that, then docker rmi again
Permission denied on a bind-mounted fileThe container user does not match yours. Run with -u "$(id -u):$(id -g)", or fix the ownership on the host
exec format errorWrong architecture. Add --platform linux/amd64, or rebuild with buildx for the machine you are on
Container cannot reach your machineUse host.docker.internal, adding --add-host=host.docker.internal:host-gateway on Linux
One container cannot reach anotherPut both on the same user-defined network and use the container name as the hostname
Disk is fulldocker system df, then prune the largest line

Two commands answer most of these: docker logs and docker inspect. Read the logs before changing anything, and check the state Docker recorded rather than the state you assume. The habit worth building is reproducing the failure interactively — override the entrypoint with a shell, get inside the image, and run the thing that broke by hand.

Dockerfile instructions

InstructionWhat it doesWatch out for
FROMThe image the build starts fromPin a tag. FROM node means node:latest
WORKDIRSets the working directory, creating it if neededUse this, not RUN cd, which does not persist
COPYCopies files in from the build contextPaths are relative to the context, not the Dockerfile
ADDCOPY, plus URL fetching and tar extractionPrefer COPY; the extra behaviour surprises people
RUNRuns a command at build timeEach one adds a layer. Chain with && where it matters
ENVA variable for the build and for the running containerNever a secret. It is baked into the image
ARGA variable for the build onlyAlso not a secret: docker history shows it
EXPOSEDocuments which port the app listens onPublishes nothing. -p does that
CMDDefault command or argumentsOnly the last one counts
ENTRYPOINTThe executable the container always runsCMD becomes its arguments
USERSwitches user for the rest of the build and at run timeAdd one. Root is the default
VOLUMEDeclares a path as a mount pointCreates an anonymous volume when nothing is mounted there
HEALTHCHECKCommand Docker runs to test the containerCompose and orchestrators act on it; docker run only reports it
LABELMetadata on the image
STOPSIGNALSignal sent by docker stopDefault is SIGTERM

Only RUN, COPY and ADD put content into the image. The rest set metadata, so moving them around costs nothing. Moving the three that do costs you the build cache from that line down, which is what the next section is about.

A Dockerfile worth copying

# syntax=docker/dockerfile:1

FROM node:24-alpine AS build
WORKDIR /app

# Manifests before source. This layer is only invalidated when a dependency
# changes, so editing a component does not reinstall node_modules.
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev

# Only the built output crosses over. The compiler, the source and the dev
# dependencies stay in the stage above and never ship.
COPY --from=build /app/dist ./dist

# The node images already provide an unprivileged `node` user.
USER node

EXPOSE 3000
CMD ["node", "dist/index.js"]

Two things are doing the work. Layer order: each instruction is cached against the state of its inputs, and a cache miss invalidates every layer after it, so the files that change least often are copied first. The second FROM: only the final stage becomes the image, so build tools, source files and dev dependencies are present while building and absent from what you deploy.

CMD or ENTRYPOINT

CMDENTRYPOINT
What it isDefault argumentsThe thing that runs
docker run image lsReplaces it entirelyAppends ls as an argument
How to overridedocker run <image> <command>docker run --entrypoint <command> <image>
Good alone forAn image that runs one fixed commandRarely useful alone
Used togetherBecomes the default argumentsBecomes the fixed command

Write both in exec form, as a JSON array: CMD ["node", "dist/index.js"]. The shell form, CMD node dist/index.js, wraps the command in /bin/sh -c, which makes the shell PID 1. Signals then arrive at the shell rather than your process, so docker stop waits out its ten-second grace period and kills the container instead of letting it shut down.

Making the image smaller

Size is worth caring about because it is pull time on every deploy and attack surface for the whole life of the image.

  1. Start smaller. -slim and -alpine tags are a fraction of the default. Alpine uses musl rather than glibc, which occasionally breaks native modules, so test rather than assume.
  2. Use a multi-stage build so compilers and source never reach the final stage.
  3. Write a .dockerignore. It keeps node_modules, .git and build output out of the context, which makes the upload smaller and stops COPY . . shipping them.
  4. Install production dependencies only in the runtime stage.
  5. Run docker history <image> and read it top down. The layer that surprises you is the one to fix.

A compose file worth copying

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5

volumes:
  db-data:

db in that connection string is the service name, which resolves because Compose puts both containers on a network it creates. The depends_on condition is the part people leave out: plain depends_on waits for the container to start, which is not the same as Postgres being ready to accept a connection, and service_healthy waits for the health check instead.

There is no version key. It was required by Compose V1, is ignored by V2, and Compose warns about it. The default filename is now compose.yaml, though docker-compose.yml is still read.

Common questions

What is the difference between a Docker image and a container?

An image is a read-only template: a stack of filesystem layers plus the metadata that says what to run. A container is one running instance of an image, with a thin writable layer of its own on top. One image can back any number of containers, and the writable layer is discarded when the container is deleted, which is why anything that has to survive belongs in a volume.

How do I get a shell inside a running Docker container?

docker exec -it followed by the container name and sh. Use bash instead of sh if the image has it; Alpine-based images usually do not. If the container has already exited you cannot exec into it, so start a new one from the same image with docker run --rm -it --entrypoint sh instead.

What is the difference between CMD and ENTRYPOINT?

ENTRYPOINT is the executable the container always runs. CMD is the default arguments, and anything you type after the image name on docker run replaces them. Used together, ENTRYPOINT fixes the command and CMD supplies defaults you can override. Used alone, CMD is the whole command. Write both as a JSON array so your process runs as PID 1 and receives signals directly.

Should I use COPY or ADD in a Dockerfile?

COPY, almost always. It copies files and folders from the build context and does nothing else. ADD also fetches URLs and silently extracts local tar archives, which means a file named something.tar.gz ends up unpacked rather than copied. Use ADD only when you actually want that extraction.

How do I free up disk space used by Docker?

Run docker system df to see where it went, then docker system prune to remove stopped containers, unused networks, dangling images and build cache. Add -a to also remove every image no container references. Add --volumes only when you are certain, because volume data is not recoverable and is usually the part that mattered.

Why does my container lose its data when it restarts?

Because a container's writable layer belongs to that container. docker restart keeps it, but docker rm followed by docker run creates a new container with a fresh layer, and Compose recreates containers whenever the service definition changes. Mount a named volume at the path the data lives in and it will outlive any container that uses it.

Is it docker compose or docker-compose?

docker compose, with a space. That is Compose V2, written in Go and shipped as a plugin to the Docker CLI. The hyphenated docker-compose was V1, a separate Python tool, and it reached end of life in July 2023. The file format is the same, so an existing docker-compose.yml still works, though the default filename is now compose.yaml and the version key at the top is obsolete.

How do I connect from a container to a service running on my host machine?

Use the hostname host.docker.internal rather than localhost, because localhost inside a container is the container. On Docker Desktop that name already resolves. On Linux, add --add-host=host.docker.internal:host-gateway to docker run, or the equivalent extra_hosts entry in Compose.

More cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.