timothykim df179546ae update docs, fix deploy to not restart unchanged services
- deploy.sh: use compose up instead of down+up, fetch secrets individually
- configure.sh: delegate all service deployment to deploy.sh
- docs: add garage cluster init, .env.keys mechanism, update configure.sh description
- remove 2>/dev/null from bws calls
2026-03-19 12:39:55 -04:00
2026-03-19 11:23:53 -04:00

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
  <domain>/            # 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:

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

./tools/new-app.sh <domain>

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.

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

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 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/<app>/ 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):

# 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 <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

This only needs to be done once. If only hantim is reprovisioned, argento retains the layout and hantim reconnects automatically.

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

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
S
Description
No description provided
Readme 500 KiB
Languages
Shell 65.2%
Python 33.5%
Assembly 0.5%
Dockerfile 0.4%
HTML 0.4%