# hantim-server Server provisioning and app management for the hantim webserver. This repo lives at `/opt/hantim` on the server and contains Docker Compose configs, deploy scripts, nginx configs, and Gitea Actions workflows for each app. See ARCHITECTURE.md for technical deep-dives, USECASES.md for expected behaviors. ## Infrastructure overview Two servers and a cloud secrets service: - **Hantim** (Vultr, Rocky Linux 9) — production web server running nginx, app containers, and a Garage storage node via Docker Compose - **Argento** (home server) — Gitea instance (`git.timothykim.net`) with Docker registry, CI runner, and a second Garage storage node (with web UI) - **Bitwarden Secrets Manager** (cloud) — all secrets, fetched via the `bws` CLI All app containers share a `shared` Docker network. Only nginx binds host ports (80/443). Garage provides S3-compatible media storage replicated across both nodes. ## Repo structure ``` scripts/ bootstrap.sh # First-time server setup (manual, uses Bitwarden) configure.sh # Idempotent server config (CI-safe) deploy.sh # Deploy + provision (called via SSH) tools/ app.sh # Add a new static site (run from dev machine) new-service.sh # Add a new Docker service (run from dev machine) remove-app.sh # Remove a static site (run from dev machine) docker/ nginx/ # Reverse proxy (nginx:alpine) + SSL config nginx.conf # Main nginx config conf.d/ # Per-app server blocks (e.g. example.com.conf) compose.yml / # Per-app compose files (e.g. example.com) compose.yml garage/ # Garage S3-compatible object storage compose.yml Dockerfile garage.toml .env.keys tests/ # pytest integration tests (run post-deploy) .gitea/workflows/ # Per-app deploy workflows + provision.yml ``` ## Provisioning a new server On a fresh Rocky Linux 9 install: ```bash dnf install -y git git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim bash /opt/hantim/scripts/bootstrap.sh ``` `bootstrap.sh` runs once manually. It installs dependencies, sets up the `bws` CLI, creates the `deploy` user, and calls `configure.sh`. `configure.sh` is idempotent and CI-safe. It opens firewall ports, sets up certbot renewal, and deploys all services (nginx first, then apps). **Prerequisites:** - DNS for all configured domains must point to the server - Bitwarden Secrets Manager access token for the `hantim-server` machine account Restoring from backup is identical — run the same three commands on a fresh server. Everything is idempotent. ## Adding a new static site ```bash ./tools/app.sh all ``` This single command handles everything: DNS records (Vultr), Gitea repo (from `static-site-template`), nginx/compose/workflow configs, SSL cert, Garage media bucket, first build, and verification. **Prerequisites:** domain nameservers pointed to Vultr, `bws`/`jq`/`dig` installed, `BWS_ACCESS_TOKEN` env var or token at `~/.config/hantim/bws-token`. After the site is live, clone the app repo and customize: ```bash git clone git@git.timothykim.net:hantim/.git cd # edit content git add . && git commit -m "initial content" && git push ``` ## Deploying changes **App code change** — push to an app repo (e.g., `example.com`): - `build.yml` builds the image, pushes to registry, SSHes to server to deploy **Compose/nginx config change** — push to this repo: - Path-matched workflows trigger and SSH to server - nginx deploys test config before applying (`nginx -t`) **Setup change** — push changes to `scripts/configure.sh`: - `provision.yml` triggers and re-runs `configure.sh` ## Media hosting Media files are stored in Garage and served via nginx at `/media/` on each domain. This keeps large files out of git repos. **Endpoints:** - `s3.hantim.net` — S3 API for uploads (authenticated) - `garage.hantim.net` — Admin API for bucket management - `https://www./media/` — public file access **Uploading:** ```bash # One-time AWS CLI setup aws configure # key ID + secret from bws, region: garage, output: json # Then add to ~/.aws/config under [default]: # endpoint_url = https://s3.hantim.net # Upload aws s3 cp photo.jpg s3://example.com/photo.jpg aws s3 sync static/media/ s3://example.com/ ``` **Local development:** store media in `static/media/` (gitignored) so files are accessible at `/media/` when serving locally. **Creating buckets manually** (for existing domains without buckets): ```bash # Get admin token and media key ID from bws # Create bucket curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"globalAlias": "example.com"}' https://garage.hantim.net/v2/CreateBucket # Note the bucket ID from the response, then: curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"bucketId": "BUCKET_ID", "accessKeyId": "MEDIA_KEY_ID", "permissions": {"read": true, "write": true, "owner": false}}' \ https://garage.hantim.net/v2/AllowBucketKey curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"websiteAccess": {"enabled": true, "indexDocument": "index.html"}}' \ "https://garage.hantim.net/v2/UpdateBucket?id=BUCKET_ID" ``` ## Garage cluster initialization After provisioning a fresh server, Garage connects to argento automatically. The cluster layout must be assigned manually once (node IDs change per install): ```bash docker exec garage /garage status docker exec garage /garage layout assign -z hantim -c 1G docker exec garage /garage layout assign -z argento -c 1G docker exec garage /garage layout apply --version 1 ``` If only hantim is reprovisioned, argento retains the layout and hantim reconnects automatically. ## Testing Tests live in `tests/` and run with pytest: ```bash pip install pytest pytest tests/ -v ``` Tests auto-discover sites from `docker/nginx/conf.d/*.conf` — no list to maintain when adding new sites. The `deploy-nginx.yml` workflow runs tests automatically after each nginx deploy. If tests fail, Gitea sends an email notification. ## Secrets ### Bitwarden Secrets Manager Project: `hantim`. Fetched via `bws` CLI on the server. | Secret | Purpose | Used by | |---|---|---| | `hantim-ci-registry-push` | Gitea token for Docker registry | `bootstrap.sh` | | `hantim-deploy-ssh-public-key` | Deploy user's SSH public key | `bootstrap.sh` | | `hantim-deploy-ssh-private-key` | Deploy user's SSH private key | `app.sh` | | `hantim-new-app-script` | Gitea API token for creating repos | `app.sh` | | `hantim-vultr-api-key` | Vultr API key for DNS management | `app.sh` | | `hantim-garage-rpc-secret` | Garage cluster RPC secret | `deploy.sh` | | `hantim-garage-argento-node-id` | Argento's Garage node ID + address | `deploy.sh` | | `hantim-garage-admin-token` | Garage admin API token | `deploy.sh`, `app.sh` | | `hantim-garage-media-key-id` | S3 access key ID for media uploads | `app.sh`, `aws` CLI | | `hantim-garage-media-secret-key` | S3 secret key for media uploads | `aws` CLI | Machine accounts: `hantim-server` (token at `/etc/bws-token`), `hantim-ci` (reserved). ### Service secrets (`.env.keys`) Services that need secrets declare them in `.env.keys` (format: `ENV_VAR=bws-secret-uuid`, one per line, with `# secret-name` comments). `deploy.sh` fetches each secret by UUID from bws and generates `.env` before starting the service. ### Gitea org-level secrets Stored in the `hantim` Gitea org for direct use in CI workflows: | Name | Type | Purpose | |---|---|---| | `DEPLOY_HOST` | Variable | Server IP or hostname | | `DEPLOY_HOST_KEY` | Variable | Server SSH host public key (for host key pinning) | | `DEPLOY_SSH_KEY` | Secret | SSH private key for the deploy user | | `CI_REGISTRY_TOKEN` | Secret | Gitea token for Docker registry login | After reprovisioning hantim (new host key), update `DEPLOY_HOST_KEY`: ```bash ssh-keyscan -t ed25519 hantim.net 2>/dev/null ```