Docker and Compose
Docker packages an application with everything it needs, so you run one command instead of installing a language runtime, a database driver and six libraries. Compose describes a set of those containers in one file so they start together.
You need two things installed: Docker Engine and the Compose plugin. On a
modern install they arrive together and the command is docker
compose, with a space. If your machine only has
docker-compose with a hyphen, that is the older standalone
version. It mostly works, but it predates some syntax used on this site, and
upgrading is easier than working around it.
A compose file is a list of services. Each service has an image, and usually some combination of ports, volumes and environment variables:
services:
bookstack:
image: lscr.io/linuxserver/bookstack:latest
ports:
- "6875:80"
volumes:
- ./bookstack/config:/config
environment:
- APP_URL=https://wiki.example.com
restart: unless-stopped
Four commands cover almost everything:
- docker compose up -d
- Start everything, in the background
- docker compose ps
- What is running, and on which ports
- docker compose logs -f name
- Follow one service's output. This is where the answer usually is
- docker compose down
- Stop and remove the containers. Volumes survive
docker compose ps showing Up means the process
started, not that the app is healthy. If a page will not load, go straight to
logs -f rather than restarting and hoping.Volumes and where data lives
A container's own filesystem is disposable. Anything you want to survive an update has to live in a volume. Get this wrong and you will lose your documents the first time you pull a new image.
There are two kinds, and the difference matters more than most guides admit.
| Kind | Looks like | Lives |
|---|---|---|
| Bind mount | ./paperless/data:/usr/src/paperless/data | In a folder next to your compose file, where you can see it |
| Named volume | pgdata:/var/lib/postgresql/data | Somewhere under Docker's own directory, managed for you |
This site uses bind mounts for anything you might want to open, copy or back up by hand, which is most application data. It uses named volumes for database storage, because databases care about filesystem behaviour and letting Docker manage that avoids a class of permission problem.
Permissions are the other half. Many images run as a non-root user and let
you choose which one with PUID and PGID. Set them to
your own user, which you can find with id -u and
id -g. If an app starts and then immediately reports that it
cannot write to its data directory, this is almost always why.
id -u and id -g on the machine that will
host the stack rather than copying the number from anywhere, including from
this site.Networks and container names
Containers in the same compose project can reach each other by service
name. That is why a Paperless service can point at
redis://broker:6379 without knowing any IP address. The name
comes from the key in the compose file, not the image and not the container
name.
Two consequences worth internalising:
- Internal traffic uses the container port. If a service
publishes
"8000:8000", other containers still talk to it on 8000, not on whatever host port you chose. A published port is for you, not for them. - A service does not need a published port at all.
Databases in particular should not have one. If only the application talks to
Postgres, leave
ports:off the database entirely and it becomes unreachable from your network.
Host ports are a finite resource and clashes are the most common first-run failure. Two services both wanting 3000 will not start, and the error names the port rather than the app, which is why it confuses people. Forgejo, Gogs and CryptPad all default to 3000. The Generator assigns unique host ports for exactly this reason.
Reverse proxies
A reverse proxy sits in front of your containers and routes by hostname, so
wiki.example.com and files.example.com both arrive
on port 443 and get sent to different services. It also handles TLS
certificates.
You want one as soon as you have more than about three services, for three reasons: you stop memorising port numbers, you get HTTPS, and several of the applications here simply do not work correctly without it. CryptPad needs it. Jitsi needs it. Anything with WebSockets needs it configured to pass those through.
| Proxy | Config style | Suits |
|---|---|---|
| Caddy | A few lines per site, certificates automatic | Most people, most of the time |
| Traefik | Labels on each container, discovered automatically | Stacks that change often |
| Nginx Proxy Manager | Web UI | People who would rather click than edit files |
Start with Caddy unless you have a reason not to. A working site block is two lines, and it gets certificates without you configuring anything.
Updating without breaking things
Updating is pulling a newer image and recreating the container. The data in your volumes stays put.
docker compose pull docker compose up -d
The risk is not the command, it is the tag. latest means
whatever the maintainer pushed most recently, including the next major
version with a changed database schema. That is how people wake up to a
migrated database and an app that will not start.
ghcr.io/requarks/wiki:2. Pulling latest there
changes the schema underneath a running instance.Two more upgrade traps specific to modules on this site:
- Dashy 4.0.0 removed the
build-and-startentrypoint. Compose files that override the command crash on start until changed tonode server.js. - Huly 0.6 to 0.7 is not an in-place upgrade. The project publishes dedicated migration steps and you have to follow them.
Before any update on a stack that matters, take a backup and read the release notes for the app you are updating. Watchtower and similar tools that update automatically are a bad fit for this kind of stack, because an unattended major version bump is exactly the failure you cannot recover from quickly.