# 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/`) - 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 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/.conf # Per-app server blocks docker//compose.yml # Per-app compose files docker/garage/ # Garage S3-compatible object storage compose.yml garage.toml.template .gitea/workflows/deploy-.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.` 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- -> 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-.yml triggers (based on changed paths under docker//) -> SSH to deploy@hantim as deploy- -> 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//`) - **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/:latest` - All HTTP traffic redirects to `https://www.` - Bare domain HTTPS redirects to `https://www.`