# 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 ├── 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: not yet configured (Restic → object storage planned). --- ## 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 │ └── 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 ``` --- ## 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): ```bash # 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: ```bash ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml ``` --- ## Quick Reference Commands ```bash # 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 --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 " ``` --- ## Future Work - **Backup setup** — Restic to object storage, systemd timer, retention policy - **Security review** — firewall audit, container hardening, secret management (move secrets out of group_vars) - **CI/CD** — automated deployments, documentation pipeline - **Migration plan** — move to permanent VPS once company incorporation is complete - **Portainer** — optional web UI for container management (low priority)