3.9 KiB
3.9 KiB
CLAUDE.md
Quick context for Claude Code. See README.md for full details, ARCHITECTURE.md for technical deep-dives, USECASES.md for expected behaviors.
Key files
scripts/bootstrap.sh-- First-time server setup (manual, uses Bitwarden)scripts/configure.sh-- Idempotent server config (firewall, certbot, start services; CI-safe)scripts/deploy.sh-- SSH-triggered deploy + cert issuance + provision (deploy-, cert-, provision commands)tools/app.sh-- App provisioning by subcommand (dns, repo, files, cert, garage, build, verify, monitor, all)tools/service.sh-- Manage Docker services by subcommand (dns, files, nginx, cert, all)tools/remove-app.sh-- Remove a static site (local files + Gitea repo)docker/nginx/conf.d/-- Per-app nginx server blocksdocker/<domain>/compose.yml-- Per-app compose filesdocker/goatcounter/-- GoatCounter analytics (compose.yaml, .env.keys)docker/garage/-- Garage S3 object storage (Dockerfile, garage.toml, .env.keys).gitea/workflows/-- Per-app deploy workflows + provision.yml
Conventions
- App name = domain (e.g.,
example.com) - Container name = dots → underscores (e.g.,
example_com) - One domain per conf file -- deploy.sh derives cert name from filename
- All HTTP → HTTPS; bare domain →
www.<domain> - All shell scripts use
set -euo pipefailand must be idempotent - The
sharedDocker network is created by nginx compose and used by all apps - Secrets are in Bitwarden Secrets Manager (see README.md for full list)
- Service secrets use
.env.keysfiles (see README.md)
Deploy flow
- App repo push →
build.yml→ build/push image → SSHdeploy-<domain>→ deploy.sh - This repo push →
deploy-<app>.yml(path match) → SSHdeploy-<app>→ deploy.sh - Setup change →
provision.yml→ SSHprovision→ configure.sh
Build and test
No build step. Run e2e tests with:
uvx pytest tests/test_app_e2e.py -v -x
Prerequisites
uvinstalled (curl -LsSf https://astral.sh/uv/install.sh | sh)- CLI tools:
bws,jq,dig,ssh,curl,git - bws token: set
BWS_ACCESS_TOKENenv var or save to~/.config/hantim/bws-token - Vultr API key must allow requests from the machine's IP (check https://my.vultr.com/settings/#settingsapi)
Test structure
tests/conftest.py-- Fixtures: preflight checks, bws secrets, API helper, cleanuptests/test_app_e2e.py-- E2E tests fortools/app.shagainst real servicestests/test_security_headers.py-- Verify security headers on live sitestests/test_default_server.py-- Verify unknown Host headers are droppedtests/test_tls.py-- Verify TLS hardening
E2E tests (test_app_e2e.py)
Tests use kgfamily.com as a throwaway test domain. Two phases:
- TestAppSubcommands -- Runs each
app.shsubcommand individually (dns, repo, files, cert, garage, build, monitor), verifies side effects via API, then cleans up. Does not testverify(needs full deploy flow). - TestAppAll -- Runs
app.sh all kgfamily.com, verifies everything including site liveness and git commit, then cleans up (including reverting the commit+push).
Cleanup is done directly via APIs (Vultr, Gitea, Garage, UptimeRobot) + local file
deletion. Does NOT use remove-app.sh to avoid coupling. Runs before and after each
phase to handle leftovers from crashed runs.
Known issue
- Vultr API keys can be IP-restricted. If
test_dnsfails with HTTP 401, add the machine's IP to the Vultr API access control list.
Other tests
The security/TLS/default-server tests run against live sites and are triggered automatically after nginx deploy. They can also be run locally:
uvx pytest tests/ -v --ignore=tests/test_app_e2e.py
Commit style
- Short, lowercase commit messages (e.g., "add example.com", "fix deploy command")
- Do NOT include "Co-Authored-By" lines
- Commit only when explicitly asked