Files
ansible/CLAUDE.md
T
PhilandClaude Opus 5.5 9b9be6a2f0 Retire Authentik now that every app logs in with Pocket ID
All of Authentik's applications (including Tailscale's tailnet login)
have moved to Pocket ID, and its only recent sign-ins were those
handovers. Drops its stack, the auth. route and DNS record, the auth
subdomain variables, Gotify's leftover hairpin entries, and the dockcheck
exclude; the docs now name Pocket ID as the SSO provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:07:39 -06:00

196 lines
9.6 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 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.
**Important**: Always review `DEPLOYMENT_LEARNINGS.md` when working on this repository for lessons learned and troubleshooting guidance.
## 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
### Initial Setup
```bash
# Install Ansible dependencies
ansible-galaxy install -r requirements.yml
# 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
```
### 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
```
## 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
### 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)
### Docker Role Organization (Reorganized into Logical Categories)
The docker role is now organized into logical service groups under `roles/docker/tasks/`:
- **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
### Variable Management
**Critical**: This infrastructure uses a centralized variable hierarchy in `group_vars/all/`:
- **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
**Important**: All templates use variables instead of hardcoded values. Never hardcode domains, IPs, or secrets.
### 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
## 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)
### 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.
### 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
### 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
### 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:
```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
**Note**: Use variables instead of hardcoded values for maintainability.
### Service-Specific Reference Configurations
- **Dawarich**: Based on production compose file at https://github.com/Freika/dawarich/blob/master/docker/docker-compose.production.yml
## 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 }}"
```