DEV Community

James Joyner
James Joyner

Posted on

The Docker CLI Commands I Actually Use Every Day

I've watched a lot of engineers reach for a GUI or a fresh Google search every time they need to do something with Docker. Nothing wrong with that, but after enough years you notice that the day-to-day work — building an image, running a container, tailing its logs, cleaning up the mess afterward — really only touches about fifteen commands. Learn those well, learn the flags people skip, and you stop context-switching in the middle of a task.

This isn't an exhaustive reference. It's the working set I actually type, grouped by the thing I'm trying to do: look around, run something, get inside a running container to debug, and clean up before my disk fills. If you can drive these from muscle memory, you've covered maybe 95% of real Docker work.

Looking around: what's running and what's on disk

The first two commands I run in any unfamiliar environment tell me what's alive and what images are available.

# Running containers only
docker ps

# Everything, including stopped/exited containers
docker ps -a

# Just the IDs — handy for scripting
docker ps -q
Enter fullscreen mode Exit fullscreen mode

docker ps -a is the one people forget. A container that crashed on startup won't show up in plain docker ps, and then you're confused about why "nothing is running." The -a flag shows you the exited ones and, more usefully, their exit status.

For images:

docker images
Enter fullscreen mode Exit fullscreen mode

That's it for orientation. Two commands, and you know the state of the host.

Running containers: the flags that matter

docker run is where most of the real decisions happen, and it's where the useful flags live. Here's a run that uses the ones I reach for constantly:

docker run -d \
  --name web-1 \
  -p 8080:80 \
  -e APP_ENV=staging \
  -v "$(pwd)/data:/var/lib/app" \
  myapp:1.4.2
Enter fullscreen mode Exit fullscreen mode

Breaking that down, because each flag earns its place:

  • -d — detached. Runs in the background instead of holding your terminal hostage.
  • --name web-1 — give it a real name. If you don't, Docker assigns something like nostalgic_bohr, and now every future command needs you to copy a container ID. Naming is a five-second habit that pays off every time you type docker logs web-1.
  • -p 8080:80 — publish a port. Host port on the left, container port on the right. Get the order wrong and you'll swear the app is down when it's just unreachable.
  • -e APP_ENV=staging — set an environment variable. Repeat -e for each one, or use --env-file for a whole file.
  • -v host:container — mount a volume so data survives the container.

For a throwaway container — a quick test, a one-off script — add --rm so it cleans itself up on exit:

docker run --rm -it myapp:1.4.2 sh
Enter fullscreen mode Exit fullscreen mode

--rm plus -it (interactive + TTY) drops you into a shell in a fresh container that deletes itself when you leave. I use this constantly to poke at an image without leaving stopped containers lying around.

Getting inside and watching: exec, logs, stats

Once something is running, most of my time is spent watching it or stepping into it.

To get a shell inside a running container:

docker exec -it web-1 sh
# or, if the image has bash
docker exec -it web-1 bash
Enter fullscreen mode Exit fullscreen mode

exec -it is the workhorse of debugging. Note that it runs a new process in the existing container — it doesn't restart anything, so it's safe to use on something live (within reason). If the container has already exited, exec won't help you; that's a different problem.

For logs, -f follows them like tail -f, and --tail limits how far back you start:

# Follow the last 100 lines and keep streaming
docker logs -f --tail 100 web-1

# Add timestamps
docker logs -f -t web-1
Enter fullscreen mode Exit fullscreen mode

--tail matters more than it looks. On a chatty container, docker logs web-1 with no limit will dump the entire history and scroll your terminal into oblivion. Start with --tail 100 and expand if you need more.

To see live resource usage:

# All containers, live
docker stats

# One container, and exit after a single snapshot
docker stats --no-stream web-1
Enter fullscreen mode Exit fullscreen mode

docker stats is the quickest way to answer "is this thing pegged?" — CPU, memory against its limit, network, and block I/O, refreshed live. The --no-stream flag gives you one snapshot and returns, which is what you want in a script.

