StarPunk 2.0.0 logs in with Pocket ID and issues its own Micropub tokens (ADR-064 in the StarPunk repo), so the Gondulf IndieAuth server is no longer used. Removes its task, templates, Caddy route, subdomain and service entry, and moves its DNS record to removed_records. The StarPunk template was still on v1.6.0-rc.2 with Gondulf hairpin hosts; deploying it would have broken login. It now matches production: v2.0.0, Pocket ID hairpin, OIDC client from vault_starpunk.pocketid and the admin sub in starpunk_admin_sub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
8.4 KiB
Markdown
168 lines
8.4 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Overview
|
|
|
|
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.
|
|
|
|
If a local `DEPLOYMENT_LEARNINGS.md` exists (it's gitignored), read it for
|
|
past lessons.
|
|
|
|
## Working alongside other agents (read first)
|
|
|
|
Several agents change this repo and deploy to the same server at once. Two
|
|
things went wrong before these rules: a deploy from an unpushed branch was
|
|
silently reverted by the next deploy from main, and vault edits overwrote each
|
|
other. Follow these steps:
|
|
|
|
1. **Never edit the main checkout.** Start each task in its own worktree:
|
|
`bin/worktree <branch-name>` (based on the latest origin/main, with the
|
|
vault files linked in).
|
|
2. **Land on main before deploying.** Commit, `git fetch && git rebase
|
|
origin/main`, `git push origin HEAD:main`, then deploy that exact commit.
|
|
`deploy-guard.yml` (imported by site.yml and dns.yml) refuses real runs from
|
|
a checkout that is dirty or isn't origin/main. `--check` runs are always
|
|
allowed; `-e allow_unpushed=true` is for emergencies only, and the fix must
|
|
be pushed straight after.
|
|
3. **Deploy with `bin/deploy`**, not ansible-playbook directly. It holds a lock
|
|
so only one deploy runs at a time (`bin/deploy --tags <service>`;
|
|
`PLAYBOOK=dns.yml bin/deploy` for DNS). Use the narrowest tag that covers
|
|
your change.
|
|
4. **Edit the vault only with `bin/vault-run <command>`.** It locks, backs up,
|
|
decrypts to a private temp file passed as `$VAULT_PLAIN`, checks the YAML,
|
|
re-encrypts and swaps it in at once. Never `ansible-vault edit`, and don't
|
|
decrypt vault.yml in place.
|
|
5. **Check the live result after deploying** (e.g. `docker inspect`, a login
|
|
test). If someone else's deploy undid yours, rebase, push, and redeploy
|
|
rather than deploying an older checkout.
|
|
6. **Shared files** (Caddyfile.j2, domains.yml, dns.yml, main.yml task lists)
|
|
are touched by everyone: keep edits small, rebase just before pushing, and
|
|
resolve conflicts by keeping both sides.
|
|
|
|
## Common Commands
|
|
|
|
```bash
|
|
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)
|
|
|
|
# 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
|
|
```
|
|
|
|
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
|
|
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`.
|
|
|
|
DNS A records point at the public IP; Caddy on the same machine routes by
|
|
subdomain.
|
|
|
|
### Roles (`site.yml` runs common, docker, cron)
|
|
- **bootstrap**: user, SSH key, passwordless sudo, UFW, then `artis3n.tailscale`.
|
|
- **common**: `aptitude`, `jq`, daily unattended security updates, 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).
|
|
|
|
### Docker role categories (`roles/docker/tasks/<category>/`)
|
|
- **infrastructure/**: Caddy, Pocket ID, 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
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
Never hardcode domains, IPs or secrets in templates; use these variables.
|
|
|
|
### 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
|
|
|
|
### 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
|
|
`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
|
|
- 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.
|
|
|
|
### 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, StarPunk. 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 (v2.0.0+) also
|
|
runs its own IndieAuth server so Micropub apps can post; Gondulf was retired.
|
|
|
|
### 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
|
|
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 }}"
|
|
```
|
|
|
|
Most often needed for `subdomains.pocketid` (OIDC); also `subdomains.git`
|
|
(Gitea itself) and `primary_domain` (StarPunk).
|
|
|
|
### Template pattern
|
|
```yaml
|
|
networks:
|
|
default:
|
|
external: true
|
|
name: "{{ docker.network_name }}"
|
|
```
|
|
|
|
### 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
|