ce-infra/CLAUDE.md

194 lines
7.4 KiB
Markdown

# 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):
```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 <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.