Docker and Compose

For running a few containers rather than a cluster. Weighted toward the commands you want when something is already broken.

What this page covers

  • Running and stopping

    run, exec, ps and the flags that matter day to day.

  • Compose

    up, down, logs, and the difference between restart and recreate.

  • Debugging

    Getting inside a container, reading logs, and inspecting what it really has.

  • Images and layers

    Building, tagging, and why the image is bigger than you expected.

  • Volumes and data

    Named volumes against bind mounts, and where the data actually lives.

  • Reclaiming disk

    prune, and what each variant deletes - this is the one that catches people.

Running and inspecting

docker ps                    # running containers
docker ps -a                 # including stopped ones
docker run --rm -it alpine sh    # throwaway shell, deleted on exit
docker run -d -p 8080:80 --name web nginx

docker exec -it web sh       # a shell INSIDE a running container
docker logs -f --tail 100 web
docker inspect web           # everything, as JSON
docker stats                 # live CPU and memory per container

docker stop web && docker rm web

--rm on a throwaway container is the difference between a clean machine and forty stopped containers you have to clear out later.

Compose

docker compose up -d            # start, detached
docker compose up -d --build    # rebuild images first
docker compose down             # stop and remove containers + network
docker compose down -v          # ALSO delete named volumes - your data

docker compose ps
docker compose logs -f web
docker compose exec web sh
docker compose restart web      # restart the process, same container
docker compose up -d --force-recreate web   # new container, new config

restart and --force-recreate are not the same thing. A restart reuses the existing container, so a changed environment variable or image is not picked up. If a config change "did nothing", this is usually why.

down -v deletes named volumes. That is your database. down on its own does not.

When something is wrong

# It exited immediately - the logs are still there
docker logs <container>

# Why did it stop? Exit code and reason.
docker inspect <container> --format '{{.State.ExitCode}} {{.State.Error}}'

# It has no shell (distroless, scratch). Attach a toolbox to its namespace:
docker run -it --rm --pid container:<name> --network container:<name> \
  nicolaka/netshoot

# What ports is it actually publishing?
docker port <container>

# What does it think its environment is?
docker exec <container> env

A container that exits instantly has usually failed at its entrypoint, and docker logs still has the output even though docker ps shows nothing.

Images, builds and layers

docker images
docker build -t myapp:local .
docker build --no-cache -t myapp:local .     # ignore the layer cache
docker history myapp:local                   # which layer added the weight

docker tag myapp:local registry.example.com/myapp:1.4
docker push registry.example.com/myapp:1.4

Order your Dockerfile from least to most frequently changed. Copy the dependency manifest and install BEFORE copying the source, or every source edit invalidates the dependency layer and reinstalls everything:

COPY package*.json ./
RUN npm ci
COPY . .            # this line changes constantly - keep it last

Volumes, and where data actually is

docker volume ls
docker volume inspect mydata
docker run -v mydata:/var/lib/data myapp      # named volume - managed
docker run -v "$(pwd)":/app myapp             # bind mount - your folder

A named volume is managed by Docker and survives down. A bind mount is a directory on your machine and changes both ways. Use bind mounts in development so edits appear immediately, and named volumes for anything you would be upset to lose.

Reclaiming disk, carefully

This is the section people come back for. Docker does not clean up after itself and the numbers get large quietly.

docker system df                # what is using the space, by category

docker container prune          # stopped containers
docker image prune              # dangling (untagged) images only
docker image prune -a           # every image not used by a container
docker builder prune            # the build cache, often the biggest
docker volume prune             # UNUSED volumes - can delete real data

docker system prune             # containers + networks + dangling images
docker system prune -a --volumes    # nearly everything. Read that twice.

Run docker system df first, every time. It tells you which category is actually large, and usually it is the build cache rather than the images - in which case builder prune alone reclaims the space with no risk to anything you are running.

volume prune is the one to be careful with: "unused" means no running container references it, and a stopped database is unused by that definition.