Files
hantim-server/ARCHITECTURE.md
T
timothykim b51fe596c6 automate new-app.sh: DNS, cert, commit, build, verify
- resolve server IP from hantim.net instead of env var
- create Vultr DNS zone and A records via API
- wait for DNS propagation before cert issuance
- issue SSL cert via webroot (zero downtime)
- auto commit and push hantim-server
- trigger initial app build via Gitea API
- verify site is live with curl check
- update all docs to reflect single-command flow
2026-03-16 01:57:10 -04:00

4.2 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 setup.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 (cloud)

Bitwarden free tier is used as the secret store. Stores:

  • hantim-git-crypt-key -- git-crypt symmetric key (Secure Note, base64)
  • hantim-ci-registry-push -- Gitea API token for registry access (Secure Note)
  • hantim-server-deploy -- deploy user SSH key pair (SSH Key)
  • hantim-new-app-script -- Gitea API token for creating repos (Secure Note)
  • hantim-vultr-api-key -- Vultr API key for DNS management (Secure Note)

The Bitwarden CLI (bw) is used in setup.sh (on the server) and new-app.sh (on dev machines) to fetch secrets at runtime.

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.

setup.sh                          # Server provisioning (idempotent)
scripts/deploy.sh                 # Receives deploy commands via SSH
scripts/new-app.sh                # Scaffolds + deploys a new app (single command)
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
.gitea/workflows/deploy-<app>.yml # Per-app deploy workflows

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., timothykim.net)

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)
              |          |
    timothykim_net    hcsuzuki_net    ...
       (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., timothykim.net)
  -> 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): test config with nginx -t before applying

Conventions

  • App name = domain (e.g., hcsuzuki.net)
  • Container name = domain with dots replaced by underscores (e.g., hcsuzuki_net)
  • Cert name = domain (stored at /etc/letsencrypt/live/<domain>/)
  • Image name = git.timothykim.net/hantim/<domain>:latest
  • All HTTP traffic redirects to https://www.<domain>
  • Bare domain HTTPS redirects to https://www.<domain>