Files
hantim-server/USECASES.md
T
2026-03-20 11:28:36 -04:00

6.2 KiB

Use Cases

0. Initial Bitwarden Secrets Manager setup

One-time setup before anything else works. Create the secrets project and machine accounts in Bitwarden Secrets Manager.

  1. Create a Bitwarden organization (free tier)
  2. Enable Secrets Manager for the organization
  3. Create a project named hantim
  4. Create all required secrets (see README.md Secrets table for the full list)
  5. Create machine accounts:
    • hantim-server — grant access to the hantim project
    • hantim-ci — reserved for future CI use
  6. Generate access tokens for each machine account

1. Provision a new server

Set up a fresh Rocky Linux 9 server from scratch.

dnf install -y git
git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim
bash /opt/hantim/scripts/bootstrap.sh

What happens:

  • Installs all dependencies (jq, certbot, unzip, Docker)
  • Installs bws CLI, prompts for Bitwarden Secrets Manager access token
  • Fetches registry token and deploy SSH public key via bws
  • Logs into Docker registry
  • Creates deploy user with restricted SSH + sudo
  • Runs configure.sh:
    • Opens firewall ports (HTTP, HTTPS, Garage RPC)
    • Sets up certbot renewal
    • Deploys all services via deploy.sh (generates .env files, issues certs, starts containers)

Prerequisites:

  • DNS for all configured domains must point to the server
  • Bitwarden Secrets Manager access token for the hantim-server machine account
  • Docker images must exist in the Gitea registry (or apps will start on next push)

2. Restore from backup

Identical to provisioning a new server. Run the same three commands on a fresh server.

  • bootstrap.sh and configure.sh are idempotent -- safe to re-run
  • SSL certs are re-issued automatically (Let's Encrypt)
  • Docker images are pulled from the Gitea registry
  • No data migration needed for stateless static sites

3. Add a new static site

From your dev machine (single command):

./tools/new-app.sh example.com

Prerequisites:

  • Bitwarden Secrets Manager must contain hantim-new-app-script, hantim-vultr-api-key, and hantim-deploy-ssh-private-key
  • Domain nameservers must be pointed to Vultr (configured on directnic.com)
  • Dependencies: bws, jq, dig
  • BWS_ACCESS_TOKEN env var or token saved at ~/.config/hantim/bws-token

What happens (fully automated):

  1. Resolves server IP from hantim.net
  2. Fetches Gitea API token, Vultr API key, and deploy SSH key from Bitwarden Secrets Manager
  3. Creates DNS zone on Vultr (if needed) and A records for bare + www
  4. Waits for DNS to propagate
  5. Creates Gitea repo hantim/example.com from static-site-template
  6. Creates local files:
    • docker/example.com/compose.yml (container name: example_com)
    • .gitea/workflows/deploy-example.com.yml
    • docker/nginx/conf.d/example.com.conf (HTTP->HTTPS, bare->www, proxy)
  7. SSHes to server as deploy user, issues SSL cert via webroot (zero downtime)
  8. Creates Garage media bucket via admin API (create, allow media-key, enable website)
  9. Commits and pushes hantim-server (triggers deploy)
  10. Triggers initial build of the app repo via Gitea API
  11. Polls https://www.example.com until it responds (up to 3 minutes)

After the site is live, clone the app repo and customize:

git clone git@git.timothykim.net:hantim/example.com.git
cd example.com
# edit static/index.html, etc.
git add . && git commit -m "initial content" && git push

4. Update app code

Push a change to the app repo (e.g., example.com):

cd example.com
# make changes
git add . && git commit -m "update content" && git push

What happens:

  1. Gitea Actions runs build.yml: builds Docker image, pushes to registry
  2. SSHes into server, runs deploy-example.com
  3. deploy.sh pulls latest hantim-server, pulls new image, restarts container

No manual steps needed.

5. Update Docker Compose config

Edit a compose file in this repo and push:

vim docker/example.com/compose.yml
git add docker/example.com/compose.yml
git commit -m "update example.com config"
git push

What happens:

  1. deploy-example.com.yml workflow triggers (path match)
  2. deploy.sh pulls latest code, pulls image, runs docker compose up -d

6. Update nginx config

Edit an nginx config in this repo and push:

vim docker/nginx/conf.d/example.com.conf
git add docker/nginx/conf.d/example.com.conf
git commit -m "update nginx config"
git push

What happens:

  1. deploy-nginx.yml workflow triggers (path match)
  2. deploy.sh pulls latest code
  3. If any conf files reference certs that don't exist yet, nginx is briefly stopped and certs are issued via standalone certbot (auto-restarts on failure)
  4. Tests nginx config with nginx -t -- if it fails, deploy aborts and the running nginx is untouched
  5. Runs docker compose up -d and reloads nginx

Note: Each conf file must contain server blocks for only one domain. The cert name is derived from the filename (e.g. example.com.conf -> cert example.com). Redirect-only domains get their own conf file.

7. Re-run configure on existing server

Safe to do at any time, manually or via CI:

cd /opt/hantim
git pull
bash scripts/configure.sh

Or push a change to scripts/configure.sh — the provision.yml workflow triggers automatically.

  • All steps are idempotent
  • Existing certs are skipped
  • Brief nginx downtime (seconds) while certbot checks run
  • Services are restarted

8. Upload media files

Media files (images, videos) are stored in Garage and served at /media/ on each domain.

# One-time 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 a single file
aws s3 cp photo.jpg s3://example.com/photo.jpg

# Sync a directory
aws s3 sync static/media/ s3://example.com/

What happens:

  1. File is uploaded to the Garage S3 API via s3.hantim.net
  2. Garage replicates the file to both nodes (hantim + argento)
  3. File is accessible at https://www.example.com/media/photo.jpg

Local development: Store media in static/media/ (gitignored). The app's nginx serves these locally, matching the /media/ path used in production.

Reference in HTML: Use absolute paths like /media/photo.jpg.