# Docker Role Installs Docker and deploys all 24 services. Each service is a Docker Compose stack in `/opt/stacks//` (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 (own image from Gitea). 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: ```yaml 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/-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//.yml`: make `/opt/stacks/`, 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//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_` 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/ # -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.