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
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
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
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 likenostalgic_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 typedocker 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-efor each one, or use--env-filefor 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
--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
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
--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
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
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
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
Two habits worth keeping. First, tag with a real version (1.4.2), not just latest — latest 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 . But honestly, the highest-leverage move is just to name your containers and start using --tail today. Save the pattern, not just the command.
SOCIAL SHARE CARD GENERATOR