Lesson 6.1 · Docker & Compose
Modern self-hosting runs on containers, and the everyday tool is Docker. You met the concept in Lesson 4.4 — containers share the host kernel and package an app with its dependencies, lighter than a full VM. Now you’ll actually use them: run services as containers, persist their data, and — the part that matters most for this curriculum — declare an entire multi-service stack in a single text file you commit to git. That last idea is the bridge from “I installed some software” to “my infrastructure is code.”
Why containers changed operations
Section titled “Why containers changed operations”Before containers, installing a service meant wrestling its dependencies onto your specific OS, hoping nothing conflicted with another service, and dreading the upgrade. Containers fixed this by packaging an application with everything it needs to run into a portable, isolated unit:
- Reproducible — the same container runs identically on your laptop, your server, and the cloud. “Works on my machine” stops being a problem, because the machine comes with the app.
- Isolated — each service runs in its own container with its own dependencies; two services needing different versions of the same library no longer conflict.
- Disposable — you can destroy and recreate a container in seconds, which (as you’ll see) changes how fearlessly you operate.
The mental model: images, containers, volumes
Section titled “The mental model: images, containers, volumes”Three concepts unlock Docker:
graph LR
Registry[(Registry<br/>Docker Hub, etc.)] -->|pull| Image[Image<br/>a read-only template]
Image -->|run| Container[Container<br/>a running instance]
Volume[(Volume<br/>persistent data)] -.attached to.- Container
- An image is a read-only template — a packaged application and its dependencies (e.g. the
official
nginximage, orforgejo). You pull images from a registry (Docker Hub is the default public one). - A container is a running instance of an image — like a process (Lesson 1.1) is a running instance of a program. You can run many containers from one image.
- A volume is persistent storage attached to a container. This is the crucial one (below): containers are disposable, but your data must not be.
The critical distinction: ephemeral vs. persistent
Section titled “The critical distinction: ephemeral vs. persistent”Here’s the concept that, misunderstood, causes new self-hosters to lose data — connecting straight back to Module 4:
A container’s own filesystem is ephemeral. Destroy and recreate the container (to upgrade it, say) and anything written inside it is gone. That’s by design — it’s what makes containers disposable and reproducible. So anything that must survive — a database, uploaded files, your git repositories — must live in a volume mounted into the container, not in the container itself.
Basic Docker commands
Section titled “Basic Docker commands”Install Docker (on your server, or a VM on your Proxmox host from Module 4), then:
docker run -d --name web -p 8080:80 nginx # run nginx detached, mapping host:8080 -> container:80docker ps # list running containers (like `ps`, Lesson 1.1)docker ps -a # include stopped onesdocker logs web # a container's logs (like journalctl -u, Lesson 2.2)docker logs -f web # follow them livedocker exec -it web sh # get a shell inside a running containerdocker stop web && docker rm web # stop and remove itdocker images # local imagesdocker pull forgejo/forgejo # fetch an image without running itNotice how much transfers from earlier modules: docker ps is ps, docker logs is
journalctl, port mapping (-p 8080:80) is the ports-and-sockets idea from
Lesson 1.2. Containers aren’t a new world — they’re the
Linux fundamentals you already have, packaged.
Docker Compose: your stack as a file
Section titled “Docker Compose: your stack as a file”Running containers with long docker run commands doesn’t scale past a couple of services, and —
worse — it isn’t reproducible or version-controlled. Docker Compose fixes this: you
declare your whole stack in a single YAML file (compose.yaml), and bring it up or down with one
command.
# compose.yaml — a service declared as codeservices: blog: image: ghost:5 restart: unless-stopped ports: - "8080:2368" volumes: - blog-data:/var/lib/ghost/content # persistent data survives container recreation environment: - url=https://blog.example.com
volumes: blog-data: # a named volume, managed by Dockerdocker compose up -d # create and start everything in the file, detacheddocker compose ps # status of this stackdocker compose logs -f # follow all services' logsdocker compose down # stop and remove the containers (volumes persist!)docker compose pull && docker compose up -d # upgrade: pull new images, recreate containersTwo things about this file make it the heart of the module:
- It’s declarative. You describe the desired state — which images, which ports, which volumes, which settings — and Compose makes reality match it. This is the same mindset as the Infrastructure-as-Code you’ll formalize in Module 7 with Ansible.
- It goes in git. The
compose.yamlis text, so it’s committed, diffed, and reviewed like any code (Module 0). Your services become reproducible: destroy the container,docker compose up -d, and it’s back exactly as specified — with its data intact in the volume.
Why this is the foundation for everything that follows
Section titled “Why this is the foundation for everything that follows”Every service in the rest of this module — the reverse proxy, your blog, your git server, the supporting apps — is deployed as a container declared in a Compose file. By the end you’ll have a small repository of Compose files that is your homelab’s service layer: reproducible, backed up (the volumes), and version-controlled. That repository, plus the Ansible from Module 7, is what lets you rebuild your entire homelab from code — the payoff this whole curriculum drives toward.
Quick self-check
Section titled “Quick self-check”- Explain image vs. container vs. volume in your own words.
- Why is a container’s own filesystem ephemeral, and what’s the consequence for a database?
- Where must persistent data live, and how does that connect to your Module 4 backups?
- What does it mean that a Compose file is “declarative,” and why does that matter?
- Why does keeping
compose.yamlin git make your services reproducible? - Where do secrets go, if not in the committed Compose file?