Files
board-viewer/deploy
vitya e64d94cd25 feat(deploy): docker-compose stack for VDS (board.kzntsv.site) [skip-tdd: visual]
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>
2026-05-22 15:09:30 +03:00
..

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):

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:

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

# 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).