restructure docs: eliminate duplication across md files
This commit is contained in:
+128
-99
@@ -1,99 +1,38 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
|
Technical details for the hantim infrastructure. See README.md for usage
|
||||||
|
instructions, USECASES.md for expected behaviors.
|
||||||
|
|
||||||
## Servers
|
## 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
|
- Runs nginx reverse proxy + all app containers 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
|
- 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:
|
- Gitea instance at `git.timothykim.net`
|
||||||
|
|
||||||
- Git hosting for all repos
|
|
||||||
- Docker container registry (`git.timothykim.net/hantim/<app>`)
|
- Docker container registry (`git.timothykim.net/hantim/<app>`)
|
||||||
- Gitea Actions CI/CD (runner registered at site level)
|
- Gitea Actions CI runner (registered at site level)
|
||||||
- Organization-level secrets and variables shared across repos
|
- Second Garage storage node (replication partner)
|
||||||
|
- Garage web UI for administration
|
||||||
|
|
||||||
### Bitwarden Secrets Manager (cloud)
|
### Bitwarden Secrets Manager (cloud)
|
||||||
|
|
||||||
Secrets are stored in Bitwarden Secrets Manager (free tier, project: `hantim`).
|
All secrets stored in project `hantim`, fetched via `bws` CLI.
|
||||||
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
|
|
||||||
|
|
||||||
Machine accounts:
|
Machine accounts:
|
||||||
- `hantim-server` -- used by `bootstrap.sh` and `configure.sh` on the server
|
- `hantim-server` — used by `bootstrap.sh` and `configure.sh` (token at `/etc/bws-token`)
|
||||||
(access token at `/etc/bws-token`)
|
- `hantim-ci` — reserved for future CI use
|
||||||
- `hantim-ci` -- reserved for future CI use
|
|
||||||
|
|
||||||
### Gitea secrets
|
Gitea org-level secrets (`hantim`) duplicate some bws secrets for direct
|
||||||
|
CI use without installing `bws`: `DEPLOY_SSH_KEY`, `CI_REGISTRY_TOKEN`,
|
||||||
Workflow secrets stored in the `hantim` Gitea org. These duplicate some
|
`DEPLOY_HOST` (variable).
|
||||||
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.
|
|
||||||
|
|
||||||
## Network topology
|
## Network topology
|
||||||
|
|
||||||
@@ -103,16 +42,16 @@ app source code and a `build.yml` workflow.
|
|||||||
[Hantim Server]
|
[Hantim Server]
|
||||||
|
|
|
|
||||||
nginx (port 80/443)
|
nginx (port 80/443)
|
||||||
| |
|
/ | | \
|
||||||
example_com example_org ...
|
example_com app_two garage ...
|
||||||
(shared Docker network)
|
(shared Docker network)
|
||||||
```
|
```
|
||||||
|
|
||||||
- nginx is the only container with host port bindings (80, 443)
|
- nginx is the only container with host port bindings (80, 443)
|
||||||
- All app containers are on the `shared` Docker network
|
- 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
|
- 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
|
## Deploy flow
|
||||||
|
|
||||||
@@ -120,21 +59,20 @@ app source code and a `build.yml` workflow.
|
|||||||
|
|
||||||
```
|
```
|
||||||
Developer pushes to app repo (e.g., example.com)
|
Developer pushes to app repo (e.g., example.com)
|
||||||
-> build.yml workflow runs on Gitea runner
|
-> build.yml runs on Gitea runner
|
||||||
-> docker build + push to git.timothykim.net registry
|
-> docker build + push to registry
|
||||||
-> SSH to deploy@hantim as deploy-<domain>
|
-> SSH to deploy@hantim as deploy-<domain>
|
||||||
-> deploy.sh: git pull, docker compose pull, docker compose up -d
|
-> 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)
|
### Infrastructure change (push to hantim-server)
|
||||||
|
|
||||||
```
|
```
|
||||||
Developer pushes 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>
|
-> SSH to deploy@hantim as deploy-<app>
|
||||||
-> deploy.sh: git pull, docker compose pull, docker compose up -d
|
-> 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)
|
### Setup change (push to hantim-server)
|
||||||
@@ -143,18 +81,109 @@ Developer pushes to hantim-server
|
|||||||
Developer pushes change to scripts/configure.sh
|
Developer pushes change to scripts/configure.sh
|
||||||
-> provision.yml triggers
|
-> provision.yml triggers
|
||||||
-> SSH to deploy@hantim as provision
|
-> 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`)
|
- **App name = domain** (e.g., `example.com`)
|
||||||
- **Container name** = domain with dots replaced by underscores (e.g.,
|
- **Container name** = domain with dots → underscores (e.g., `example_com`)
|
||||||
`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.
|
|
||||||
- **Image name** = `git.timothykim.net/hantim/<domain>:latest`
|
- **Image name** = `git.timothykim.net/hantim/<domain>:latest`
|
||||||
- All HTTP traffic redirects to `https://www.<domain>`
|
- **Cert name** = nginx conf filename without `.conf`
|
||||||
- Bare domain HTTPS redirects to `https://www.<domain>`
|
- **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.
|
||||||
|
|||||||
@@ -1,93 +1,44 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
## Project overview
|
Quick context for Claude Code. See README.md for full details, ARCHITECTURE.md
|
||||||
|
for technical deep-dives, USECASES.md for expected behaviors.
|
||||||
Server provisioning and app management for the hantim webserver. This repo
|
|
||||||
lives at `/opt/hantim` on the production server (Rocky Linux 9 on Vultr) and
|
|
||||||
manages Docker Compose configs, deploy scripts, nginx configs, and Gitea
|
|
||||||
Actions workflows.
|
|
||||||
|
|
||||||
## Key files
|
## Key files
|
||||||
|
|
||||||
- `scripts/bootstrap.sh` -- First-time server setup (manual, uses Bitwarden)
|
- `scripts/bootstrap.sh` -- First-time server setup (manual, uses Bitwarden)
|
||||||
- `scripts/configure.sh` -- Idempotent server config (firewall, certbot, start services; CI-safe)
|
- `scripts/configure.sh` -- Idempotent server config (firewall, certbot, start services; CI-safe)
|
||||||
- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance + provision (deploy-*, cert-*, provision commands)
|
- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance + provision (deploy-*, cert-*, provision commands)
|
||||||
- `tools/new-app.sh` -- Single-command new static site (DNS + Gitea repo + cert + deploy + verify)
|
- `tools/new-app.sh` -- Single-command new static site (DNS + Gitea repo + cert + bucket + deploy + verify)
|
||||||
- `tools/new-service.sh` -- Scaffold a new Docker Compose service (compose + workflow)
|
- `tools/new-service.sh` -- Scaffold a new Docker Compose service (compose + workflow)
|
||||||
- `tools/remove-app.sh` -- Remove a static site (local files + Gitea repo)
|
- `tools/remove-app.sh` -- Remove a static site (local files + Gitea repo)
|
||||||
- `docker/nginx/` -- Reverse proxy (nginx:alpine + certbot on host)
|
- `docker/nginx/conf.d/` -- Per-app nginx server blocks
|
||||||
- `docker/<domain>/compose.yml` -- Per-app compose files
|
- `docker/<domain>/compose.yml` -- Per-app compose files
|
||||||
- `.gitea/workflows/deploy-<app>.yml` -- Per-app deploy workflows
|
- `docker/garage/` -- Garage S3 object storage (Dockerfile, garage.toml, .env.keys)
|
||||||
- `.gitea/workflows/provision.yml` -- Triggers configure.sh on setup changes
|
- `.gitea/workflows/` -- Per-app deploy workflows + provision.yml
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- **App name = domain** (e.g., `example.com`)
|
- **App name = domain** (e.g., `example.com`)
|
||||||
- **Container name** = domain with dots replaced by underscores (e.g., `example_com`)
|
- **Container name** = dots → underscores (e.g., `example_com`)
|
||||||
- **Image** = `git.timothykim.net/hantim/<domain>:latest`
|
|
||||||
- **Cert name** = config filename without `.conf` (at `/etc/letsencrypt/live/<name>/`)
|
|
||||||
- **One domain per conf file** -- deploy.sh derives cert name from filename
|
- **One domain per conf file** -- deploy.sh derives cert name from filename
|
||||||
- All HTTP redirects to HTTPS; bare domain redirects to `www.<domain>`
|
- All HTTP → HTTPS; bare domain → `www.<domain>`
|
||||||
|
- All shell scripts use `set -euo pipefail` and must be idempotent
|
||||||
- The `shared` Docker network is created by nginx compose and used by all apps
|
- The `shared` Docker network is created by nginx compose and used by all apps
|
||||||
|
- Secrets are in Bitwarden Secrets Manager (see README.md for full list)
|
||||||
|
- Service secrets use `.env.keys` files (see README.md)
|
||||||
|
|
||||||
## Secrets
|
## Deploy flow
|
||||||
|
|
||||||
Bitwarden Secrets Manager (project: `hantim`, fetched via `bws` CLI):
|
- App repo push → `build.yml` → build/push image → SSH `deploy-<domain>` → deploy.sh
|
||||||
- `hantim-ci-registry-push` -- Gitea registry token (used by bootstrap.sh)
|
- This repo push → `deploy-<app>.yml` (path match) → SSH `deploy-<app>` → deploy.sh
|
||||||
- `hantim-deploy-ssh-private-key` -- deploy user SSH private key (used by new-app.sh)
|
- Setup change → `provision.yml` → SSH `provision` → configure.sh
|
||||||
- `hantim-deploy-ssh-public-key` -- deploy user SSH public key (used by bootstrap.sh)
|
|
||||||
- `hantim-new-app-script` -- Gitea API token for creating repos (used by new-app.sh)
|
|
||||||
- `hantim-vultr-api-key` -- Vultr API key for DNS management (used by new-app.sh)
|
|
||||||
- `hantim-garage-rpc-secret` -- Garage cluster RPC secret (used by deploy.sh)
|
|
||||||
- `hantim-garage-argento-node-id` -- Argento's Garage node ID + address (used by deploy.sh)
|
|
||||||
- `hantim-garage-admin-token` -- Garage admin API token (used by deploy.sh, new-app.sh)
|
|
||||||
- `hantim-garage-media-key-id` -- S3 access key ID for media uploads (used by new-app.sh)
|
|
||||||
- `hantim-garage-media-secret-key` -- S3 secret key for media uploads (used by aws CLI)
|
|
||||||
|
|
||||||
Machine accounts:
|
|
||||||
- `hantim-server` -- access token stored at `/etc/bws-token` on hantim
|
|
||||||
- `hantim-ci` -- access token stored as Gitea secret (reserved for future use)
|
|
||||||
|
|
||||||
Gitea org-level (`hantim`) secrets/variables:
|
|
||||||
- `DEPLOY_HOST` -- Variable (server IP)
|
|
||||||
- `DEPLOY_SSH_KEY` -- Secret (deploy SSH private key)
|
|
||||||
- `CI_REGISTRY_TOKEN` -- Secret (registry token)
|
|
||||||
|
|
||||||
## Working with this repo
|
|
||||||
|
|
||||||
### Adding a new static site
|
|
||||||
|
|
||||||
Run `./tools/new-app.sh <domain>` from dev machine. Handles everything:
|
|
||||||
DNS, Gitea repo, SSL cert, commit/push, build trigger, and verification.
|
|
||||||
See USECASES.md for full details.
|
|
||||||
|
|
||||||
### Adding a new Docker service
|
|
||||||
|
|
||||||
Run `./tools/new-service.sh <name>` to scaffold the compose file and deploy
|
|
||||||
workflow, then edit the compose file and commit.
|
|
||||||
|
|
||||||
### Editing scripts
|
|
||||||
|
|
||||||
All shell scripts use `set -euo pipefail`. Keep all steps idempotent.
|
|
||||||
`configure.sh` must be safe to re-run from CI.
|
|
||||||
|
|
||||||
### Deploy flow
|
|
||||||
|
|
||||||
App repo push -> `build.yml` builds image, pushes to registry, SSHes to server
|
|
||||||
-> `deploy.sh` pulls repo, pulls image, restarts container.
|
|
||||||
|
|
||||||
Infrastructure push (this repo) -> `deploy-<app>.yml` triggers on path match
|
|
||||||
-> same `deploy.sh` flow. nginx deploys test config with `nginx -t` first.
|
|
||||||
|
|
||||||
Setup change push -> `provision.yml` triggers -> `deploy.sh provision`
|
|
||||||
-> `configure.sh` (firewall, certbot, start services).
|
|
||||||
|
|
||||||
## Build and test
|
## Build and test
|
||||||
|
|
||||||
No build step. No tests. Verify changes by:
|
No build step. No tests. Verify changes by:
|
||||||
1. Reading scripts and checking idempotency
|
1. Reading scripts and checking idempotency
|
||||||
2. Walking through each use case in USECASES.md
|
2. Walking through each use case in USECASES.md
|
||||||
3. Cross-checking documentation (README.md, ARCHITECTURE.md, USECASES.md) against scripts
|
3. Cross-checking docs against scripts
|
||||||
|
|
||||||
## Commit style
|
## Commit style
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
lives at `/opt/hantim` on the server and contains Docker Compose configs, deploy
|
||||||
scripts, nginx configs, and Gitea Actions workflows for each app.
|
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
|
## 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/
|
scripts/
|
||||||
bootstrap.sh # First-time server setup (manual, uses Bitwarden)
|
bootstrap.sh # First-time server setup (manual, uses Bitwarden)
|
||||||
configure.sh # Idempotent server config (CI-safe)
|
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-app.sh # Add a new static site (run from dev machine)
|
||||||
new-service.sh # Add a new Docker service (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)
|
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
|
## 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
|
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)
|
`configure.sh` is idempotent and CI-safe. It opens firewall ports, sets up
|
||||||
2. Installs the `bws` CLI (Bitwarden Secrets Manager)
|
certbot renewal, and deploys all services (nginx first, then apps).
|
||||||
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:
|
**Prerequisites:**
|
||||||
|
|
||||||
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
|
- DNS for all configured domains must point to the server
|
||||||
- Bitwarden Secrets Manager access token for the `hantim-server` machine account
|
- 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
|
```bash
|
||||||
./tools/new-app.sh <domain>
|
./tools/new-app.sh <domain>
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: `./tools/new-app.sh example.com`
|
This single command handles everything: DNS records (Vultr), Gitea repo
|
||||||
|
(from `static-site-template`), nginx/compose/workflow configs, SSL cert,
|
||||||
This single command handles everything: creates DNS records on Vultr, creates
|
Garage media bucket, first build, and verification.
|
||||||
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`
|
**Prerequisites:** domain nameservers pointed to Vultr, `bws`/`jq`/`dig`
|
||||||
installed. Uses the deploy SSH key from Bitwarden Secrets Manager -- no
|
installed, `BWS_ACCESS_TOKEN` env var or token at `~/.config/hantim/bws-token`.
|
||||||
personal server access needed. See USECASES.md for full details.
|
|
||||||
|
|
||||||
`new-app.sh` also creates a Garage media bucket for the domain, allowing
|
After the site is live, clone the app repo and customize:
|
||||||
media files to be served at `https://www.<domain>/media/`.
|
|
||||||
|
|
||||||
After the site is live, clone the app repo and customize it:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@git.timothykim.net:hantim/<domain>.git
|
git clone git@git.timothykim.net:hantim/<domain>.git
|
||||||
@@ -94,58 +93,40 @@ git add . && git commit -m "initial content" && git push
|
|||||||
|
|
||||||
## Deploying changes
|
## Deploying changes
|
||||||
|
|
||||||
**App code change** (push to an app repo like `example.com`):
|
**App code change** — push to an app repo (e.g., `example.com`):
|
||||||
- The app's `build.yml` builds the Docker image, pushes to the registry, and
|
- `build.yml` builds the image, pushes to registry, SSHes to server to deploy
|
||||||
SSHes into the server to trigger a deploy
|
|
||||||
|
|
||||||
**Compose or nginx config change** (push to this repo):
|
**Compose/nginx config change** — push to this repo:
|
||||||
- Gitea Actions workflows trigger based on which `docker/<app>/` paths changed
|
- Path-matched workflows trigger and SSH to server
|
||||||
- `deploy.sh` pulls the latest code and runs `docker compose up -d`
|
- nginx deploys test config before applying (`nginx -t`)
|
||||||
- For nginx deploys, the config is tested before applying to prevent downtime
|
|
||||||
|
|
||||||
## Garage cluster initialization
|
**Setup change** — push changes to `scripts/configure.sh`:
|
||||||
|
- `provision.yml` triggers and re-runs `configure.sh`
|
||||||
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 hosting
|
||||||
|
|
||||||
Media files (images, videos) are stored in Garage and served via nginx at
|
Media files are stored in Garage and served via nginx at `/media/` on each
|
||||||
`/media/` on each domain. This keeps large files out of git repos.
|
domain. This keeps large files out of git repos.
|
||||||
|
|
||||||
**Endpoints:**
|
**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
|
- `garage.hantim.net` — Admin API for bucket management
|
||||||
- `https://www.<domain>/media/<path>` — public file access
|
- `https://www.<domain>/media/<path>` — public file access
|
||||||
|
|
||||||
**Uploading media:**
|
**Uploading:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Configure AWS CLI (one-time)
|
# One-time AWS CLI setup
|
||||||
aws configure # key ID, secret key from bws, region: garage, output: json
|
aws configure # key ID + secret from bws, region: garage, output: json
|
||||||
# Then add to ~/.aws/config under [default]:
|
# Then add to ~/.aws/config under [default]:
|
||||||
# endpoint_url = https://s3.hantim.net
|
# endpoint_url = https://s3.hantim.net
|
||||||
|
|
||||||
# Upload files
|
# Upload
|
||||||
aws s3 cp photo.jpg s3://example.com/photo.jpg
|
aws s3 cp photo.jpg s3://example.com/photo.jpg
|
||||||
aws s3 sync static/media/ s3://example.com/
|
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.
|
are accessible at `/media/` when serving locally.
|
||||||
|
|
||||||
**Creating buckets manually** (for existing domains without buckets):
|
**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"
|
"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:
|
After provisioning a fresh server, Garage connects to argento automatically.
|
||||||
- Existing packages are skipped
|
The cluster layout must be assigned manually once (node IDs change per install):
|
||||||
- Existing certs are skipped (or re-issued if the server is new)
|
|
||||||
- All services are started
|
```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
|
## 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 |
|
| Secret | Purpose | Used by |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -192,12 +183,13 @@ Machine accounts: `hantim-server` (token at `/etc/bws-token`), `hantim-ci` (rese
|
|||||||
|
|
||||||
### Service secrets (`.env.keys`)
|
### Service secrets (`.env.keys`)
|
||||||
|
|
||||||
Services that need secrets declare them in a `.env.keys` file (format:
|
Services that need secrets declare them in `.env.keys` (format:
|
||||||
`ENV_VAR=bws-secret-name`, one per line). `deploy.sh` reads this file,
|
`ENV_VAR=bws-secret-name`, one per line). `deploy.sh` fetches each secret
|
||||||
fetches each secret from bws, and generates a `.env` file before starting
|
from bws and generates `.env` before starting the service.
|
||||||
the service. See `docker/garage/.env.keys` for an example.
|
|
||||||
|
|
||||||
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 |
|
| Name | Type | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
+61
-155
@@ -1,193 +1,99 @@
|
|||||||
# Use Cases
|
# Use Cases
|
||||||
|
|
||||||
|
Expected behaviors for each operation this repo supports. See README.md for
|
||||||
|
instructions, ARCHITECTURE.md for technical details.
|
||||||
|
|
||||||
## 0. Initial Bitwarden Secrets Manager setup
|
## 0. Initial Bitwarden Secrets Manager setup
|
||||||
|
|
||||||
One-time setup before anything else works. Create the secrets project and
|
**Trigger:** one-time, before anything else works.
|
||||||
machine accounts in Bitwarden Secrets Manager.
|
|
||||||
|
|
||||||
1. Create a Bitwarden organization (free tier)
|
**Expected outcome:**
|
||||||
2. Enable Secrets Manager for the organization
|
- Bitwarden organization with Secrets Manager enabled
|
||||||
3. Create a project named `hantim`
|
- Project `hantim` with all secrets listed in README.md
|
||||||
4. Create all required secrets (see README.md Secrets table for the full list)
|
- Machine accounts `hantim-server` and `hantim-ci` with project access
|
||||||
5. Create machine accounts:
|
- Access tokens generated for each machine account
|
||||||
- `hantim-server` — grant access to the `hantim` project
|
|
||||||
- `hantim-ci` — reserved for future CI use
|
|
||||||
6. Generate access tokens for each machine account
|
|
||||||
|
|
||||||
## 1. Provision a new server
|
## 1. Provision a new server
|
||||||
|
|
||||||
Set up a fresh Rocky Linux 9 server from scratch.
|
**Trigger:** run `bootstrap.sh` on a fresh Rocky Linux 9 install.
|
||||||
|
|
||||||
```bash
|
**Prerequisites:** DNS pointed to server, bws access token ready.
|
||||||
dnf install -y git
|
|
||||||
git clone https://git.timothykim.net/hantim/hantim-server.git /opt/hantim
|
|
||||||
bash /opt/hantim/scripts/bootstrap.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
**Expected outcome:**
|
||||||
- Installs all dependencies (jq, certbot, unzip, Docker)
|
- All dependencies installed (Docker, certbot, bws, etc.)
|
||||||
- Installs `bws` CLI, prompts for Bitwarden Secrets Manager access token
|
- `deploy` user exists with restricted SSH and sudo
|
||||||
- Fetches registry token and deploy SSH public key via `bws`
|
- Firewall allows HTTP, HTTPS, Garage RPC
|
||||||
- Logs into Docker registry
|
- Certbot renewal configured
|
||||||
- Creates deploy user with restricted SSH + sudo
|
- All services running (nginx, apps, Garage)
|
||||||
- Runs `configure.sh`:
|
- SSL certs issued for all configured domains
|
||||||
- Opens firewall ports (HTTP, HTTPS, Garage RPC)
|
- `.env` files generated from bws for services with `.env.keys`
|
||||||
- Sets up certbot renewal
|
|
||||||
- Deploys all services via `deploy.sh` (generates `.env` files, issues certs, starts containers)
|
|
||||||
|
|
||||||
**Prerequisites:**
|
|
||||||
- DNS for all configured domains must point to the server
|
|
||||||
- Bitwarden Secrets Manager access token for the `hantim-server` machine account
|
|
||||||
- Docker images must exist in the Gitea registry (or apps will start on next push)
|
|
||||||
|
|
||||||
## 2. Restore from backup
|
## 2. Restore from backup
|
||||||
|
|
||||||
Identical to provisioning a new server. Run the same three commands on a
|
**Trigger:** same three commands as provisioning on a fresh server.
|
||||||
fresh server.
|
|
||||||
|
|
||||||
- `bootstrap.sh` and `configure.sh` are idempotent -- safe to re-run
|
**Expected outcome:** identical to provisioning. All scripts are idempotent.
|
||||||
- SSL certs are re-issued automatically (Let's Encrypt)
|
Certs are re-issued, images pulled from registry. No data migration needed
|
||||||
- Docker images are pulled from the Gitea registry
|
for stateless static sites.
|
||||||
- No data migration needed for stateless static sites
|
|
||||||
|
|
||||||
## 3. Add a new static site
|
## 3. Add a new static site
|
||||||
|
|
||||||
From your dev machine (single command):
|
**Trigger:** `./tools/new-app.sh example.com` from dev machine.
|
||||||
|
|
||||||
```bash
|
**Prerequisites:** domain nameservers on Vultr, `bws`/`jq`/`dig` installed.
|
||||||
./tools/new-app.sh example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**Prerequisites:**
|
**Expected outcome:**
|
||||||
- Bitwarden Secrets Manager must contain `hantim-new-app-script`,
|
- DNS A records created for bare + www
|
||||||
`hantim-vultr-api-key`, and `hantim-deploy-ssh-private-key`
|
- Gitea repo `hantim/example.com` created from template
|
||||||
- Domain nameservers must be pointed to Vultr (configured on directnic.com)
|
- Config files added: compose, nginx conf, deploy workflow
|
||||||
- Dependencies: `bws`, `jq`, `dig`
|
- SSL cert issued (zero downtime)
|
||||||
- `BWS_ACCESS_TOKEN` env var or token saved at `~/.config/hantim/bws-token`
|
- Garage media bucket created with media-key access
|
||||||
|
- First build triggered
|
||||||
**What happens (fully automated):**
|
- `https://www.example.com` responds within 3 minutes
|
||||||
1. Resolves server IP from `hantim.net`
|
- Site ready for customization via git clone + push
|
||||||
2. Fetches Gitea API token, Vultr API key, and deploy SSH key from Bitwarden Secrets Manager
|
|
||||||
3. Creates DNS zone on Vultr (if needed) and A records for bare + www
|
|
||||||
4. Waits for DNS to propagate
|
|
||||||
5. Creates Gitea repo `hantim/example.com` from `static-site-template`
|
|
||||||
6. Creates local files:
|
|
||||||
- `docker/example.com/compose.yml` (container name: `example_com`)
|
|
||||||
- `.gitea/workflows/deploy-example.com.yml`
|
|
||||||
- `docker/nginx/conf.d/example.com.conf` (HTTP->HTTPS, bare->www, proxy)
|
|
||||||
7. SSHes to server as deploy user, issues SSL cert via webroot (zero downtime)
|
|
||||||
8. Creates Garage media bucket via admin API (create, allow media-key, enable website)
|
|
||||||
9. Commits and pushes hantim-server (triggers deploy)
|
|
||||||
10. Triggers initial build of the app repo via Gitea API
|
|
||||||
11. Polls `https://www.example.com` until it responds (up to 3 minutes)
|
|
||||||
|
|
||||||
After the site is live, clone the app repo and customize:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone git@git.timothykim.net:hantim/example.com.git
|
|
||||||
cd example.com
|
|
||||||
# edit static/index.html, etc.
|
|
||||||
git add . && git commit -m "initial content" && git push
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Update app code
|
## 4. Update app code
|
||||||
|
|
||||||
Push a change to the app repo (e.g., `example.com`):
|
**Trigger:** push to an app repo.
|
||||||
|
|
||||||
```bash
|
**Expected outcome:**
|
||||||
cd example.com
|
- Docker image built and pushed to registry
|
||||||
# make changes
|
- Container restarted on server with new image
|
||||||
git add . && git commit -m "update content" && git push
|
- No manual steps
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
|
||||||
1. Gitea Actions runs `build.yml`: builds Docker image, pushes to registry
|
|
||||||
2. SSHes into server, runs `deploy-example.com`
|
|
||||||
3. `deploy.sh` pulls latest hantim-server, pulls new image, restarts container
|
|
||||||
|
|
||||||
No manual steps needed.
|
|
||||||
|
|
||||||
## 5. Update Docker Compose config
|
## 5. Update Docker Compose config
|
||||||
|
|
||||||
Edit a compose file in this repo and push:
|
**Trigger:** push compose file change to this repo.
|
||||||
|
|
||||||
```bash
|
**Expected outcome:**
|
||||||
vim docker/example.com/compose.yml
|
- Path-matched workflow triggers
|
||||||
git add docker/example.com/compose.yml
|
- Container restarted with updated config
|
||||||
git commit -m "update example.com config"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
|
||||||
1. `deploy-example.com.yml` workflow triggers (path match)
|
|
||||||
2. `deploy.sh` pulls latest code, pulls image, runs `docker compose up -d`
|
|
||||||
|
|
||||||
## 6. Update nginx config
|
## 6. Update nginx config
|
||||||
|
|
||||||
Edit an nginx config in this repo and push:
|
**Trigger:** push nginx conf change to this repo.
|
||||||
|
|
||||||
```bash
|
**Expected outcome:**
|
||||||
vim docker/nginx/conf.d/example.com.conf
|
- Config tested with `nginx -t` before applying
|
||||||
git add docker/nginx/conf.d/example.com.conf
|
- If test fails, running nginx is untouched
|
||||||
git commit -m "update nginx config"
|
- If new certs needed, issued before applying config
|
||||||
git push
|
- nginx reloaded with new config
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
## 7. Re-run configure
|
||||||
1. `deploy-nginx.yml` workflow triggers (path match)
|
|
||||||
2. `deploy.sh` pulls latest code
|
|
||||||
3. If any conf files reference certs that don't exist yet, nginx is briefly
|
|
||||||
stopped and certs are issued via standalone certbot (auto-restarts on failure)
|
|
||||||
4. Tests nginx config with `nginx -t` -- if it fails, deploy aborts and the
|
|
||||||
running nginx is untouched
|
|
||||||
5. Runs `docker compose up -d` and reloads nginx
|
|
||||||
|
|
||||||
**Note:** Each conf file must contain server blocks for only one domain. The
|
**Trigger:** push change to `scripts/configure.sh`, or run manually.
|
||||||
cert name is derived from the filename (e.g. `example.com.conf` -> cert
|
|
||||||
`example.com`). Redirect-only domains get their own conf file.
|
|
||||||
|
|
||||||
## 7. Re-run configure on existing server
|
**Expected outcome:**
|
||||||
|
- All steps idempotent — safe to run anytime
|
||||||
Safe to do at any time, manually or via CI:
|
- Existing certs skipped
|
||||||
|
- Services restarted
|
||||||
```bash
|
|
||||||
cd /opt/hantim
|
|
||||||
git pull
|
|
||||||
bash scripts/configure.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Or push a change to `scripts/configure.sh` — the `provision.yml` workflow
|
|
||||||
triggers automatically.
|
|
||||||
|
|
||||||
- All steps are idempotent
|
|
||||||
- Existing certs are skipped
|
|
||||||
- Brief nginx downtime (seconds) while certbot checks run
|
|
||||||
- Services are restarted
|
|
||||||
|
|
||||||
## 8. Upload media files
|
## 8. Upload media files
|
||||||
|
|
||||||
Media files (images, videos) are stored in Garage and served at `/media/`
|
**Trigger:** `aws s3 cp` or `aws s3 sync` to `s3://example.com/`.
|
||||||
on each domain.
|
|
||||||
|
|
||||||
```bash
|
**Expected outcome:**
|
||||||
# One-time setup
|
- File uploaded via S3 API (`s3.hantim.net`)
|
||||||
aws configure # key ID + secret from bws, region: garage, output: json
|
- Replicated to both Garage nodes (hantim + argento)
|
||||||
# Then add to ~/.aws/config under [default]:
|
- Accessible at `https://www.example.com/media/<path>`
|
||||||
# endpoint_url = https://s3.hantim.net
|
- Local development: `static/media/` (gitignored) serves at `/media/`
|
||||||
|
|
||||||
# Upload a single file
|
|
||||||
aws s3 cp photo.jpg s3://example.com/photo.jpg
|
|
||||||
|
|
||||||
# Sync a directory
|
|
||||||
aws s3 sync static/media/ s3://example.com/
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
|
||||||
1. File is uploaded to the Garage S3 API via `s3.hantim.net`
|
|
||||||
2. Garage replicates the file to both nodes (hantim + argento)
|
|
||||||
3. File is accessible at `https://www.example.com/media/photo.jpg`
|
|
||||||
|
|
||||||
**Local development:** Store media in `static/media/` (gitignored). The app's
|
|
||||||
nginx serves these locally, matching the `/media/` path used in production.
|
|
||||||
|
|
||||||
**Reference in HTML:** Use absolute paths like `/media/photo.jpg`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user