closes board-viewer-cron-deploy Two-service stack at /opt/stacks/board-viewer/ on VDS: - board-viewer-build (node:22-alpine): cron-loop `while true; do node cli.ts; sleep 300`, writes to shared docker volume `dist` - board-viewer-web (nginx:alpine): serves dist read-only, Traefik route Host(`board.kzntsv.site`) → letsEncrypt + basic-auth middleware Files: - deploy/Dockerfile.build — build image (registry.kzntsv.site/board-viewer-build) - deploy/docker-compose.yml — stack with traefik labels (`proxy` network, same as Gitea) - deploy/nginx.conf — static serving with Cache-Control - deploy/auth.toml.example — Gitea token + board_viewer_repos whitelist - deploy/.env.example — BOARD_VIEWER_USERS for basic-auth (with $$ escape note) - deploy/README.md — DNS setup + image build/push + bootstrap + smoke + DR steps Configs are declarative ([skip-tdd: visual]). Acceptance = deploy + curl (see README §4 'Verify'). Both auth.toml and .env are gitignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
4.2 KiB
Markdown
135 lines
4.2 KiB
Markdown
# Deploy — `board-viewer`
|
|
|
|
Read-only HTML kanban viewer over Gitea API. Runs as a two-service Docker
|
|
stack on the VDS, behind Traefik with basic-auth.
|
|
|
|
```
|
|
VDS
|
|
├── traefik (websecure → letsEncrypt)
|
|
│ └── Host(`board.kzntsv.site`) → board-viewer-web (basic-auth middleware)
|
|
└── /opt/stacks/board-viewer/
|
|
├── docker-compose.yml ← stack
|
|
├── auth.toml ← Gitea token + repo whitelist (NOT committed)
|
|
├── .env ← BOARD_VIEWER_USERS (basic-auth bcrypt creds, NOT committed)
|
|
└── nginx.conf
|
|
```
|
|
|
|
Two services:
|
|
|
|
- **`board-viewer-build`** (`registry.kzntsv.site/board-viewer-build`)
|
|
Cron-loop: every `BOARD_TICK_SEC` (default 300s) runs
|
|
`node --experimental-strip-types src/cli.ts` against `auth.toml`,
|
|
writes `/output/{index.html, static/*}` into the shared volume.
|
|
|
|
- **`board-viewer-web`** (`nginx:alpine`)
|
|
Serves the shared volume on port 80; Traefik attaches host + basic-auth.
|
|
|
|
Volume `dist` is internal to the compose project; nginx mounts it read-only.
|
|
|
|
---
|
|
|
|
## One-time setup
|
|
|
|
### 1. DNS
|
|
|
|
Create an A-record:
|
|
|
|
```
|
|
board.kzntsv.site → <VDS IP> (TTL 3600)
|
|
```
|
|
|
|
Wait for propagation (a few minutes typically).
|
|
|
|
### 2. Build & push the build image
|
|
|
|
From a dev machine **at the repo root** (this Dockerfile expects to be
|
|
built from the project root):
|
|
|
|
```bash
|
|
docker build -f deploy/Dockerfile.build -t registry.kzntsv.site/board-viewer-build:latest .
|
|
docker push registry.kzntsv.site/board-viewer-build:latest
|
|
```
|
|
|
|
(See `~/projects/.wiki` for the VDS registry login command if your daemon
|
|
isn't already authenticated.)
|
|
|
|
### 3. Bootstrap on VDS
|
|
|
|
SSH to the VDS, then:
|
|
|
|
```bash
|
|
sudo mkdir -p /opt/stacks/board-viewer
|
|
cd /opt/stacks/board-viewer
|
|
|
|
# Copy compose + nginx.conf from this repo's deploy/ dir
|
|
# (via scp / git pull / whatever you use for other stacks).
|
|
sudo cp .../docker-compose.yml .
|
|
sudo cp .../nginx.conf .
|
|
|
|
# auth.toml — Gitea token + repo whitelist
|
|
sudo cp .../auth.toml.example auth.toml
|
|
sudo nano auth.toml # paste real gitea_token; edit board_viewer_repos list
|
|
|
|
# .env — basic-auth credentials for the Traefik middleware
|
|
sudo cp .../.env.example .env
|
|
# Generate a bcrypt hash:
|
|
htpasswd -nbB viewer 'STRONG_PASSWORD_HERE'
|
|
# Output is `viewer:$2y$05$...`. Replace every `$` with `$$` (compose
|
|
# interpolates env_file values), then paste into BOARD_VIEWER_USERS in .env.
|
|
# Multiple users: comma-separated, each with `$$` escaping.
|
|
sudo nano .env
|
|
|
|
sudo chmod 600 auth.toml .env
|
|
|
|
sudo docker compose pull
|
|
sudo docker compose up -d
|
|
```
|
|
|
|
### 4. Verify
|
|
|
|
```bash
|
|
# Service is up
|
|
sudo docker compose ps
|
|
|
|
# First render happens immediately on container start; check logs:
|
|
sudo docker compose logs -f build
|
|
# expect: "wrote /output/index.html (N records from M repos)"
|
|
|
|
# HTTP smoke (gives 401 without creds, 200 with)
|
|
curl -I https://board.kzntsv.site/ # → 401
|
|
curl -I -u viewer:'PASS' https://board.kzntsv.site/ # → 200
|
|
```
|
|
|
|
---
|
|
|
|
## Day-to-day
|
|
|
|
- **Force regen now:** `sudo docker compose restart build` (next render
|
|
runs immediately on container start).
|
|
- **View render logs:** `sudo docker compose logs --tail=200 build`
|
|
- **View access logs:** `sudo docker compose logs --tail=200 web`
|
|
- **Edit repo list:** edit `/opt/stacks/board-viewer/auth.toml`, then
|
|
`sudo docker compose restart build`.
|
|
- **Add user:** generate `htpasswd -nbB <user> 'PASS'`, escape every `$`
|
|
as `$$`, append (preceded by a comma) to `BOARD_VIEWER_USERS` in `.env`,
|
|
then `sudo docker compose up -d` (re-applies labels on the web service).
|
|
- **New version of `board-viewer-build` image:**
|
|
on dev → `docker build … && docker push …`,
|
|
on VDS → `sudo docker compose pull && sudo docker compose up -d`.
|
|
|
|
---
|
|
|
|
## Disaster recovery
|
|
|
|
The repo is the source of truth. To rebuild the stack from scratch:
|
|
|
|
1. Restore VDS hostname + DNS A-record.
|
|
2. Restore Traefik stack (separate concern, in `/opt/stacks/traefik/`).
|
|
3. `git clone https://git.kzntsv.site/OpeItcLoc03/board-viewer.git`.
|
|
4. Follow "One-time setup" above.
|
|
|
|
No persistent state lives in this stack — every render is freshly produced
|
|
from Gitea. The only artefacts to recreate are `auth.toml` (token from
|
|
password manager) and `.env` with `BOARD_VIEWER_USERS` (regenerate bcrypt
|
|
hash from password manager).
|