Foundations

The parts you need before any of this runs.

Docker, Compose, volumes, networks and reverse proxies, explained once so the rest of the site can assume them.

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.

If this is your first container, the media server covers Docker, Compose and Arcane properly, starting from nothing. This page is a recap, not a tutorial.

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
This is the detail people get wrong. A container can be running while the application inside it is wedged. 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.

KindLooks likeLives
Bind mount./paperless/data:/usr/src/paperless/dataIn a folder next to your compose file, where you can see it
Named volumepgdata:/var/lib/postgresql/dataSomewhere 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.

1000 is a Linux assumption. The first user on most Linux systems is 1000, which is why every guide uses it. On macOS the first user is usually 501, and on a NAS the vendor may use something else again. Run id -u and id -g on the machine that will host the stack rather than copying the number from anywhere, including from this site.
Back up the folder, not the container. If your compose file and your bind-mount folders are backed up, you can rebuild the whole stack on a new machine. Databases want a proper dump rather than a file copy while running, which the Hardware page covers.

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.

ProxyConfig styleSuits
CaddyA few lines per site, certificates automaticMost people, most of the time
TraefikLabels on each container, discovered automaticallyStacks that change often
Nginx Proxy ManagerWeb UIPeople 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.

Putting a proxy in front of something does not make it safe to publish. A proxy gives you TLS and tidy hostnames. It does not add authentication, and it does not fix an app that has none. Access lists what should stay off the internet regardless of how well your proxy is configured.

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.

Pin the major version on anything holding your data. Wiki.js is the clearest example in this list. Its version 3 documentation states plainly that it is an unstable beta and should not be installed in production, so the tag to run is 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-start entrypoint. Compose files that override the command crash on start until changed to node 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.