Top 10 Essential Docker Commands Every Developer Should Know

These ten commands cover most of what you actually type into a terminal running Docker day to day. The problem with most “top 10 Docker commands” lists is they stop at syntax — one example, no context — and skip the part that actually trips people up: why docker stop sometimes hangs for ten seconds, why docker rm refuses to touch a running container until you add -f, or why your image rebuilds from scratch even though you “didn’t change anything.” Here’s the full set, grouped by what you’re doing with them, with the behavior and gotchas layered on top of the syntax.

Container Lifecycle

These four handle starting, inspecting, stopping, and removing containers — the bulk of daily Docker usage.

docker run

Creates and starts a new container from an image.

docker run -d --name my-app -p 8080:80 -e NODE_ENV=production --rm nginx
  • -d runs it in the background (detached) and prints the container ID instead of attaching your terminal to it.
  • -p 8080:80 maps host:container — that order trips people up constantly. If your app isn’t reachable, check you didn’t flip it.
  • –rm deletes the container (and its anonymous volumes) automatically when it exits. Use it for anything short-lived or you’ll accumulate dead containers.
  • –restart defaults to no — a container won’t survive a host reboot or a crash unless you explicitly set a restart policy like --restart unless-stopped. You can’t combine --restart with --rm; Docker will reject it outright.

Gotcha: if the image isn’t local, docker run pulls it automatically — convenient until you’re debugging a container running an old cached image. Add --pull=always to guarantee the current tag, not whatever was cached last time.

docker ps

Lists containers.

docker ps
docker ps -a
docker ps --filter "status=exited"
docker ps --format "{{.Names}}: {{.Status}}"

By default, docker ps only shows running containers — the most common “where did my container go” moment is forgetting it exited and isn’t in the default list. -a shows everything, including stopped ones. –filter is worth learning beyond status= — name=, ancestor= (image), and label= are all faster than piping to grep once you’ve got more than a handful running.

docker stop (and when you actually need docker kill)

docker stop my-app
docker stop -t 30 my-app
docker kill my-app

docker stop sends SIGTERM to the main process, then waits — 10 seconds on Linux, 30 on Windows, by default — before escalating to SIGKILL if the process hasn’t exited. That wait is exactly why docker stop can feel slow: if your app doesn’t handle SIGTERM at all, you’re paying the full timeout on every stop, every time. Two fixes: handle SIGTERM in your app (flush connections, exit cleanly), or shorten the wait with -t if you know it’s not going to shut down gracefully anyway.

docker kill skips the grace period entirely and sends SIGKILL immediately (or whatever signal you pass with -s). Use it when you know the container is stuck and you’re not waiting ten seconds to confirm that. Don’t reach for it as your default — docker stop existing is the whole point of giving processes a chance to shut down cleanly instead of losing in-flight writes.

docker rm

docker rm my-app
docker rm -f my-app
docker rm -v my-app

Removes a stopped container. On a running one, plain docker rm fails — you have to stop it first, or pass -f, which force-kills it with SIGKILL and removes it in one step. That’s the gotcha: docker rm -f is not “stop cleanly then remove,” it’s “kill immediately then remove.” If your process needs to flush anything before exiting, that data is gone. For anything that matters, run docker stop first and reserve -f for containers you’ve already confirmed are safe to kill.

On volumes: -v removes anonymous volumes attached to that container. Named volumes are never touched, with or without -v — they persist until you explicitly run docker volume rm. Usually what you want, but also why disk usage doesn’t drop the way people expect after cleaning up containers.

Images and Builds

docker pull

docker pull nginx:1.27
docker pull ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30

Downloads an image without running it. If you don’t specify a tag, Docker pulls :latest — and “latest” is just a tag name, not a guarantee you’re getting the newest build. Plenty of images don’t update their latest tag reliably. For anything you’re deploying, pin a real version tag or, better, a digest (@sha256:...) so you know exactly what you’re running and a surprise upstream push can’t change it under you.

docker images

docker images
docker images --filter "dangling=true"
docker images -q

Lists local images. By default it hides intermediate and dangling images, which is why disk usage from Docker often looks mysterious — dangling images (untagged layers left behind by rebuilds) pile up silently in the background. docker images --filter "dangling=true" surfaces them; pair this with the cleanup section below before you assume you need a bigger disk.

docker build

docker build -t myapp:1.2 .
docker build --no-cache -t myapp:1.2 .
docker build --build-arg NODE_ENV=production -t myapp:1.2 .

As of current Docker Engine and Docker Desktop, BuildKit is the default builder for docker build — not the legacy builder (the one exception is building Windows containers, which still use the legacy path). If you’re on an old installation or a script that explicitly disables it, you lose BuildKit’s parallel layer builds and better caching, so it’s worth confirming DOCKER_BUILDKIT isn’t set to 0 anywhere in your environment.

The gotcha that actually costs people time is layer caching order. Docker caches each instruction as a layer and invalidates everything after the first changed instruction — not just the line that changed. If your Dockerfile runs COPY . . before RUN npm install, every single code change invalidates the dependency install, and you rebuild node_modules on every build. Copy only the dependency manifest first, install, then copy the rest of your source:

COPY package.json package-lock.json ./
RUN npm install
COPY . .

Use –no-cache when you actually need a clean rebuild — debugging a cache that’s gone stale, or verifying a build works from scratch before you ship it. Running it as your default “just in case” habit throws away the exact thing BuildKit is good at.

Running Multi-Container Apps

