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.
| Thing | What it is | Lives until |
|---|---|---|
| Image | A read-only stack of layers, plus the metadata saying what to run | You run docker rmi |
| Container | One instance of an image, with a writable layer of its own | You run docker rm, and the writable layer goes with it |
| Volume | Storage that no container owns | You 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.
Searches the task, the command and the third column. Press / from anywhere on the page.
138 commands
The everyday loop
Nine commands out of ten, this is all you need.
| Task | Command |
|---|---|
| List running containers | docker ps |
| List every container, stopped included | docker ps -a |
| Run a container in the background | docker run -d <image> |
| Run one interactively and delete it after | docker run --rm -it <image> sh |
| Follow a container's logs | docker logs -f <container> |
| Get a shell inside a running container | docker exec -it <container> sh |
| Stop a container | docker stop <container> |
| Start a stopped container again | docker start <container> |
| Delete a stopped container | docker rm <container> |
| Stop and delete in one go | docker rm -f <container> |
| List images | docker images |
| Download an image | docker pull <image>:<tag> |
| Build an image from the Dockerfile here | docker 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.
| Task | Command |
|---|---|
| Run a command and leave | docker run --rm <image> <command> |
| Run interactively with a terminal | docker run --rm -it <image> sh |
| Run in the background | docker run -d <image> |
| Give it a name | docker run -d --name <name> <image> |
| Publish a port | docker run -d -p 8080:80 <image> |
| Publish every exposed port to a random one | docker run -d -P <image> |
| Mount the folder you are in | docker run -v "$(pwd)":/app <image> |
| Mount a named volume | docker run -v <volume>:/data <image> |
| Pass an environment variable | docker run -e KEY=value <image> |
| Load variables from a file | docker run --env-file .env <image> |
| Restart it automatically after a reboot | docker run -d --restart unless-stopped <image> |
| Override the image's default command | docker run <image> <command> |
| Run as your own user, not root | docker run -u "$(id -u):$(id -g)" <image> |
| Join a network so other containers can reach it | docker 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 want | Flag |
|---|---|
| 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
| Task | Command |
|---|---|
| Follow the logs | docker logs -f <container> |
| Last 100 lines only | docker logs --tail 100 <container> |
| Logs from the last ten minutes | docker logs --since 10m <container> |
| Live CPU and memory, every container | docker stats |
| Processes running inside | docker top <container> |
| Everything Docker knows about it | docker inspect <container> |
| One field, with a Go template | docker inspect -f '{{.State.Status}}' <container> |
| Why it exited | docker inspect -f '{{.State.ExitCode}}' <container> |
| Which ports are published | docker port <container> |
| Files changed since the image | docker diff <container> |
| Run a one-off command inside | docker exec <container> <command> |
| Copy a file out | docker cp <container>:/path/file ./file |
| Copy a file in | docker cp ./file <container>:/path/file |
| Attach to the main process | docker 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
| Task | Command |
|---|---|
| List images | docker images |
| List dangling images | docker images --filter dangling=true |
| Download one | docker pull <image>:<tag> |
| Add a tag | docker tag <image> <registry>/<name>:<tag> |
| Log in to a registry | docker login <registry> |
| Push it | docker push <registry>/<name>:<tag> |
| Delete an image | docker rmi <image> |
| See how each layer was built | docker history <image> |
| Read an image's metadata | docker image inspect <image> |
| Save an image to a tar file | docker save -o <file>.tar <image> |
| Load one back | docker load -i <file>.tar |
| Check an image for known CVEs | docker 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
| Task | Command |
|---|---|
| Build from the Dockerfile in this folder | docker build -t <name>:<tag> . |
| Build from a Dockerfile somewhere else | docker build -f <path>/Dockerfile -t <name> . |
| Build ignoring the cache | docker build --no-cache -t <name> . |
| Pass a build argument | docker build --build-arg KEY=value -t <name> . |
| Stop at one stage of a multi-stage build | docker build --target <stage> -t <name> . |
| Build for another architecture | docker build --platform linux/amd64 -t <name> . |
| Build and push several architectures at once | docker buildx build --platform linux/amd64,linux/arm64 -t <name> --push . |
| See the full, unfolded build log | docker build --progress=plain -t <name> . |
| List builders | docker buildx ls |
| Clear the build cache | docker 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.
| Task | Command |
|---|---|
| Create a named volume | docker volume create <name> |
| List volumes | docker volume ls |
| Where a volume lives on disk | docker volume inspect <name> |
| Delete a volume | docker volume rm <name> |
| Delete every unused volume | docker volume prune |
| Mount a named volume | docker run -v <name>:/data <image> |
| Mount a folder from your machine | docker run -v /host/path:/app <image> |
| Mount it read-only | docker run -v /host/path:/app:ro <image> |
| The same thing, spelled out | docker run --mount type=bind,src=/host/path,dst=/app <image> |
| Delete a container and its anonymous volumes | docker rm -v <container> |
| Back a volume up to a tar file | docker 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
| Task | Command |
|---|---|
| List networks | docker network ls |
| Create a network | docker network create <name> |
| Run a container on it | docker run -d --network <name> <image> |
| Attach a container that is already running | docker network connect <name> <container> |
| Detach one | docker network disconnect <name> <container> |
| See what is on a network | docker network inspect <name> |
| Delete a network | docker network rm <name> |
| Delete every unused network | docker 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.
| Task | Command |
|---|---|
| Start everything in the background | docker compose up -d |
| Start, rebuilding changed images | docker compose up -d --build |
| Stop and remove the containers | docker compose down |
| Also remove the named volumes | docker compose down -v |
| What is running | docker compose ps |
| Follow the logs of everything | docker compose logs -f |
| Follow one service | docker compose logs -f <service> |
| Shell into a service | docker compose exec <service> sh |
| Run a one-off command in a new container | docker compose run --rm <service> <command> |
| Restart one service | docker compose restart <service> |
| Rebuild without the cache | docker compose build --no-cache |
| Pull newer images | docker compose pull |
| Print the merged, resolved config | docker compose config |
| Rebuild and sync as files change | docker 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.
| Task | Command | Also removes |
|---|---|---|
| See what is using the disk | docker system df | Nothing. Start here |
| The same, itemised | docker system df -v | Nothing |
| Delete stopped containers | docker container prune | Their writable layers |
| Delete dangling images | docker image prune | Only untagged layers |
| Delete every image no container uses | docker image prune -a | Images you will have to pull again |
| Delete unused volumes | docker volume prune | The data in them, permanently |
| Delete unused networks | docker network prune | |
| Clear the build cache | docker builder prune | Nothing, but the next build is slow |
| The usual sweep | docker system prune | Stopped containers, unused networks, dangling images, build cache |
| The aggressive sweep | docker system prune -a | All of the above plus every unused image |
| Everything, volumes included | docker system prune -a --volumes | Volume 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
| Situation | Way out |
|---|---|
| Port is already allocated | docker 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 away | docker logs <container> first, then docker run --rm -it --entrypoint sh <image> and run the command by hand |
| Container exits and logs nothing | docker inspect -f '{{.State.ExitCode}}' <container>. 137 means killed, usually the memory limit |
| The image has no shell | scratch 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 output | docker build --progress=plain --no-cache |
| Build ignores the change you just made | Check .dockerignore is not excluding the file, then rebuild with --no-cache |
| Cannot delete an image, still in use | docker ps -a to find the container holding it, docker rm that, then docker rmi again |
| Permission denied on a bind-mounted file | The container user does not match yours. Run with -u "$(id -u):$(id -g)", or fix the ownership on the host |
| exec format error | Wrong architecture. Add --platform linux/amd64, or rebuild with buildx for the machine you are on |
| Container cannot reach your machine | Use host.docker.internal, adding --add-host=host.docker.internal:host-gateway on Linux |
| One container cannot reach another | Put both on the same user-defined network and use the container name as the hostname |
| Disk is full | docker 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
| Instruction | What it does | Watch out for |
|---|---|---|
FROM | The image the build starts from | Pin a tag. FROM node means node:latest |
WORKDIR | Sets the working directory, creating it if needed | Use this, not RUN cd, which does not persist |
COPY | Copies files in from the build context | Paths are relative to the context, not the Dockerfile |
ADD | COPY, plus URL fetching and tar extraction | Prefer COPY; the extra behaviour surprises people |
RUN | Runs a command at build time | Each one adds a layer. Chain with && where it matters |
ENV | A variable for the build and for the running container | Never a secret. It is baked into the image |
ARG | A variable for the build only | Also not a secret: docker history shows it |
EXPOSE | Documents which port the app listens on | Publishes nothing. -p does that |
CMD | Default command or arguments | Only the last one counts |
ENTRYPOINT | The executable the container always runs | CMD becomes its arguments |
USER | Switches user for the rest of the build and at run time | Add one. Root is the default |
VOLUME | Declares a path as a mount point | Creates an anonymous volume when nothing is mounted there |
HEALTHCHECK | Command Docker runs to test the container | Compose and orchestrators act on it; docker run only reports it |
LABEL | Metadata on the image | |
STOPSIGNAL | Signal sent by docker stop | Default 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
CMD | ENTRYPOINT | |
|---|---|---|
| What it is | Default arguments | The thing that runs |
docker run image ls | Replaces it entirely | Appends ls as an argument |
| How to override | docker run <image> <command> | docker run --entrypoint <command> <image> |
| Good alone for | An image that runs one fixed command | Rarely useful alone |
| Used together | Becomes the default arguments | Becomes 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.
- Start smaller.
-slimand-alpinetags are a fraction of the default. Alpine uses musl rather than glibc, which occasionally breaks native modules, so test rather than assume. - Use a multi-stage build so compilers and source never reach the final stage.
- Write a
.dockerignore. It keepsnode_modules,.gitand build output out of the context, which makes the upload smaller and stopsCOPY . .shipping them. - Install production dependencies only in the runtime stage.
- 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.