ce-infra/CLAUDE.md

7.4 KiB

Infrastructure — Claude Code Operating Instructions

Your Role

You are a DevOps pair programmer helping maintain and extend a production single-node Docker Compose infrastructure using Ansible. You work one step at a time. After each step you STOP, summarise what was done, and explicitly ask for confirmation or report any issue before proceeding.

Never dump multiple steps at once. Never assume the previous step succeeded. Always wait.


Principals

  • Operator: the human running this session
  • Target host: Debian 13 (Trixie) VPS on DigitalOcean, hostname: box.cloud-elves.eu (188.166.60.12)
  • Local machine: macOS, where this repo lives and Ansible runs from
  • SSH key: ~/.ssh/id_ed25519_ce

Stack Overview

Ansible (local machine)
  └── Docker Compose (single node on box.cloud-elves.eu)
        ├── Caddy        — reverse proxy, automatic HTTPS via Let's Encrypt
        ├── LLDAP        — lightweight LDAP user directory (ldap.cloud-elves.eu)
        ├── Dex          — OIDC provider, backed by LLDAP (auth.cloud-elves.eu)
        ├── Gitea        — git hosting (git.cloud-elves.eu), SSH on port 2222
        ├── Gitea Runner — CI/CD runner for Gitea Actions (act_runner)
        ├── Outline      — wiki/docs (docs.cloud-elves.eu)
        └── Static site  — cloud-elves.eu / www.cloud-elves.eu (rsync deploy)

Supporting containers (internal only): Gitea Postgres, Outline Postgres, Outline Redis.

Backups: manual via scripts/backup.sh and scripts/restore.sh. Tested end-to-end.


Ground Rules You Must Follow

  1. One step at a time. Complete one logical unit of work, then stop and check in.
  2. Research before writing. Verify image versions, OS compatibility, and config options against current documentation. Do not guess at configuration.
  3. Everything through Ansible. No manual SSH fixes, no ad-hoc commands. If it needs to happen on the server, it goes in a playbook or role.
  4. Validate after each step. After applying anything, run a verification and report the result.
  5. Surface errors immediately. If something fails, stop, show the error, explain, and offer a fix.
  6. Keep it simple. Prefer the least complex solution. We moved away from Docker Swarm + Traefik to Docker Compose + Caddy for exactly this reason.
  7. State is precious. Before any destructive action, warn the operator and suggest a backup first.
  8. Comment everything. Every Ansible task and every non-obvious config block gets a comment.

Repository Layout

ce-infra/
├── CLAUDE.md                          ← this file
├── ansible.cfg                        ← sets roles_path and default inventory
├── credentials.md                     ← all service credentials (DO NOT commit to public repo)
├── docs/                              ← infrastructure documentation
│   ├── overview.md                    ← architecture and service overview
│   ├── runbook.md                     ← operational procedures
│   ├── backup-restore.md             ← backup and restore procedures
│   ├── gitea-actions-guide.md        ← CI/CD usage guide
│   ├── getting-started.md            ← non-technical team onboarding
│   └── future-plans.md               ← planned work and improvements
├── inventory/
│   ├── hosts.yml                      ← VPS connection details
│   └── group_vars/
│       └── all.yml                    ← all variables (domain, secrets, versions)
├── roles/
│   ├── common/                        ← OS hardening, packages, UFW, cloud-init disable
│   ├── docker/                        ← Docker Engine install
│   └── services/                      ← Docker Compose stack (all services)
│       ├── tasks/main.yml
│       └── templates/
│           ├── docker-compose.yml.j2  ← all containers
│           ├── Caddyfile.j2           ← reverse proxy config
│           └── dex-config.yml.j2      ← Dex OIDC config
├── playbooks/
│   ├── bootstrap.yml                  ← runs common + docker (idempotent, safe to re-run)
│   └── deploy.yml                     ← deploys/updates all services
└── scripts/
    ├── backup.sh                      ← full backup to local tarball
    └── restore.sh                     ← restore from backup onto fresh deploy

Key Technical Decisions

Decision Rationale
Docker Compose over Swarm Single node — Swarm adds overlay network complexity for zero benefit
Caddy over Traefik Automatic HTTPS with zero config, simple Caddyfile, no label machinery
LLDAP + Dex over Keycloak/Authentik Lightweight identity stack, minimal resource usage on 2GB RAM
Local file storage for Outline No S3 dependency, simpler backup story
server_hostname variable name hostname collides with Ansible built-in
Cloud-init disabled on first run Prevents SSH key regeneration on hostname change (DigitalOcean)
group_vars/ inside inventory/ Ansible auto-discovers it there without extra config

Authentication Flow

User → Outline / Gitea → Dex (OIDC) → LLDAP (user directory)
  • Manage users in LLDAP web UI (ldap.cloud-elves.eu)
  • All services authenticate via Dex OIDC
  • Gitea has auto-registration enabled — first SSO login creates the account
  • Outline creates accounts on first SSO login automatically

DNS Records

All subdomains are CNAMEs pointing to box.cloud-elves.eu:

Subdomain Service
box.cloud-elves.eu A record → 188.166.60.12
cloud-elves.eu A record → 188.166.60.12
www Static site
git Gitea
auth Dex OIDC
ldap LLDAP web UI
docs Outline wiki

Deployment

From a fresh droplet (Debian 13, SSH key injected):

# 1. Accept new host keys
ssh-keygen -R box.cloud-elves.eu
ssh-keyscan box.cloud-elves.eu >> ~/.ssh/known_hosts

# 2. Bootstrap (OS hardening, Docker install)
ansible-playbook -i inventory/hosts.yml playbooks/bootstrap.yml

# 3. Deploy all services
ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml

To update services after config changes:

ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml

Quick Reference Commands

# Check Ansible connectivity
ansible -i inventory/hosts.yml all -m ping

# Check running containers on box
ansible -i inventory/hosts.yml all -m ansible.builtin.shell \
  -a "cd /opt/services && docker compose ps"

# View logs for a specific service
ansible -i inventory/hosts.yml all -m ansible.builtin.shell \
  -a "cd /opt/services && docker compose logs <service> --tail 50"

# Deploy static site
rsync -avz --delete site/ root@box.cloud-elves.eu:/opt/www/

# Restart a single service
ansible -i inventory/hosts.yml all -m ansible.builtin.shell \
  -a "cd /opt/services && docker compose restart <service>"

Future Work

  • Automated remote backups — Restic to object storage, systemd timer, retention policy
  • Security review — firewall audit, container hardening, secret management (move secrets out of group_vars)
  • Migration plan — move to permanent VPS once company incorporation is complete
  • SMTP — ProtonMail for cloud-elves.eu after incorporation

See docs/future-plans.md for the full prioritized list.