restructure docs: eliminate duplication across md files
This commit is contained in:
+129
-100
@@ -1,99 +1,38 @@
|
||||
# Architecture
|
||||
|
||||
Technical details for the hantim infrastructure. See README.md for usage
|
||||
instructions, USECASES.md for expected behaviors.
|
||||
|
||||
## Servers
|
||||
|
||||
### Hantim server (hantim)
|
||||
### Hantim (Vultr, Rocky Linux 9)
|
||||
|
||||
The production application server. Runs Rocky Linux 9 on Vultr.
|
||||
Production web server. This repo is cloned to `/opt/hantim`.
|
||||
|
||||
- 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
|
||||
- Runs nginx reverse proxy + all app containers via Docker Compose
|
||||
- Certbot runs on the host (not in Docker) and manages Let's Encrypt certs
|
||||
- The `deploy` user receives SSH commands from CI to trigger deploys
|
||||
- Garage storage node for media replication
|
||||
|
||||
### Gitea server (git.timothykim.net)
|
||||
### Argento (home server)
|
||||
|
||||
A separate server running the Gitea instance. Provides:
|
||||
|
||||
- Git hosting for all repos
|
||||
- Gitea instance at `git.timothykim.net`
|
||||
- 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
|
||||
- Gitea Actions CI runner (registered at site level)
|
||||
- Second Garage storage node (replication partner)
|
||||
- Garage web UI for administration
|
||||
|
||||
### 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
|
||||
All secrets stored in project `hantim`, fetched via `bws` CLI.
|
||||
|
||||
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
|
||||
- `hantim-server` — used by `bootstrap.sh` and `configure.sh` (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.
|
||||
Gitea org-level secrets (`hantim`) duplicate some bws secrets for direct
|
||||
CI use without installing `bws`: `DEPLOY_SSH_KEY`, `CI_REGISTRY_TOKEN`,
|
||||
`DEPLOY_HOST` (variable).
|
||||
|
||||
## Network topology
|
||||
|
||||
@@ -103,16 +42,16 @@ app source code and a `build.yml` workflow.
|
||||
[Hantim Server]
|
||||
|
|
||||
nginx (port 80/443)
|
||||
| |
|
||||
example_com example_org ...
|
||||
(shared Docker network)
|
||||
/ | | \
|
||||
example_com app_two garage ...
|
||||
(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
|
||||
- nginx proxies `https://www.<domain>` to the app container by name
|
||||
- The `shared` network is created by the nginx compose file and referenced
|
||||
as `external: true` by app compose files
|
||||
as `external: true` by all other compose files
|
||||
|
||||
## Deploy flow
|
||||
|
||||
@@ -120,21 +59,20 @@ app source code and a `build.yml` workflow.
|
||||
|
||||
```
|
||||
Developer pushes to app repo (e.g., example.com)
|
||||
-> build.yml workflow runs on Gitea runner
|
||||
-> docker build + push to git.timothykim.net registry
|
||||
-> build.yml runs on Gitea runner
|
||||
-> docker build + push to 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>/)
|
||||
-> deploy-<app>.yml triggers (path match on 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
|
||||
-> nginx: auto-issues certs for new domains, tests config with nginx -t
|
||||
```
|
||||
|
||||
### Setup change (push to hantim-server)
|
||||
@@ -143,18 +81,109 @@ Developer pushes 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)
|
||||
-> deploy.sh: git pull, runs configure.sh
|
||||
```
|
||||
|
||||
## Conventions
|
||||
## SSH deploy protocol
|
||||
|
||||
The `deploy` user's `authorized_keys` uses `command=` restrictions. The SSH
|
||||
client sends a command string (e.g., `deploy-example.com`), and `deploy.sh`
|
||||
parses it to determine the action:
|
||||
|
||||
- `deploy-<app>` — deploy a specific app
|
||||
- `deploy-nginx` — deploy nginx (with cert handling)
|
||||
- `cert-<domain>` — issue a cert only
|
||||
- `provision` — run `configure.sh`
|
||||
|
||||
## SSL certificate handling
|
||||
|
||||
Cert names are derived from nginx conf filenames (e.g., `example.com.conf` →
|
||||
cert `example.com`). All `server_name` values in the conf are included in the
|
||||
cert as SANs.
|
||||
|
||||
**New domain (nginx not yet running):**
|
||||
1. `deploy.sh` detects missing cert files
|
||||
2. Stops nginx temporarily
|
||||
3. Issues cert via `certbot --standalone`
|
||||
4. Starts nginx
|
||||
5. On error, cleans up the temp conf file (trap)
|
||||
|
||||
**New domain (nginx already running):**
|
||||
1. Issues cert via `certbot --webroot` using the ACME challenge directory
|
||||
2. Zero downtime
|
||||
|
||||
**Renewal:** certbot timer/cron runs daily, deploy hook reloads nginx.
|
||||
|
||||
## `.env.keys` mechanism
|
||||
|
||||
Services needing secrets declare them in `.env.keys`:
|
||||
|
||||
```
|
||||
ENV_VAR=bws-secret-name
|
||||
```
|
||||
|
||||
During deploy, `deploy.sh`:
|
||||
1. Reads each line from `.env.keys`
|
||||
2. Fetches the secret value from bws: `bws secret get <uuid> --access-token ...`
|
||||
3. Writes `ENV_VAR=value` to `.env`
|
||||
4. Docker Compose reads `.env` on startup
|
||||
|
||||
If any secret is missing, deploy fails with an error (not a warning).
|
||||
|
||||
## Naming 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.
|
||||
- **Container name** = domain with dots → underscores (e.g., `example_com`)
|
||||
- **Image name** = `git.timothykim.net/hantim/<domain>:latest`
|
||||
- All HTTP traffic redirects to `https://www.<domain>`
|
||||
- Bare domain HTTPS redirects to `https://www.<domain>`
|
||||
- **Cert name** = nginx conf filename without `.conf`
|
||||
- **One domain per conf file** — `deploy.sh` derives the cert name from the
|
||||
filename and collects all `server_name` values for that cert
|
||||
|
||||
## URL routing
|
||||
|
||||
- All HTTP → HTTPS redirect
|
||||
- Bare domain HTTPS → `https://www.<domain>` redirect
|
||||
- Each domain gets its own conf file (even redirect-only domains)
|
||||
|
||||
## Garage (S3-compatible storage)
|
||||
|
||||
Two-node cluster: hantim + argento, `replication_factor = 2`.
|
||||
|
||||
**Components:**
|
||||
- Garage v2.2.0 (`dxflrs/garage:v2.2.0`)
|
||||
- Port 3900: S3 API (proxied via `s3.hantim.net`)
|
||||
- Port 3901: RPC (inter-node, firewall opened)
|
||||
- Port 3902: Web endpoint (serves files via `Host: <bucket>.web.garage`)
|
||||
- Port 3903: Admin API (proxied via `garage.hantim.net`)
|
||||
|
||||
**Media serving path:**
|
||||
```
|
||||
Client -> nginx (443) -> /media/ location -> garage:3902
|
||||
(proxy_set_header Host <domain>.web.garage)
|
||||
```
|
||||
|
||||
**Config generation:** `garage.toml` uses `envsubst` placeholders (`${RPC_SECRET}`,
|
||||
`${ARGENTO_NODE_ID}`, `${ADMIN_TOKEN}`). The Dockerfile copies the template and
|
||||
runs envsubst at container start. Values come from `.env` generated by the
|
||||
`.env.keys` mechanism.
|
||||
|
||||
**Bucket setup** (done by `new-app.sh` via admin API v2):
|
||||
1. `POST /v2/CreateBucket` with `globalAlias: <domain>`
|
||||
2. `POST /v2/AllowBucketKey` to grant media-key read/write access
|
||||
3. `POST /v2/UpdateBucket` to enable website access
|
||||
|
||||
## App repos
|
||||
|
||||
Every site repo follows the same pattern (created from `static-site-template`):
|
||||
|
||||
```
|
||||
Dockerfile # Copies static/ into nginx:alpine, applies nginx.conf
|
||||
nginx.conf # Per-app HTTP config (port 80, routing, cache headers)
|
||||
static/ # HTML/CSS/JS content
|
||||
static/media/ # Local media files (gitignored)
|
||||
.gitea/workflows/build.yml # Build image, push to registry, SSH deploy
|
||||
```
|
||||
|
||||
The template uses Gitea template variables (`${REPO_NAME}`) so the image tag
|
||||
and deploy command match the repo name automatically. Workflow files must
|
||||
escape `$$` to prevent template variable consumption.
|
||||
|
||||
Reference in New Issue
Block a user