Skip to content
Chapter 5Docker28.x

The commands you will actually run

Docker has hundreds of flags and you need about fifteen. Here is the working set, plus the Compose file that replaces most of them during development.

4 min read

The Docker CLI is large. Daily use is not. This chapter is the subset that covers almost everything, followed by the tool that replaces most of it.

Inspecting what is running

docker ps                    # running containers
docker ps -a                 # including stopped ones
docker logs -f api           # follow logs, like tail -f
docker logs --tail 100 api   # last 100 lines
docker stats                 # live CPU/memory, like top

docker ps -a is the one people forget. A container that exited immediately does not appear in docker ps, so it looks like nothing happened — when in fact it started, crashed, and is sitting there with the answer in its logs.

Getting inside a container

docker exec -it api sh           # shell in a running container
docker exec -it api env          # just print its environment
docker run -it --rm alpine sh    # throwaway container to poke around

-it is --interactive --tty: keep stdin open and allocate a terminal. Without it you get no prompt and it looks like the command hung.

Building and tagging

docker build -t myapp:1.4.0 .
docker build -t myapp:1.4.0 -t myapp:latest .
docker build --no-cache -t myapp:1.4.0 .
docker build --progress=plain -t myapp:1.4.0 .   # full build output

--progress=plain is how you see what a RUN step actually printed. The default collapsed output hides it, which makes build failures much harder to read than they need to be.

Cleaning up

Docker accumulates disk usage quietly. Check it:

docker system df

Then reclaim:

docker container prune      # stopped containers
docker image prune          # dangling images
docker image prune -a       # every image not used by a container
docker builder prune        # the build cache — often the biggest offender
docker system prune -a      # all of the above

Compose: the thing you will actually use

Running an app plus a database with raw docker run means creating a network, starting the database, waiting for it, starting the app with the right environment — every time. Compose declares it instead.

compose.yaml:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:

Note how chapter three’s concepts show up: db is reachable at the hostname db because Compose puts both services on one network and names them after the service. pgdata is a named volume, so the database survives docker compose down.

The Compose commands

docker compose up -d           # start everything in the background
docker compose up --build      # rebuild images first
docker compose watch           # start with live sync (uses `develop:` above)
docker compose ps              # what is running
docker compose logs -f api     # follow one service
docker compose exec api sh     # shell into a service
docker compose down            # stop and remove containers + network
docker compose down -v         # ...and delete the volumes

A realistic daily loop

docker compose watch                # start the stack, sync source on save
docker compose logs -f api          # watch the app in another terminal
docker compose exec db psql -U app  # open a psql shell when needed
docker compose restart api          # after a config change
docker compose down                 # end of day

That is genuinely most of it. docker compose watch rebuilds only when dependencies change and syncs files otherwise, which removes the rebuild-on-every-save problem that made early Docker development unpleasant.

Worth knowing exists

docker inspect api                  # full JSON: mounts, networks, env, config
docker diff api                     # files changed since the image
docker cp api:/app/out.txt ./       # copy a file out of a container
docker history myapp:1.4.0          # layer sizes — find what made it big

docker history is the one to reach for when an image is unexpectedly large. It shows you exactly which instruction added the weight.

Next: the six mistakes that cost everyone a day at some point.