# Personal Infrastructure Ansible Playbook Ansible that builds and runs a personal self-hosted stack: 24 Docker services on one server, served on `thesatelliteoflove.com` and `nerder.land`, with Caddy for HTTPS and Pocket ID (passkeys) for single sign-on. ## The server There is one server (hostname `cogitator-01`, a netcup VPS). The inventory lists it twice, under two names: | Inventory name | Address | Used by | |---|---|---| | `netcup` (group `bootstrap`) | 152.53.36.98 (public IP) | `bootstrap.yml`, for first-time setup before Tailscale is up | | `docker-01` (group `docker`) | 100.70.169.99 (Tailscale IP) | `site.yml`, for everything after that | All public DNS records point at the public IP. Caddy on the same machine takes ports 80/443 and passes each subdomain to the right container. ## Services | Category | Service | Address | Login | |---|---|---|---| | Infrastructure | Caddy (reverse proxy, static sites) | — | — | | | Pocket ID (passkey OIDC provider) | id.thesatelliteoflove.com | passkeys | | | Gondulf (IndieAuth for the main site) | gondulf.thesatelliteoflove.com | email code | | | Dockge (compose stack manager) | port 5001, no public route | local | | Development | Gitea + Actions runner | git.thesatelliteoflove.com (SSH on port 222) | Pocket ID | | | ByteStash (code snippets) | snippets.thesatelliteoflove.com | Pocket ID | | Media | Audiobookshelf | audio.thesatelliteoflove.com | Pocket ID | | | Kavita (ebooks, OPDS, KOReader sync) | books.thesatelliteoflove.com | Pocket ID | | | Karakeep (bookmarks, formerly Hoarder) | bookmarks.thesatelliteoflove.com | Pocket ID | | | Manyfold (3D models) | models.thesatelliteoflove.com | Pocket ID | | Productivity | Paperless-ngx (documents) | paper.thesatelliteoflove.com | Pocket ID | | | Baikal (CalDAV/CardDAV) | cal.thesatelliteoflove.com | local | | | Syncthing | Tailscale only, port 8384 | local | | | HeyForm (forms) | forms.nerder.land | local | | | Dawarich (location history) | loclog.thesatelliteoflove.com | local | | | Obsidian LiveSync (CouchDB) | Tailscale only, port 5984 | local | | | Gokapi (file sharing) | gokapi.thesatelliteoflove.com | Pocket ID | | Communication | GoToSocial (fediverse) | social.thesatelliteoflove.com | Pocket ID | | | Listmonk (mailing lists) | listmonk.nerder.land | local | | | StarPunk (Micropub for the main site) | starpunk.thesatelliteoflove.com | IndieAuth | | Monitoring | Changedetection | watcher.thesatelliteoflove.com | local | | | Apprise API (notifications) | Tailscale only, port 8000 | — | | | Gotify + iGotify assistant | gotify.thesatelliteoflove.com, gotify-assistant.thesatelliteoflove.com | local | | | Cronmaster (cron UI) | Tailscale only, port 40123 | local | "Tailscale only" means the port is bound to the Tailscale IP and has no public address. Caddy also serves static sites from `/opt/stacks/caddy/site/` (the files are not managed by Ansible): the apex `thesatelliteoflove.com` (plus WebFinger for the fediverse and Tailscale login), `pdg.thesatelliteoflove.com`, `nerder.land` and `repair.nerder.land`. `repaircafe.nerder.land` redirects to the Listmonk signup form. ## Layout ``` ├── site.yml # Main playbook: common, docker and cron roles ├── bootstrap.yml # First-time server setup (user, SSH key, UFW, Tailscale) ├── dns.yml # Route53 A records (adds current ones, removes retired ones) ├── deploy-guard.yml # Stops real runs from anything but a clean origin/main ├── migrate-lava-network.yml # One-off: recreate the shared docker network ├── hosts.yml # Inventory ├── requirements.yml # Galaxy roles (artis3n.tailscale) ├── bin/ │ ├── worktree # Start a task in its own git worktree │ ├── deploy # Run a playbook while holding the deploy lock │ └── vault-run # Edit vault.yml safely ├── group_vars/all/ │ ├── domains.yml # Domains and subdomain map │ ├── infrastructure.yml # Docker network, IPs, SMTP, paths │ └── vault.yml # Encrypted secrets (gitignored) └── roles/ ├── bootstrap/ # First-time setup ├── common/ # Base packages and UFW ├── docker/ # Docker, shell setup and every service └── cron/ # Host cron jobs (dockcheck, Warhammer feed) ``` Role details: [bootstrap](roles/bootstrap/README.md), [common](roles/common/README.md), [docker](roles/docker/README.md), [cron](roles/cron/README.md). ## Usage ### Setup The `bin/` helpers use the virtualenv at `.venv/` in the main checkout. The `ansible` package brings the collections used here (`community.docker`, `community.general`, `amazon.aws`, `ansible.posix`); `boto3` is needed for Route53. ```bash python -m venv .venv && .venv/bin/pip install ansible boto3 .venv/bin/ansible-galaxy install -r requirements.yml ``` `vault_pass` and `group_vars/all/vault.yml` are gitignored and live only in the main checkout. `bin/worktree` links them into each worktree. ### Making a change Several agents work on this repo at once, so every change follows the same steps (see [CLAUDE.md](CLAUDE.md) for why): ```bash bin/worktree my-change # new worktree off origin/main # ...edit, commit... git fetch && git rebase origin/main git push origin HEAD:main bin/deploy --tags # deploy that exact commit ``` ### Deploying ```bash bin/deploy --tags kavita # one service bin/deploy --tags media,productivity # whole categories bin/deploy --tags services # every service, no system setup bin/deploy # everything in site.yml bin/deploy --check --diff --tags caddy # dry run (allowed from any checkout) PLAYBOOK=dns.yml bin/deploy # DNS records ``` Category tags: `infrastructure`, `development`, `media`, `productivity`, `communication`, `monitoring`. Role tags: `common`, `docker`, `cron`. Each service has its own tag, named after its task file (e.g. `paperlessngx`, `obsidian-livesync`); Karakeep answers to both `karakeep` and `hoarder`. ### Bootstrapping a new server ```bash .venv/bin/ansible-playbook bootstrap.yml -i hosts.yml --vault-password-file vault_pass ``` ### Secrets All secrets live in `group_vars/all/vault.yml` under `vault_` keys. Edit them only with `bin/vault-run `, never `ansible-vault edit`. ## Security notes - UFW allows SSH from anywhere. `ufw-docker` blocks container ports from the internet except Caddy (80/443) and Gitea SSH (222). - Admin tools (Syncthing, Cronmaster, Apprise, CouchDB) bind to the Tailscale IP only. Dockge has no public route. - Pocket ID is the single sign-on for most apps. Authentik was retired on 2026-10-31. - Paperless-ngx and GoToSocial back up their volumes daily to S3 (7-day retention). Other services have no automated backups yet (see [todo.md](todo.md)).