Files
ansible/roles/docker/README.md
T
PhilandClaude Opus 5.5 68a1622665 Deploy StarPunk from its CI-built image
StarPunk's Gitea Actions workflow now builds and pushes the image on
vX.Y.Z tags. Pin the tag in starpunk_version (v2.0.2, the first CI build)
and always pull it, so an image built by hand on the server with the
same name can't be deployed instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:24:08 -06:00

7.6 KiB

Docker Role

Installs Docker and deploys all 24 services. Each service is a Docker Compose stack in /opt/stacks/<service>/ (Dockge lives in /opt/dockge/), built from Jinja2 templates in this role.

What runs before the services

tasks/main.yml does the system setup, then imports each category:

  1. Base packages (python3-pip, virtualenv, duf, …).
  2. Shell setup (shell.yml): zsh as the login shell, Atuin (installed and logged in once, dotfile sync on), and bat.
  3. Docker CE from Docker's apt repository.
  4. ufw-docker.sh copied to /usr/local/bin/ and its rules installed (UFW is restarted once after install).
  5. The shared lava network (see below).
  6. /opt/stacks and /opt/dockge.

Services by category

Tags: each category file has a category tag, each service its own tag.

Infrastructure (infrastructure)

Tag Service Notes
caddy Caddy Fixed IP 172.20.0.5. Ports 80/443 (+443/udp). Caddyfile.j2 changes restart it via a handler; the stack itself is deployed with recreate: never. Static sites are served from /opt/stacks/caddy/site/.
pocketid Pocket ID v2 Passkey OIDC provider at id.. OIDC clients are created in its web UI; their IDs and secrets go in the vault.
dockge Dockge Compose UI on port 5001, managing /opt/stacks.

Development (development)

Tag Service Notes
gitea Gitea + act_runner + BuildKit Fixed IP 172.20.0.6 so the ufw-docker rule for SSH (host port 222) stays valid. Pocket ID login is set up in Gitea's admin UI; OpenID 2.0 login is off. Jobs run inside the runner container (label docker:host), which has the Caddy site folders at /sites but no Docker socket. Image builds use the rootless buildkitd container through unix:///run/buildkit/buildkit/buildkitd.sock (docker buildx create --driver remote).
bytestash ByteStash Pocket ID through env vars.

Media (media)

Tag Service Notes
audiobookshelf Audiobookshelf Pocket ID set up in the app's settings. Podcasts are not used.
kavita Kavita Ebooks at books.. Also handles OPDS and KOReader sync (replaced Calibre-Web, dir2opds and kosync). Pocket ID set up in the app's settings.
karakeep, hoarder Karakeep Files are still named hoarder*. Web, headless Chrome and Meilisearch. Pocket ID through hoarder-env.j2.
manyfold Manyfold Pocket ID through env vars.

Productivity (productivity)

Tag Service Notes
paperlessngx Paperless-ngx Redis, Gotenberg, Tika. Pocket ID through django-allauth (paperlessngx.env.j2). Daily S3 backup of the data volume.
baikal Baikal CalDAV/CardDAV. Caddy redirects the .well-known paths.
syncthing Syncthing Ports on the Tailscale IP only.
heyform HeyForm MongoDB (Percona) and KeyDB. On forms.nerder.land.
dawarich Dawarich App, Sidekiq, PostGIS, Redis. Based on the upstream production compose file.
obsidian-livesync CouchDB for Obsidian LiveSync Port 5984 on the Tailscale IP only. Has a restart handler.
gokapi Gokapi Pocket ID login. config.json is only written on a fresh install, because Gokapi rewrites it on upgrade. To change it, delete /opt/stacks/gokapi/config/config.json and redeploy.

Communication (communication)

