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 rmit, 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.
- Named volume (
-
Network — by default each Docker host has a
bridgenetwork 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 pullfetches,docker pushuploads. -
Dockerfile — text recipe to build an image (
FROM,RUN,COPY,CMD, ...). Built withdocker build. -
Compose file (
docker-compose.ymlorcompose.yaml) — declarative description of one or more containers, their volumes, networks and dependencies. Run withdocker compose.
Running and Inspecting Containers
docker run [opts] IMAGE [cmd]— create and start a single container.
Useful flags:-ddetach (run in background)--rmdelete the container as soon as it exits-itinteractive + tty (for a shell)--name NAMEgive it a human-readable name-p HOST:CONTAINERpublish a port (e.g.-p 8080:80)-v SRC:DSTmount a volume or host path-e KEY=VALUEset an environment variable--restart unless-stoppedkeep it running across reboots
docker psrunning containers. Add-ato also see exited ones.docker logs CONTAINERprint stdout/stderr. Add-fto follow,--tail 100to limit.docker exec -it CONTAINER shopen a shell inside a running container. Usebashif the image has it (Alpine images don't).docker inspect CONTAINERdumps everything Docker knows about the container as JSON. Pipe throughjqfor pretty output.docker stop CONTAINERSIGTERM, then SIGKILL after 10 seconds.docker start/docker restart/docker rm— what you would expect.rm -fstops 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 imageslist local images.docker pull IMAGEfetch from a registry.docker push IMAGEupload to a registry (afterdocker login).docker build -t myapp:1.0 .build an image from theDockerfilein the current directory.docker tag SRC DESTadd another name to an image. Cheap, no copy.docker rmi IMAGEremove 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-ato 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 --volumesit 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 -dcreate and start everything in the file.docker compose downstop and remove containers and the default network. Add-vto also remove named volumes (data loss).docker compose pswhat is running for this project.docker compose logs -f [SERVICE]tail the logs.docker compose exec SERVICE shshell into one of the services.docker compose pullfetch 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
:latestis 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=3or 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 --volumesdeletes data. Read the prompt, then read it again.