207 lines
7.9 KiB
Markdown
207 lines
7.9 KiB
Markdown
# hantim-server
|
|
|
|
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.
|
|
|
|
## 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)
|
|
deploy.sh # Deploy + cert issuance + provision (called via SSH)
|
|
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
|
|
```
|
|
|
|
## Provisioning a new server
|
|
|
|
On a fresh Rocky Linux 9 install:
|
|
|
|
```bash
|
|
dnf install -y git
|
|
git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim
|
|
bash /opt/hantim/scripts/bootstrap.sh
|
|
```
|
|
|
|
`bootstrap.sh` runs once manually. It:
|
|
|
|
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:
|
|
|
|
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**:
|
|
- 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
|
|
|
|
```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.
|
|
|
|
**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.
|
|
|
|
`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:
|
|
|
|
```bash
|
|
git clone git@git.timothykim.net:hantim/<domain>.git
|
|
cd <domain>
|
|
# edit content
|
|
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
|
|
|
|
**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
|
|
|
|
## 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.
|
|
|
|
## 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.
|
|
|
|
**Endpoints:**
|
|
- `s3.hantim.net` — S3 API for uploads (authenticated with `media-key`)
|
|
- `garage.hantim.net` — Admin API for bucket management
|
|
- `https://www.<domain>/media/<path>` — public file access
|
|
|
|
**Uploading media:**
|
|
|
|
```bash
|
|
# Configure AWS CLI (one-time)
|
|
aws configure # key ID, secret key from bws, region: garage, output: json
|
|
# Then add to ~/.aws/config under [default]:
|
|
# endpoint_url = https://s3.hantim.net
|
|
|
|
# Upload files
|
|
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
|
|
are accessible at `/media/` when serving locally.
|
|
|
|
**Creating buckets manually** (for existing domains without buckets):
|
|
|
|
```bash
|
|
# Get admin token and media key ID from bws
|
|
# Create bucket
|
|
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"globalAlias": "example.com"}' https://garage.hantim.net/v2/CreateBucket
|
|
# Note the bucket ID from the response, then:
|
|
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"bucketId": "BUCKET_ID", "accessKeyId": "MEDIA_KEY_ID", "permissions": {"read": true, "write": true, "owner": false}}' \
|
|
https://garage.hantim.net/v2/AllowBucketKey
|
|
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
-d '{"websiteAccess": {"enabled": true, "indexDocument": "index.html"}}' \
|
|
"https://garage.hantim.net/v2/UpdateBucket?id=BUCKET_ID"
|
|
```
|
|
|
|
## Restoring from backup
|
|
|
|
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
|
|
|
|
## Secrets
|
|
|
|
Bitwarden Secrets Manager (project: `hantim`, fetched via `bws` CLI on the server):
|
|
|
|
| Secret | Purpose | Used by |
|
|
|---|---|---|
|
|
| `hantim-ci-registry-push` | Gitea token for Docker registry | `bootstrap.sh` |
|
|
| `hantim-deploy-ssh-public-key` | Deploy user's SSH public key | `bootstrap.sh` |
|
|
| `hantim-deploy-ssh-private-key` | Deploy user's SSH private key | `new-app.sh` |
|
|
| `hantim-new-app-script` | Gitea API token for creating repos | `new-app.sh` |
|
|
| `hantim-vultr-api-key` | Vultr API key for DNS management | `new-app.sh` |
|
|
| `hantim-garage-rpc-secret` | Garage cluster RPC secret | `deploy.sh` |
|
|
| `hantim-garage-argento-node-id` | Argento's Garage node ID + address | `deploy.sh` |
|
|
| `hantim-garage-admin-token` | Garage admin API token | `deploy.sh`, `new-app.sh` |
|
|
| `hantim-garage-media-key-id` | S3 access key ID for media uploads | `new-app.sh`, `aws` CLI |
|
|
| `hantim-garage-media-secret-key` | S3 secret key for media uploads | `aws` CLI |
|
|
|
|
Machine accounts: `hantim-server` (token at `/etc/bws-token`), `hantim-ci` (reserved).
|
|
|
|
### 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.
|
|
|
|
Gitea org-level (`hantim`) secrets/variables:
|
|
|
|
| Name | Type | Purpose |
|
|
|---|---|---|
|
|
| `DEPLOY_HOST` | Variable | Server IP or hostname |
|
|
| `DEPLOY_SSH_KEY` | Secret | SSH private key for the deploy user |
|
|
| `CI_REGISTRY_TOKEN` | Secret | Gitea token for Docker registry login |
|