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.shin this repo - This repo is cloned to
/opt/hantim - The
deployuser 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 accesshantim-deploy-ssh-private-key-- deploy user SSH private keyhantim-deploy-ssh-public-key-- deploy user SSH public keyhantim-new-app-script-- Gitea API token for creating reposhantim-vultr-api-key-- Vultr API key for DNS managementhantim-garage-rpc-secret-- Garage cluster RPC secrethantim-garage-argento-node-id-- Argento's Garage node ID + addresshantim-garage-admin-token-- Garage admin API tokenhantim-garage-media-key-id-- S3 access key ID for media uploadshantim-garage-media-secret-key-- S3 secret key for media uploads
Machine accounts:
hantim-server-- used bybootstrap.shandconfigure.shon 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 keyCI_REGISTRY_TOKEN-- Gitea registry tokenDEPLOY_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
sharedDocker network - nginx proxies
https://www.<domain>to the app's container by name - The
sharednetwork is created by the nginx compose file and referenced asexternal: trueby 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.shderives the cert name from the conf filename and collects allserver_namevalues 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>