Bring the docs up to date with the running stack

The READMEs, CLAUDE.md and todo.md still described Authentik, services
removed long ago (Ghost, MMDL, Postiz, Pinchflat...) and two servers. Now
they list the 24 services actually deployed (checked against docker ps),
explain that netcup and docker-01 are one machine, document Gondulf,
StarPunk, Listmonk, Gokapi and dockcheck, and use bin/deploy throughout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Phil
2026-10-31 11:30:50 -06:00
co-authored by Claude Opus 5.5
parent 0282debf07
commit 0c14ee5b1d
8 changed files with 426 additions and 639 deletions
+89 -118
View File
@@ -4,9 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Overview
This is a personal infrastructure Ansible playbook that automates deployment and management of 21 self-hosted Docker services across two domains (`thesatelliteoflove.com` and `nerder.land`). The setup uses Tailscale VPN for secure networking and Caddy for reverse proxy with automated HTTPS.
Personal infrastructure as Ansible: 24 self-hosted Docker services on one
server, across `thesatelliteoflove.com` and `nerder.land`. Caddy is the
reverse proxy (automatic HTTPS), Pocket ID is the passkey SSO, and Tailscale
is how Ansible reaches the server. See `README.md` for the service list.
**Important**: Always review `DEPLOYMENT_LEARNINGS.md` when working on this repository for lessons learned and troubleshooting guidance.
If a local `DEPLOYMENT_LEARNINGS.md` exists (it's gitignored), read it for
past lessons.
## Working alongside other agents (read first)
@@ -41,156 +45,123 @@ other. Follow these steps:
## Common Commands
### Initial Setup
```bash
# Install Ansible dependencies
ansible-galaxy install -r requirements.yml
bin/worktree <branch> # start a task (see above)
bin/deploy --tags <service> # deploy one service
bin/deploy --tags media,productivity # deploy categories
bin/deploy --tags services # every service, no system setup
bin/deploy --tags common,cron # base setup and cron jobs only
bin/deploy --check --diff --tags <tag> # dry run, allowed from any checkout
PLAYBOOK=dns.yml bin/deploy # Route53 records
bin/vault-run <command> # edit secrets ($VAULT_PLAIN)
# Bootstrap new server (creates user, installs Tailscale, security hardening)
ansible-playbook bootstrap.yml -i hosts.yml
# Deploy all Docker services
ansible-playbook site.yml -i hosts.yml
# Update DNS records in AWS Route53
ansible-playbook dns.yml -i hosts.yml
# First-time setup of a new server (public IP, before Tailscale)
.venv/bin/ansible-playbook bootstrap.yml -i hosts.yml --vault-password-file vault_pass
```
### Service Management
```bash
# Deploy specific services using tags (now properly isolated)
ansible-playbook site.yml -i hosts.yml --tags caddy --vault-password-file vault_pass
ansible-playbook site.yml -i hosts.yml --tags pocketid --vault-password-file vault_pass
ansible-playbook site.yml -i hosts.yml --tags gitea --vault-password-file vault_pass
ansible-playbook site.yml -i hosts.yml --tags docker --vault-password-file vault_pass # entire docker role
ansible-playbook site.yml -i hosts.yml --tags services --vault-password-file vault_pass # all services only
# Deploy services by category
ansible-playbook site.yml -i hosts.yml --tags infrastructure --vault-password-file vault_pass
ansible-playbook site.yml -i hosts.yml --tags media,productivity --vault-password-file vault_pass
ansible-playbook site.yml -i hosts.yml --tags development,monitoring --vault-password-file vault_pass
# Deploy only infrastructure components
ansible-playbook site.yml -i hosts.yml --tags common,cron --vault-password-file vault_pass
```
The venv (`.venv/`), `vault_pass` and `group_vars/all/vault.yml` live in the
main checkout; the `bin/` helpers find them from any worktree.
## Architecture
### Host Configuration
- **Bootstrap Host** (`netcup`): 152.53.36.98 - Initial server setup target
- **Docker Host** (`docker-01`): 100.70.169.99 - Main service deployment via Tailscale
### Host
One server, `cogitator-01` (netcup VPS), listed twice in `hosts.yml`:
- `netcup` (group `bootstrap`): public IP 152.53.36.98, used only by `bootstrap.yml`.
- `docker-01` (group `docker`): Tailscale IP 100.70.169.99, used by `site.yml`.
### Role Structure
- **bootstrap**: Initial server hardening, user creation, Tailscale VPN setup
- **common**: Basic system configuration, UFW firewall management
- **docker**: Comprehensive service deployment (21 containerized applications, organized by category)
- **cron**: Scheduled task management (currently Warhammer RSS feed generation)
DNS A records point at the public IP; Caddy on the same machine routes by
subdomain.
### Docker Role Organization (Reorganized into Logical Categories)
The docker role is now organized into logical service groups under `roles/docker/tasks/`:
### Roles (`site.yml` runs common, docker, cron)
- **bootstrap**: user, SSH key, passwordless sudo, UFW, then `artis3n.tailscale`.
- **common**: `aptitude`, `jq`, UFW on with default deny.
- **docker**: Docker CE, shell setup (zsh, Atuin, bat), ufw-docker, the `lava`
network, and every service.
- **cron**: dockcheck (08:00, auto-updates labelled containers, Gotify notices)
and the Warhammer RSS feed (09:10).
- **infrastructure/**: Core platform components
- Caddy (reverse proxy), Pocket ID (passkey SSO), Dockge (container management)
- **development/**: Development and collaboration tools
- Gitea, ByteStash
- **media/**: Content creation and consumption
- Audiobookshelf, Kavita, Karakeep (formerly Hoarder), Manyfold
- **productivity/**: Personal organization and document management
- Paperless-NGX, Baikal (CalDAV/CardDAV), Syncthing, Heyform, Dawarich, Obsidian LiveSync
- **communication/**: Social media
- GoToSocial (Fediverse)
- **monitoring/**: System monitoring and alerts
- Changedetection, AppriseAPI, Gotify, Cronmaster
### Docker role categories (`roles/docker/tasks/<category>/`)
- **infrastructure/**: Caddy, Pocket ID, Gondulf (IndieAuth), Dockge
- **development/**: Gitea (+ act_runner), ByteStash
- **media/**: Audiobookshelf, Kavita, Karakeep (files named `hoarder`), Manyfold
- **productivity/**: Paperless-ngx, Baikal, Syncthing, HeyForm, Dawarich, Obsidian LiveSync, Gokapi
- **communication/**: GoToSocial, Listmonk, StarPunk (Micropub)
- **monitoring/**: Changedetection, Apprise API, Gotify (+ iGotify), Cronmaster
### Variable Management
**Critical**: This infrastructure uses a centralized variable hierarchy in `group_vars/all/`:
Each category has a tag (`infrastructure`, `media`, …) and each service its
own tag; `services` covers all of them. Karakeep has both `karakeep` and
`hoarder` tags.
- **domains.yml**: Domain and subdomain mappings (use `{{ subdomains.service }}`)
- **infrastructure.yml**: Network configuration, Docker settings (`{{ docker.network_name }}`, `{{ docker.hairpin_ip }}`)
- **vault.yml**: Encrypted secrets with `vault_` prefix
- **services.yml**: Service-specific configuration and feature flags
### Variables (`group_vars/all/`)
- **domains.yml**: `primary_domain`, `secondary_domain`, `subdomains.<service>`, email domains.
- **infrastructure.yml**: `docker.*` (network name, subnet, fixed IPs), `smtp.*` (Resend), `network.*` (public and Tailscale IPs).
- **vault.yml**: encrypted secrets, one `vault_<service>` dict per service.
**Important**: All templates use variables instead of hardcoded values. Never hardcode domains, IPs, or secrets.
Never hardcode domains, IPs or secrets in templates; use these variables.
### Data Structure
- All service data stored in `/opt/stacks/[service-name]/` on docker host
- Docker Compose files generated from Jinja2 templates in `roles/docker/templates/`
- Environment files templated for services requiring configuration
- All configurations use centralized variables for consistency
### Data
- Each service lives in `/opt/stacks/<service>/` on the server (Dockge in `/opt/dockge/`).
- Compose files come from `roles/docker/templates/<service>-compose.yml.j2`; env and config files from `<service>-env.j2` / `.env.j2` / `-config.json.j2`.
- Retired services' final backups are in `/opt/stacks/_retired/`.
## Key Implementation Details
### Template-Driven Configuration
The docker role uses Jinja2 templates exclusively for all services. When modifying services:
- Update templates in `roles/docker/templates/[service]-compose.yml.j2`
- Environment files use `.env.j2` templates where needed
- Task files organized by category in `roles/docker/tasks/[category]/[service].yml`
- All services now use templated configurations (no static compose files)
### Adding or changing a service
- Template: `roles/docker/templates/<service>-compose.yml.j2`.
- Tasks: `roles/docker/tasks/<category>/<service>.yml`, imported with a tag in that category's `main.yml`.
- Public access: subdomain in `domains.yml`, route in `Caddyfile.j2`, A record in `dns.yml`.
- Use `diff: false` / `no_log: true` on tasks that write secrets.
- Add the label `mag37.dockcheck.update: true` if dockcheck may update it unattended.
### DNS Management
The `dns.yml` playbook manages AWS Route53 records for both domains. All subdomains point to the netcup server (152.53.36.98), with Caddy handling internal routing to the docker host via Tailscale.
### DNS
`dns.yml` manages Route53 A records for both domains with a Route53-only IAM
user (`vault_aws_dns`). It also deletes the records listed under
`removed_records`; add a retired service's subdomain there.
### Security Architecture
- Tailscale provides secure networking between management and service hosts
- Services are network-isolated using Docker
- Caddy handles SSL termination with automatic Let's Encrypt certificates
- UFW firewall managed through Docker integration script
### Security
- Caddy terminates TLS with Let's Encrypt.
- `ufw-docker` makes UFW govern container ports. Only Caddy (80/443) and
Gitea SSH (host port 222) are allowed in from the internet.
- Admin UIs (Syncthing, Cronmaster, Apprise, CouchDB) bind to the Tailscale IP only.
### Service Dependencies
Most services log in through Pocket ID (OIDC, passkeys) at `{{ subdomains.pocketid }}`. When deploying new services, consider:
- Whether SSO integration is needed
- Caddy routing configuration for subdomain access
- Network connectivity requirements within Docker stack
- Hairpinning fixes for internal service-to-service communication
### SSO (Pocket ID)
Pocket ID (`id.thesatelliteoflove.com`) replaced Authentik on 2026-10-31.
Apps that log in with it: Gitea, ByteStash, Audiobookshelf, Kavita, Karakeep,
Manyfold, Paperless-ngx, Gokapi, GoToSocial. Some read the client from env
vars; Gitea, Audiobookshelf and Kavita store it in their own database (set in
their admin UI). OIDC clients are created in Pocket ID's UI and their ID and
secret go in the vault as `vault_<service>.pocketid`. StarPunk logs in with
IndieAuth through Gondulf instead.
### Shared Docker Network (`lava`)
Created by the docker role as `{{ docker.subnet }}` with `ip_range: {{ docker.ip_range }}`. Docker only hands out dynamic addresses from that range, so `172.20.0.x` is reserved for fixed IPs: Caddy (`docker.hairpin_ip`, .5) and Gitea (`docker.gitea_ip`, .6). Both need fixed IPs because ufw-docker firewall rules point at container IPs. A network without the range (the 2024 original) is migrated once with `migrate-lava-network.yml`.
### Hairpinning Resolution
Services inside Docker containers cannot reach external domains that resolve to the same server. Fix by adding `extra_hosts` mappings:
### Hairpinning
Containers can't reach domains that resolve to this server's public IP. Map
any of our domains a container calls to Caddy with `extra_hosts`:
```yaml
extra_hosts:
- "{{ subdomains.pocketid }}:{{ docker.hairpin_ip }}"
- "{{ subdomains.cal }}:{{ docker.hairpin_ip }}"
```
Common domains requiring hairpinning fixes:
- `{{ subdomains.pocketid }}` (Pocket ID SSO)
- `{{ subdomains.cal }}` (Baikal CalDAV)
- Any service domain the container needs to communicate with
Most often needed for `subdomains.pocketid` (OIDC); also `subdomains.git`
(Gitea itself), `subdomains.gondulf` and `primary_domain` (StarPunk).
**Note**: Use variables instead of hardcoded values for maintainability.
### Template pattern
```yaml
networks:
default:
external: true
name: "{{ docker.network_name }}"
```
### Service-Specific Reference Configurations
- **Dawarich**: Based on production compose file at https://github.com/Freika/dawarich/blob/master/docker/docker-compose.production.yml
### Service-Specific Notes
- **Dawarich**: based on https://github.com/Freika/dawarich/blob/master/docker/docker-compose.production.yml
- **Gokapi**: `config.json` is only written on a fresh install (`force: false`); delete it on the server to apply template changes.
- **Caddy**: Caddyfile changes restart it via a handler; the stack uses `recreate: never`.
## Service Memories
- karakeep (formerly called hoarder) is deployed with both 'hoarder' and 'karakeep' tags for backward compatibility
- whenever i ask you what containers need updates, run dockcheck and return a list of containers needing updates
- when i ask for the status container updates i want you to run dockcheck on the docker host https://github.com/mag37/dockcheck?ref=selfh.st
## Variable Management Implementation Notes
**Major Infrastructure Update**: Variable management system was implemented to replace all hardcoded values with centralized variables.
### Key Changes Made:
- Created comprehensive `group_vars/all/` structure
- Updated all Docker Compose templates to use variables
- Fixed service tag isolation (individual service tags now deploy only that service)
- Standardized domain and network configuration
- Organized secrets by service with consistent `vault_` prefix
### Service Tag Fix:
**Critical**: Service tags are properly isolated. Individual service tags deploy only that service. The `services` tag runs all service tasks across all categories.
### Template Pattern:
All templates now follow this pattern:
```yaml
# Use variables, not hardcoded values
networks:
default:
external: true
name: "{{ docker.network_name }}"
extra_hosts:
- "{{ subdomains.pocketid }}:{{ docker.hairpin_ip }}"
```