186 lines
6.9 KiB
Markdown
186 lines
6.9 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
|
|
├── 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 <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
|
|
|
|
- **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)
|