Tag Service Notes
gotosocial GoToSocial Pocket ID through env vars. Account domain is the apex; Caddy serves WebFinger there. Daily S3 backup of its volume.
listmonk Listmonk Postgres 17. On listmonk.nerder.land.
starpunk StarPunk Micropub server for the main site. Image built by the StarPunk repo's Gitea Actions on vX.Y.Z tags; deploy a release by bumping starpunk_version. Admin login via Pocket ID; issues its own Micropub tokens.

Monitoring (monitoring)

Tag Service Notes
changedetection Changedetection With a sockpuppetbrowser for JavaScript pages.
appriseapi Apprise API Port 8000 on the Tailscale IP only. Other containers reach it at http://apprise:8000.
gotify Gotify + iGotify assistant Push notifications. Dockcheck sends its update notices here.
cronmaster Cronmaster Cron UI, port 40123 on the Tailscale IP only.

Networking

Shared network (lava)

Every stack joins the external network lava (172.20.0.0/16). Docker only hands out dynamic addresses from 172.20.128.0/17, so 172.20.0.x is free for fixed IPs: Caddy (.5) and Gitea (.6). Both need fixed IPs because the ufw-docker rules point at container IPs. The role creates the network if it is missing and warns if it lacks the reserved range; migrate-lava-network.yml fixes an old network.

Hairpinning

Containers can't reach the server's own public IP, so any app that calls another of our domains (usually Pocket ID for OIDC) maps it to Caddy:

extra_hosts:
  - "{{ subdomains.pocketid }}:{{ docker.hairpin_ip }}"

Firewall

ufw-docker.sh makes UFW apply to published container ports. Only Caddy (80/tcp, 443/tcp, 443/udp) and Gitea (22/tcp, published as 222) are allowed in from the internet; the Caddy and Gitea task files add those rules.

Updates

Containers labelled mag37.dockcheck.update: true are updated automatically each morning by dockcheck (set up in the cron role). Containers without the label (StarPunk, Listmonk, HeyForm, Dockge and most databases) only get a Gotify notice. Dawarich's database and Redis are skipped entirely. StarPunk uses a pinned version tag.

Adding a service

  1. templates/<service>-compose.yml.j2 (plus -env.j2 if needed). Join the external {{ docker.network_name }} network and add extra_hosts for any of our domains the app calls.
  2. tasks/<category>/<service>.yml: make /opt/stacks/<service>, template the files, deploy with community.docker.docker_compose_v2. Use diff: false or no_log: true on tasks that write secrets.
  3. Import it in tasks/<category>/main.yml with a tag named after the service.
  4. Subdomain in group_vars/all/domains.yml, route in templates/Caddyfile.j2, A record in dns.yml.
  5. Secrets under vault_<service> with bin/vault-run.

Files

roles/docker/
├── tasks/
│   ├── main.yml              # System setup, network, category imports
│   ├── shell.yml             # zsh, Atuin, bat
│   ├── infrastructure/       # caddy, pocketid (+ dockge in main.yml)
│   ├── development/          # gitea, bytestash
│   ├── media/                # audiobookshelf, kavita, hoarder (Karakeep), manyfold
│   ├── productivity/         # paperlessngx, baikal, syncthing, heyform, dawarich, obsidian-livesync, gokapi
│   ├── communication/        # gotosocial, listmonk, starpunk
│   └── monitoring/           # changedetection, appriseapi, gotify, cronmaster
├── templates/                # <service>-compose.yml.j2, env and config templates, Caddyfile.j2
├── files/ufw-docker.sh       # UFW integration for Docker
└── handlers/main.yml         # restart caddy, restart obsidian-livesync

Troubleshooting

  • OIDC login fails with a connection error: the container is missing the extra_hosts entry for id.thesatelliteoflove.com.
  • OIDC discovery fails: check the issuer URL has no trailing slash.
  • Caddy fails with "Address already in use": another container took 172.20.0.5; the network lacks the reserved range. Run migrate-lava-network.yml.
  • Gitea SSH stops working after a recreate: Gitea lost its fixed IP; redeploy with --tags gitea to restore it and the firewall rule.