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

6.0 KiB

Architecture

Servers

Hantim server (hantim)

The production application server. Runs Rocky Linux 9 on Vultr.

  • Hosts all app containers and the nginx reverse proxy via Docker Compose
  • Provisioned by bootstrap.sh + configure.sh in this repo
  • This repo is cloned to /opt/hantim
  • The deploy user receives SSH commands from CI to trigger deploys
  • Certbot runs on the host (not in Docker) and manages Let's Encrypt certs

Gitea server (git.timothykim.net)

A separate server running the Gitea instance. Provides:

  • Git hosting for all repos
  • Docker container registry (git.timothykim.net/hantim/<app>)
  • Gitea Actions CI/CD (runner registered at site level)
  • Organization-level secrets and variables shared across repos

Bitwarden Secrets Manager (cloud)

Secrets are stored in Bitwarden Secrets Manager (free tier, project: hantim). The bws CLI fetches secrets using machine account access tokens.

  • hantim-ci-registry-push -- Gitea API token for registry access
  • hantim-deploy-ssh-private-key -- deploy user SSH private key
  • hantim-deploy-ssh-public-key -- deploy user SSH public key
  • hantim-new-app-script -- Gitea API token for creating repos
  • hantim-vultr-api-key -- Vultr API key for DNS management
  • hantim-garage-rpc-secret -- Garage cluster RPC secret
  • hantim-garage-argento-node-id -- Argento's Garage node ID + address
  • hantim-garage-admin-token -- Garage admin API token
  • hantim-garage-media-key-id -- S3 access key ID for media uploads
  • hantim-garage-media-secret-key -- S3 secret key for media uploads

Machine accounts:

  • hantim-server -- used by bootstrap.sh and configure.sh on the server (access token at /etc/bws-token)
  • hantim-ci -- reserved for future CI use

Gitea secrets

Workflow secrets stored in the hantim Gitea org. These duplicate some Bitwarden secrets for direct use in CI workflows without installing bws:

  • DEPLOY_SSH_KEY -- deploy SSH private key
  • CI_REGISTRY_TOKEN -- Gitea registry token
  • DEPLOY_HOST -- server IP (variable, not secret)

Repos

All repos live under the hantim organization on Gitea.

hantim-server

This repo. Contains server provisioning, Docker Compose configs, nginx configs, deploy scripts, and Gitea Actions workflows.

scripts/bootstrap.sh              # First-time server setup (manual, uses Bitwarden)
scripts/configure.sh              # Idempotent server config (CI-safe)
scripts/deploy.sh                 # Receives deploy + cert + provision commands via SSH
tools/new-app.sh                  # Scaffolds + deploys a new static site (run from dev machine)
tools/new-service.sh              # Scaffolds a new Docker Compose service (run from dev machine)
tools/remove-app.sh               # Removes a static site (run from dev machine)
docker/nginx/                     # nginx reverse proxy
  compose.yml                     # nginx:alpine container
  nginx.conf                      # Main nginx config
  conf.d/<domain>.conf            # Per-app server blocks
docker/<domain>/compose.yml       # Per-app compose files
docker/garage/                    # Garage S3-compatible object storage
  compose.yml
  Dockerfile
  garage.toml
  .env.keys
.gitea/workflows/deploy-<app>.yml # Per-app deploy workflows
.gitea/workflows/provision.yml    # Triggers configure.sh on setup changes

static-site-template

A Gitea template repo used by new-app.sh to scaffold new static sites. Contains a Dockerfile, nginx config, placeholder HTML, and a build.yml workflow that builds/pushes a Docker image and triggers a deploy.

Uses Gitea template variables (${REPO_NAME}) so the image tag and deploy command are automatically set to the repo name.

App repos (e.g., example.com)

Each app is its own repo created from static-site-template. Contains the app source code and a build.yml workflow.

Network topology

                  Internet
                     |
              [Hantim Server]
                     |
            nginx (port 80/443)
              |          |
    example_com    example_org    ...
       (shared Docker network)
  • nginx is the only container with host port bindings (80, 443)
  • All app containers are on the shared Docker network
  • nginx proxies https://www.<domain> to the app's container by name
  • The shared network is created by the nginx compose file and referenced as external: true by app compose files

Deploy flow

App code change (push to app repo)

Developer pushes to app repo (e.g., example.com)
  -> build.yml workflow runs on Gitea runner
    -> docker build + push to git.timothykim.net registry
    -> SSH to deploy@hantim as deploy-<domain>
      -> deploy.sh: git pull, docker compose pull, docker compose up -d
      -> (if image not yet in registry, skips gracefully)

Infrastructure change (push to hantim-server)

Developer pushes to hantim-server
  -> deploy-<app>.yml triggers (based on changed paths under docker/<app>/)
    -> SSH to deploy@hantim as deploy-<app>
      -> deploy.sh: git pull, docker compose pull, docker compose up -d
      -> (nginx only): auto-issues certs for new domains, then tests config with nginx -t

Setup change (push to hantim-server)

Developer pushes change to scripts/configure.sh
  -> provision.yml triggers
    -> SSH to deploy@hantim as provision
      -> deploy.sh: git pull, configure.sh (firewall, certbot, start services)

Conventions

  • App name = domain (e.g., example.com)
  • Container name = domain with dots replaced by underscores (e.g., example_com)
  • Cert name = config filename without .conf (stored at /etc/letsencrypt/live/<name>/)
  • One domain per conf file -- deploy.sh derives the cert name from the conf filename and collects all server_name values for that cert. Putting unrelated domains in the same conf file will bundle them into one cert incorrectly.
  • Image name = git.timothykim.net/hantim/<domain>:latest
  • All HTTP traffic redirects to https://www.<domain>
  • Bare domain HTTPS redirects to https://www.<domain>