restructure docs: eliminate duplication across md files

This commit is contained in:
2026-03-20 11:42:06 -04:00
parent be1ad6b456
commit eecdf7b002
4 changed files with 286 additions and 408 deletions
+80 -88
View File
@@ -4,21 +4,25 @@ Server provisioning and app management for the hantim webserver. This repo
lives at `/opt/hantim` on the server and contains Docker Compose configs, deploy
scripts, nginx configs, and Gitea Actions workflows for each app.
See ARCHITECTURE.md for technical deep-dives, USECASES.md for expected behaviors.
## Infrastructure overview
Two servers and a cloud secrets service:
- **Hantim** (Vultr, Rocky Linux 9) — production web server running nginx,
app containers, and a Garage storage node via Docker Compose
- **Argento** (home server) — Gitea instance (`git.timothykim.net`) with
Docker registry, CI runner, and a second Garage storage node (with web UI)
- **Bitwarden Secrets Manager** (cloud) — all secrets, fetched via the `bws` CLI
All app containers share a `shared` Docker network. Only nginx binds host ports
(80/443). Garage provides S3-compatible media storage replicated across both
nodes.
## Repo structure
```
docker/
nginx/ # Reverse proxy (nginx:alpine) + SSL config
nginx.conf # Main nginx config
conf.d/ # Per-app server blocks (e.g. example.com.conf)
compose.yml
<domain>/ # Per-app static site (e.g. example.com)
compose.yml
garage/ # Garage S3-compatible object storage
compose.yml
Dockerfile
garage.toml
.env.keys
scripts/
bootstrap.sh # First-time server setup (manual, uses Bitwarden)
configure.sh # Idempotent server config (CI-safe)
@@ -27,7 +31,19 @@ tools/
new-app.sh # Add a new static site (run from dev machine)
new-service.sh # Add a new Docker service (run from dev machine)
remove-app.sh # Remove a static site (run from dev machine)
.gitea/workflows/ # Per-app deploy workflows triggered by path changes
docker/
nginx/ # Reverse proxy (nginx:alpine) + SSL config
nginx.conf # Main nginx config
conf.d/ # Per-app server blocks (e.g. example.com.conf)
compose.yml
<domain>/ # Per-app compose files (e.g. example.com)
compose.yml
garage/ # Garage S3-compatible object storage
compose.yml
Dockerfile
garage.toml
.env.keys
.gitea/workflows/ # Per-app deploy workflows + provision.yml
```
## Provisioning a new server
@@ -40,50 +56,33 @@ git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim
bash /opt/hantim/scripts/bootstrap.sh
```
`bootstrap.sh` runs once manually. It:
`bootstrap.sh` runs once manually. It installs dependencies, sets up the `bws`
CLI, creates the `deploy` user, and calls `configure.sh`.
1. Installs system dependencies (jq, certbot, unzip, Docker)
2. Installs the `bws` CLI (Bitwarden Secrets Manager)
3. Prompts for a Bitwarden Secrets Manager access token (saved to `/etc/bws-token`)
4. Fetches the Docker registry token and deploy SSH public key via `bws`
5. Logs into the Gitea Docker registry
6. Creates the `deploy` user with restricted SSH access and sudo
7. Runs `configure.sh` (firewall, certbot, start services)
`configure.sh` is idempotent and CI-safe. It opens firewall ports, sets up
certbot renewal, and deploys all services (nginx first, then apps).
`configure.sh` is idempotent and CI-safe. It:
1. Opens firewall ports (HTTP, HTTPS, Garage RPC)
2. Sets up certbot renewal hooks and timer
3. Deploys all services via `deploy.sh` (nginx first, then all apps)
- Generates `.env` files from bws for services with `.env.keys`
- Issues SSL certs for new domains (nginx only)
- Builds or pulls images, starts containers
**Prerequisites**:
**Prerequisites:**
- DNS for all configured domains must point to the server
- Bitwarden Secrets Manager access token for the `hantim-server` machine account
## Adding a new app
Restoring from backup is identical — run the same three commands on a fresh
server. Everything is idempotent.
## Adding a new static site
```bash
./tools/new-app.sh <domain>
```
Example: `./tools/new-app.sh example.com`
This single command handles everything: creates DNS records on Vultr, creates
the Gitea repo from `static-site-template`, generates all config files, issues
the SSL cert (zero downtime), commits and pushes, triggers the first build,
and verifies the site is live.
This single command handles everything: DNS records (Vultr), Gitea repo
(from `static-site-template`), nginx/compose/workflow configs, SSL cert,
Garage media bucket, first build, and verification.
**Prerequisites:** domain nameservers pointed to Vultr, `bws`/`jq`/`dig`
installed. Uses the deploy SSH key from Bitwarden Secrets Manager -- no
personal server access needed. See USECASES.md for full details.
installed, `BWS_ACCESS_TOKEN` env var or token at `~/.config/hantim/bws-token`.
`new-app.sh` also creates a Garage media bucket for the domain, allowing
media files to be served at `https://www.<domain>/media/`.
After the site is live, clone the app repo and customize it:
After the site is live, clone the app repo and customize:
```bash
git clone git@git.timothykim.net:hantim/<domain>.git
@@ -94,58 +93,40 @@ git add . && git commit -m "initial content" && git push
## Deploying changes
**App code change** (push to an app repo like `example.com`):
- The app's `build.yml` builds the Docker image, pushes to the registry, and
SSHes into the server to trigger a deploy
**App code change** push to an app repo (e.g., `example.com`):
- `build.yml` builds the image, pushes to registry, SSHes to server to deploy
**Compose or nginx config change** (push to this repo):
- Gitea Actions workflows trigger based on which `docker/<app>/` paths changed
- `deploy.sh` pulls the latest code and runs `docker compose up -d`
- For nginx deploys, the config is tested before applying to prevent downtime
**Compose/nginx config change** push to this repo:
- Path-matched workflows trigger and SSH to server
- nginx deploys test config before applying (`nginx -t`)
## Garage cluster initialization
After provisioning a fresh server, Garage starts and connects to argento
automatically via `bootstrap_peers`. However, the cluster layout must be
assigned manually (node IDs change on fresh installs):
```bash
# Check both nodes are connected
docker exec garage /garage status
# Assign roles (use the node IDs from status output)
docker exec garage /garage layout assign <hantim-node-id> -z hantim -c 1G
docker exec garage /garage layout assign <argento-node-id> -z argento -c 1G
docker exec garage /garage layout apply --version 1
```
This only needs to be done once. If only hantim is reprovisioned, argento
retains the layout and hantim reconnects automatically.
**Setup change** — push changes to `scripts/configure.sh`:
- `provision.yml` triggers and re-runs `configure.sh`
## Media hosting
Media files (images, videos) are stored in Garage and served via nginx at
`/media/` on each domain. This keeps large files out of git repos.
Media files are stored in Garage and served via nginx at `/media/` on each
domain. This keeps large files out of git repos.
**Endpoints:**
- `s3.hantim.net` — S3 API for uploads (authenticated with `media-key`)
- `s3.hantim.net` — S3 API for uploads (authenticated)
- `garage.hantim.net` — Admin API for bucket management
- `https://www.<domain>/media/<path>` — public file access
**Uploading media:**
**Uploading:**
```bash
# Configure AWS CLI (one-time)
aws configure # key ID, secret key from bws, region: garage, output: json
# One-time AWS CLI setup
aws configure # key ID + secret from bws, region: garage, output: json
# Then add to ~/.aws/config under [default]:
# endpoint_url = https://s3.hantim.net
# Upload files
# Upload
aws s3 cp photo.jpg s3://example.com/photo.jpg
aws s3 sync static/media/ s3://example.com/
```
**Local development:** Store media in `static/media/` (gitignored) so files
**Local development:** store media in `static/media/` (gitignored) so files
are accessible at `/media/` when serving locally.
**Creating buckets manually** (for existing domains without buckets):
@@ -164,16 +145,26 @@ curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/jso
"https://garage.hantim.net/v2/UpdateBucket?id=BUCKET_ID"
```
## Restoring from backup
## Garage cluster initialization
Run the same three commands as provisioning. `bootstrap.sh` is idempotent:
- Existing packages are skipped
- Existing certs are skipped (or re-issued if the server is new)
- All services are started
After provisioning a fresh server, Garage connects to argento automatically.
The cluster layout must be assigned manually once (node IDs change per install):
```bash
docker exec garage /garage status
docker exec garage /garage layout assign <hantim-node-id> -z hantim -c 1G
docker exec garage /garage layout assign <argento-node-id> -z argento -c 1G
docker exec garage /garage layout apply --version 1
```
If only hantim is reprovisioned, argento retains the layout and hantim
reconnects automatically.
## Secrets
Bitwarden Secrets Manager (project: `hantim`, fetched via `bws` CLI on the server):
### Bitwarden Secrets Manager
Project: `hantim`. Fetched via `bws` CLI on the server.
| Secret | Purpose | Used by |
|---|---|---|
@@ -192,12 +183,13 @@ Machine accounts: `hantim-server` (token at `/etc/bws-token`), `hantim-ci` (rese
### Service secrets (`.env.keys`)
Services that need secrets declare them in a `.env.keys` file (format:
`ENV_VAR=bws-secret-name`, one per line). `deploy.sh` reads this file,
fetches each secret from bws, and generates a `.env` file before starting
the service. See `docker/garage/.env.keys` for an example.
Services that need secrets declare them in `.env.keys` (format:
`ENV_VAR=bws-secret-name`, one per line). `deploy.sh` fetches each secret
from bws and generates `.env` before starting the service.
Gitea org-level (`hantim`) secrets/variables:
### Gitea org-level secrets
Stored in the `hantim` Gitea org for direct use in CI workflows:
| Name | Type | Purpose |
|---|---|---|