I've used Docker every working day for years, and here's the honest truth: most developers learn maybe five commands (run, build, ps, logs, exec) and then never go deeper. That's like owning a Swiss army knife and only ever using the bottle opener.
After debugging enough "works on my machine" fires, slow CI builds, and mystery "no space left on device" errors, I settled on a set of about 30 patterns that cover 95% of real daily work. Below is the cheat sheet I wish someone had handed me on day one — grouped by what you're actually trying to do, not alphabetically.
Daily flow (the ones you run 50 times a day)
docker ps # running containers only
docker ps -a # ALL containers, including stopped (the one you actually want)
docker images # local images
docker pull nginx # fetch without running
The single most useful flag nobody memorizes early:
docker run --rm -it alpine sh
# --rm = auto-delete the container when it exits (no clutter)
# -it = interactive + tty, so you get a real shell
Spin up a real service with everything attached:
docker run -d --name web \
-p 8080:80 \
-v "$PWD/html:/usr/share/nginx/html:ro" \
-e NGINX_HOST=example.com \
--restart unless-stopped \
nginx:alpine
Read that right to left: image, restart policy, env var, bind-mounted volume (read-only), port mapping, detached, name. Once this line is muscle memory, you stop reaching for docker-compose for trivial things.
Build & image management
docker build -t myapp:1.0 . # tag while building
docker build -t myapp --target deps . # build only one stage of a multi-stage Dockerfile
docker image prune -a # remove ALL unused images (frees serious disk)
docker system df # how much space Docker is eating
Two things that pay back forever:
1. Always have a .dockerignore. Without it, docker build ships your node_modules, .git, and .env into the build context. Builds get slow, layers get huge, secrets leak.
node_modules
.git
.env
*.log
dist
2. Use multi-stage builds. Build in a fat image, copy only the artifact into a slim runtime image. A Node app can drop from 1.2GB to 150MB.
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
Debugging (when something is broken at 2am)
docker logs -f --tail 100 web # follow last 100 lines
docker exec -it web sh # shell into a running container
docker exec -it web sh # alpine has no bash — use sh
docker inspect web # full config: IP, mounts, env, entrypoint
docker stats # live CPU/mem/net per container (like htop)
docker top web # processes inside the container
The debugging combo I use most: docker inspect --format to pull one field without drowning in JSON:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web
Networks & volumes (the "why can't they talk?" problems)
docker network create mynet
docker network ls
docker run -d --name api --network mynet myapi # no -p needed inside the net
docker run -d --name web --network mynet nginx
Containers on the same user-defined network resolve each other by container name. So from web you can curl http://api:3000. This is the part that makes people stop hand-managing IPs.
For data:
docker volume create pgdata # named volume (managed by Docker)
docker run -d -v pgdata:/var/lib/postgresql/data postgres
docker volume ls
docker volume rm pgdata
Rule of thumb: named volumes for databases (Docker manages them, survives down), bind mounts (-v $PWD:/app) for source code you're editing live.
Compose (when docker run gets too long)
docker compose up -d # build + start everything in compose.yaml
docker compose down -v # stop AND delete volumes (the -v is the dangerous one)
docker compose logs -f api # one service's logs
docker compose exec db psql # run a command in a service
docker compose build --no-cache api
A starter compose.yaml that's worth memorizing:
services:
web:
build: .
ports: ["8080:3000"]
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on: [db]
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:
docker compose up -d and you have a full stack. docker compose down -v tears it all down including data — perfect for a clean local reset.
Advanced / cleanup (the ones that save your disk)
docker system prune -a --volumes # nuclear: everything not currently in use
docker tag myapp:1.0 me/myapp:latest
docker push me/myapp:latest
docker save myapp:1.0 | gzip > app.tar.gz # export image for offline/airgapped transfer
docker load < app.tar.gz
docker context use remote # run docker CLI against a remote daemon
And the one that makes builds 10x faster on CI — BuildKit cache mounts:
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm \
npm ci
The npm cache survives between builds instead of being thrown away every layer. Pip, apt, and cargo all benefit from the same trick.
The 80/20
If you only remember five things:
-
docker run --rm -itfor throwaway shells. -
.dockerignore+ multi-stage builds keep images tiny. -
docker exec -it <c> shto get inside a sick container. - User-defined networks let containers talk by name.
-
docker system dfthendocker system prune -awhen the disk fills up.
Docker's CLI has ~60 commands, but these 30 patterns are what actually get typed in a real workday. Once they're reflexive, you stop fighting containers and start using them as the disposable, reproducible environments they're meant to be.
If you found this useful, I put together a printable version with all **50 commands* plus a quick-reference PDF, common failure modes, and copy-paste snippets for databases, queues, and reverse proxies: The Complete Docker Command Reference.*
What's the one Docker command or flag you wish you'd learned earlier? I'm always collecting these.
Top comments (0)