Inspecting: getting exact answers with --format

docker inspect returns everything Docker knows about a container or image as JSON. The raw output is a wall of text, so the flag that makes it useful is --format, which takes a Go template and pulls out exactly the field you want:

# What's the container's IP?
docker inspect --format '{{ .NetworkSettings.IPAddress }}' web-1

# Is it running, and what was the exit code?
docker inspect --format '{{ .State.Status }} {{ .State.ExitCode }}' web-1

# List the mounts
docker inspect --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}' web-1
Enter fullscreen mode Exit fullscreen mode

Learning even a little Go template syntax here changes how you work. Instead of eyeballing JSON, you ask a precise question and get a precise answer — which is also what makes inspect scriptable.

Moving files: cp

Sometimes you need a file out of a container (a log, a generated config) or into one (a patched file for a quick test). docker cp works in both directions:

# Out of the container
docker cp web-1:/var/log/app/error.log ./error.log

# Into the container
docker cp ./patched.conf web-1:/etc/app/app.conf
Enter fullscreen mode Exit fullscreen mode

It's not a substitute for a proper volume or a rebuild, but for grabbing a file during an investigation it's exactly right.

Building, tagging, and shipping images

The build-and-push loop is its own small vocabulary:

# Build from the Dockerfile in the current directory and tag it
docker build -t myapp:1.4.2 .

# Add a second tag pointing at the same image
docker tag myapp:1.4.2 registry.example.com/myapp:1.4.2

# Push and pull
docker push registry.example.com/myapp:1.4.2
docker pull registry.example.com/myapp:1.4.2
Enter fullscreen mode Exit fullscreen mode

Two habits worth keeping. First, tag with a real version (1.4.2), not just latestlatest is a source of "it worked on my machine" confusion because it means something different depending on when you last pulled. Second, the -t on build accepts the full registry path, so you can tag for your registry at build time and skip the separate docker tag step.

If a build or push throws an error you don't recognize — and Docker's error messages can be terse — it's worth having a reference for the common ones. I keep a set of Docker error-fix guides at devopsaitoolkit.com/categories/docker for exactly the "what does this one mean" moments.

Cleaning up before your disk fills

This is the part everyone skips until they get a "no space left on device" at the worst possible time. Docker accumulates stopped containers, dangling images, unused volumes, and build cache, and none of it goes away on its own.

Start by seeing where the space actually went:

docker system df
Enter fullscreen mode Exit fullscreen mode

That gives you a breakdown by images, containers, volumes, and build cache. Then prune deliberately:

# Remove stopped containers, dangling images, unused networks, build cache
docker system prune

# Also remove unused images (not just dangling ones)
docker system prune -a

# Volumes are NOT touched by default — prune them separately
docker volume prune
Enter fullscreen mode Exit fullscreen mode

I keep volume prune as a separate, conscious step on purpose. Volumes hold data, and system prune deliberately leaves them alone so you don't wipe a database because you wanted to reclaim a few gigabytes. When you've been burned by an accidental data deletion once, you appreciate that Docker makes you ask for volumes explicitly.

Networks and volumes: the basics worth knowing

You don't need to be a networking expert to be effective, but knowing these four commands covers most needs:

docker network ls
docker network create app-net
docker volume ls
docker volume create app-data
Enter fullscreen mode Exit fullscreen mode

Create a user-defined network and containers on it can reach each other by name — web-1 can talk to db-1 without you hunting down IP addresses. That alone is a reason to prefer a named network over the default bridge for anything with more than one container.

Wrapping up

None of these are advanced. That's the point. The engineers who move fast with Docker aren't the ones who memorized every flag in the manual — they're the ones who made a small, correct set of commands automatic, so the tool gets out of the way and they can think about the actual problem.

If you want the fuller reference version of this — the flags, the templates, the cleanup patterns in one place — I keep a Docker toolkit at devopsaitoolkit.com/stacks/docker. But honestly, the highest-leverage move is just to name your containers and start using --tail today. Save the pattern, not just the command.

Top comments (0)