Browsers will use HTTPS for each site for a year. includeSubDomains is left off so hosts not served by this Caddy are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 | |
| 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 | Pocket ID | |
| 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, common, docker, cron.
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.
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 for why):
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 <service> # deploy that exact commit
Deploying
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
.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_<service> keys.
Edit them only with bin/vault-run <command>, never ansible-vault edit.
Security notes
- UFW allows SSH from anywhere.
ufw-dockerblocks 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).