Alex Debugs!

Docker Primer

2026/05/23 — alex — docker docker-compose container linux CLI

Docker Primer

Docker is a default way Linux server software gets shipped and run today.

Under the hood Docker is a thin wrapper around two old Linux kernel features: namespaces (process, network, mount, user, ...) and cgroups (resource limits). It is not a virtual machine. The container shares the host kernel.


Core Concepts

  • Image — a read-only filesystem snapshot plus some metadata (entrypoint, env, exposed ports). Built once, run many times. Identified by repo/name:tag, e.g. nginx:1.27-alpine. If you don't specify a tag you get :latest, which I would not use in production.

  • Container — a running (or stopped) instance of an image. Adds a writable layer on top of the image. Has its own PID 1, its own network namespace, its own filesystem view. Containers are ephemeral — when you docker rm it, the writable layer goes with it.

  • Volume — persistent storage that lives outside any single container. Use it for databases, uploads, anything you don't want to lose when a container is recreated. Two flavors:

    • Named volume (-v mydata:/var/lib/mysql) — Docker manages the storage location.
    • Bind mount (-v /srv/mysql:/var/lib/mysql) — you point at a host path directly.
  • Network — by default each Docker host has a bridge network where containers get a private IP and reach the outside world via NAT. You can create your own user-defined bridges, which also give you DNS resolution by container name.

  • Registry — where images live when they are not on your host. Docker Hub is the default; ghcr.io, quay.io and self-hosted registries are common. docker pull fetches, docker push uploads.

  • Dockerfile — text recipe to build an image (FROM, RUN, COPY, CMD, ...). Built with docker build.

  • Compose file (docker-compose.yml or compose.yaml) — declarative description of one or more containers, their volumes, networks and dependencies. Run with docker compose.


Running and Inspecting Containers

  • docker run [opts] IMAGE [cmd] — create and start a single container.
    Useful flags:
    • -d detach (run in background)
    • --rm delete the container as soon as it exits
    • -it interactive + tty (for a shell)
    • --name NAME give it a human-readable name
    • -p HOST:CONTAINER publish a port (e.g. -p 8080:80)
    • -v SRC:DST mount a volume or host path
    • -e KEY=VALUE set an environment variable
    • --restart unless-stopped keep it running across reboots
  • docker ps running containers. Add -a to also see exited ones.
  • docker logs CONTAINER print stdout/stderr. Add -f to follow, --tail 100 to limit.
  • docker exec -it CONTAINER sh open a shell inside a running container. Use bash if the image has it (Alpine images don't).
  • docker inspect CONTAINER dumps everything Docker knows about the container as JSON. Pipe through jq for pretty output.
  • docker stop CONTAINER SIGTERM, then SIGKILL after 10 seconds.
  • docker start / docker restart / docker rm — what you would expect. rm -f stops and removes in one go.

A typical "just give me a shell on Debian" one-liner:

docker run --rm -it debian:bookworm bash

Working with Images

  • docker images list local images.
  • docker pull IMAGE fetch from a registry.
  • docker push IMAGE upload to a registry (after docker login).
  • docker build -t myapp:1.0 . build an image from the Dockerfile in the current directory.
  • docker tag SRC DEST add another name to an image. Cheap, no copy.
  • docker rmi IMAGE remove an image. Will refuse if a container still references it.

Volumes and Networks

  • docker volume ls, docker volume create NAME, docker volume rm NAME, docker volume inspect NAME.
  • docker network ls, docker network create NAME, docker network connect NAME CONTAINER.

If you create a user-defined network and put two containers into it, they reach each other by container name — no need to publish ports or hard-code IPs.


Cleanup

Docker hoards disk space. Stopped containers, dangling images, unused volumes and build cache all stay around until you tell them to leave.

  • docker ps -a — see what is taking up space.
  • docker system df — summary of disk usage by category.
  • docker container prune — remove all stopped containers.
  • docker image prune — remove dangling images. Add -a to also remove any image with no container using it (be careful).
  • docker volume prune — remove unreferenced volumes. This deletes data, do not run blindly.
  • docker system prune — the lot. With -a --volumes it is the nuclear option.

docker compose

Once you have more than one container that has to come up together (web + db, app + cache + reverse-proxy), do not script docker run calls. Write a compose.yaml.

  • docker compose up -d create and start everything in the file.
  • docker compose down stop and remove containers and the default network. Add -v to also remove named volumes (data loss).
  • docker compose ps what is running for this project.
  • docker compose logs -f [SERVICE] tail the logs.
  • docker compose exec SERVICE sh shell into one of the services.
  • docker compose pull fetch newer images.
  • docker compose restart [SERVICE] restart without recreating.

Note the space between docker and compose. The legacy docker-compose Python tool (with the dash) still works on many systems but is no longer the upstream — Compose is now a Go plugin shipped with Docker.

A minimal compose.yaml for a database with persistent storage, on its own network, reachable from the host on port 5432:

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: changeme
    volumes:
      - dbdata:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"

volumes:
  dbdata:

docker compose up -d in the same directory and you have a database. docker compose down (without -v) tears everything down but keeps the data. That is the whole loop.


Things That Will Bite You

  • :latest is not a version, it is whatever the registry decided to call latest the last time you pulled. Pin tags in production.
  • Logs grow forever unless you configure a log driver with rotation. Add --log-opt max-size=10m --log-opt max-file=3 or set it in /etc/docker/daemon.json.
  • Time inside a container is the host clock. If the host is wrong, the container is wrong.
  • docker system prune -a --volumes deletes data. Read the prompt, then read it again.