# 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. ## Repo structure ``` 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 static site (e.g. example.com) compose.yml garage/ # Garage S3-compatible object storage compose.yml Dockerfile garage.toml .env.keys scripts/ bootstrap.sh # First-time server setup (manual, uses Bitwarden) configure.sh # Idempotent server config (CI-safe) deploy.sh # Deploy + cert issuance + provision (called via SSH) tools/ new-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) .gitea/workflows/ # Per-app deploy workflows triggered by path changes ``` ## 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: 1. Installs system dependencies (jq, certbot, unzip, Docker) 2. Installs the `bws` CLI (Bitwarden Secrets Manager) 3. Prompts for a Bitwarden Secrets Manager access token (saved to `/etc/bws-token`) 4. Fetches the Docker registry token and deploy SSH public key via `bws` 5. Logs into the Gitea Docker registry 6. Creates the `deploy` user with restricted SSH access and sudo 7. Runs `configure.sh` (firewall, certbot, start services) `configure.sh` is idempotent and CI-safe. It: 1. Opens firewall ports (HTTP, HTTPS, Garage RPC) 2. Sets up certbot renewal hooks and timer 3. Deploys all services via `deploy.sh` (nginx first, then all apps) - Generates `.env` files from bws for services with `.env.keys` - Issues SSL certs for new domains (nginx only) - Builds or pulls images, starts containers **Prerequisites**: - DNS for all configured domains must point to the server - Bitwarden Secrets Manager access token for the `hantim-server` machine account ## Adding a new app ```bash ./tools/new-app.sh ``` Example: `./tools/new-app.sh example.com` This single command handles everything: creates DNS records on Vultr, creates the Gitea repo from `static-site-template`, generates all config files, issues the SSL cert (zero downtime), commits and pushes, triggers the first build, and verifies the site is live. **Prerequisites:** domain nameservers pointed to Vultr, `bws`/`jq`/`dig` installed. Uses the deploy SSH key from Bitwarden Secrets Manager -- no personal server access needed. See USECASES.md for full details. `new-app.sh` also creates a Garage media bucket for the domain, allowing media files to be served at `https://www./media/`. After the site is live, clone the app repo and customize it: ```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 like `example.com`): - The app's `build.yml` builds the Docker image, pushes to the registry, and SSHes into the server to trigger a deploy **Compose or nginx config change** (push to this repo): - Gitea Actions workflows trigger based on which `docker//` paths changed - `deploy.sh` pulls the latest code and runs `docker compose up -d` - For nginx deploys, the config is tested before applying to prevent downtime ## Garage cluster initialization After provisioning a fresh server, Garage starts and connects to argento automatically via `bootstrap_peers`. However, the cluster layout must be assigned manually (node IDs change on fresh installs): ```bash # Check both nodes are connected docker exec garage /garage status # Assign roles (use the node IDs from status output) 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 ``` This only needs to be done once. If only hantim is reprovisioned, argento retains the layout and hantim reconnects automatically. ## Media hosting Media files (images, videos) 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 with `media-key`) - `garage.hantim.net` — Admin API for bucket management - `https://www./media/` — public file access **Uploading media:** ```bash # Configure AWS CLI (one-time) aws configure # key ID, secret key from bws, region: garage aws configure set default.endpoint_url https://s3.hantim.net # Upload files 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" ``` ## Restoring from backup Run the same three commands as provisioning. `bootstrap.sh` is idempotent: - Existing packages are skipped - Existing certs are skipped (or re-issued if the server is new) - All services are started ## 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 | `new-app.sh` | | `hantim-new-app-script` | Gitea API token for creating repos | `new-app.sh` | | `hantim-vultr-api-key` | Vultr API key for DNS management | `new-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`, `new-app.sh` | | `hantim-garage-media-key-id` | S3 access key ID for media uploads | `new-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 a `.env.keys` file (format: `ENV_VAR=bws-secret-name`, one per line). `deploy.sh` reads this file, fetches each secret from bws, and generates a `.env` file before starting the service. See `docker/garage/.env.keys` for an example. Gitea org-level (`hantim`) secrets/variables: | Name | Type | Purpose | |---|---|---| | `DEPLOY_HOST` | Variable | Server IP or hostname | | `DEPLOY_SSH_KEY` | Secret | SSH private key for the deploy user | | `CI_REGISTRY_TOKEN` | Secret | Gitea token for Docker registry login |