7.3 KiB
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
bwsCLI
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 + 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)
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
<domain>/ # Per-app compose files (e.g. example.com)
compose.yml
garage/ # Garage S3-compatible object storage
compose.yml
Dockerfile
garage.toml
.env.keys
.gitea/workflows/ # Per-app deploy workflows + provision.yml
Provisioning a new server
On a fresh Rocky Linux 9 install:
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-servermachine account
Restoring from backup is identical — run the same three commands on a fresh server. Everything is idempotent.
Adding a new static site
./tools/new-app.sh <domain>
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:
git clone git@git.timothykim.net:hantim/<domain>.git
cd <domain>
# 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.ymlbuilds 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.ymltriggers and re-runsconfigure.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 managementhttps://www.<domain>/media/<path>— public file access
Uploading:
# 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):
# 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):
docker exec garage /garage status
docker exec garage /garage layout assign <hantim-node-id> -z hantim -c 1G
docker exec garage /garage layout assign <argento-node-id> -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.
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 .env.keys (format:
ENV_VAR=bws-secret-name, one per line). deploy.sh fetches each secret
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_SSH_KEY |
Secret | SSH private key for the deploy user |
CI_REGISTRY_TOKEN |
Secret | Gitea token for Docker registry login |