docker compose up

docker compose up -d
docker compose up --build
docker compose up --force-recreate --remove-orphans

Note the space: docker compose (the v2 plugin, built into the Docker CLI) is current. The old standalone Python binary, docker-compose with a hyphen, is Compose v1 — no longer maintained, and Docker’s own migration docs tell you to move to v2. If you’re still typing the hyphenated version out of habit, it may still work today, but you’re one dependency update away from it not being installed at all.

Gotcha: docker compose up does not rebuild your images just because your Dockerfile changed. It only rebuilds when the image doesn’t exist yet or you explicitly pass –build. This is the single most common “I changed my code and it’s not showing up” complaint — Compose is reusing the image it already built. --force-recreate forces new containers even if nothing in the config changed, which is useful when you suspect a container’s internal state (not the image) is the problem.

Debugging a Running Container

docker exec

docker exec -it my-app sh
docker exec -u root my-app whoami
docker exec -w /app my-app ls

Starts a new process inside a running container’s namespaces — it does not touch the container’s main process. This is the right tool for “let me poke around inside this container” almost every time: open a shell with -it, run one-off commands, override the user with -u or working directory with -w without disturbing whatever PID 1 is actually doing.

docker exec vs. docker attach

docker attach is different and far less commonly what you want: it connects your terminal directly to the container’s main process (its actual STDIN/STDOUT/STDERR), not a new process. The gotcha that burns people: if the container wasn’t started with -it, hitting Ctrl-C after attaching sends SIGINT to that main process and can kill the container outright. If it was started with -it, you can detach cleanly with Ctrl-P Ctrl-Q and leave it running. Default to docker exec -it ... sh for debugging; reach for attach only when you specifically need to see or interact with the main process’s own console output.

docker logs

docker logs -f my-app
docker logs --tail 100 my-app
docker logs --since 30m my-app

-f streams new output live, –tail limits how far back you go (default is everything, which is painful on a long-running container), and –since/–until let you scope to a time window instead of scrolling. The gotcha: docker logs only shows anything useful if your logging driver actually captures it. The default json-file driver works fine for this, but if a container’s logging driver is set to none, or something shipping straight to syslog/a remote collector, docker logs will come back empty even though the app is clearly producing output somewhere.

Cleanup

docker system prune

docker system prune
docker system prune -a
docker system prune -a --volumes -f

Plain docker system prune removes stopped containers, unused networks, dangling images only, and unused build cache. Volumes are not touched by default — Docker’s own docs are explicit that this is intentional, to avoid silently deleting data.

-a widens image cleanup from “dangling only” to “any image not currently used by a container” — including tagged images you might still want, just not actively running. –volumes is the flag that actually removes unused volumes, and it’s the one to be careful with: “unused” means not attached to a running container right now, which includes volumes backing containers you’ve stopped but intend to start again. Running docker system prune -a --volumes -f in a cron job or CI cleanup step without checking what’s actually unused is how people lose data they assumed was safe. Run it without -f first and read what it’s about to delete.

Frequently Asked Questions

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

docker stop sends SIGTERM and gives the process a grace period (10s on Linux, 30s on Windows by default) before escalating to SIGKILL. docker kill sends SIGKILL immediately, with no grace period, unless you override the signal with -s. Use stop by default; use kill when you already know the container is unresponsive.

Does docker rm delete volumes?

Not by default, and not named volumes even with -v. docker rm -v only removes anonymous volumes tied to that specific container. Named volumes survive until you explicitly run docker volume rm.

Why does docker build keep rebuilding everything, even unrelated changes?

Almost always Dockerfile instruction order. Docker invalidates the cache for an instruction and everything after it once any earlier instruction changes. Put dependency installation (package.json/requirements.txt + install) before you COPY your full source tree, so code changes don’t force a dependency reinstall on every build.

Should I use docker-compose or docker compose?

docker compose (no hyphen, v2, bundled into the Docker CLI). The hyphenated standalone binary is Compose v1, which Docker no longer maintains. If you have scripts still calling docker-compose, update them — it’s not guaranteed to be installed on newer setups.

What’s the safest way to clean up unused Docker disk space?

Run docker system prune -a without --volumes first to clear unused images and containers, check actual disk recovered, and only add --volumes once you’ve confirmed (with docker volume ls) that what’s left unused is genuinely disposable.

If you’re running these commands as part of a larger orchestration setup, our Kubernetes complete guide covers what changes once a single host and manual container management stop being enough.

Quick Summary

  • docker run starts a container; remember -p host:container order and that --restart defaults to no.
  • docker ps only shows running containers by default — add -a for everything.
  • docker stop gives processes a grace period (SIGTERM then SIGKILL); docker kill skips straight to SIGKILL.
  • docker rm -f force-kills before removing — it’s not a clean stop, use it deliberately.
  • docker pull without a tag grabs :latest, which isn’t a reproducibility guarantee — pin versions or digests for anything deployed.
  • docker images hides dangling images by default; they’re often where your disk space is going.
  • docker build runs on BuildKit by default; order your Dockerfile so dependency installs cache separately from source code changes.
  • docker compose up (v2, space) is current; it won’t rebuild on code changes without --build.
  • docker exec -it for debugging inside a container; save docker attach for when you specifically need the main process’s own console.
  • docker logs depends on your logging driver actually capturing output — empty logs don’t always mean a silent app.
  • docker system prune never touches volumes unless you add --volumes — check what’s “unused” before you force it.