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>
This commit is contained in:
vitya
2026-05-22 15:09:30 +03:00
parent cab4a8b92d
commit e64d94cd25
7 changed files with 267 additions and 0 deletions

65
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,65 @@
# Deploy: board-viewer
# Target: /opt/stacks/board-viewer/docker-compose.yml on VDS
#
# Prereqs:
# 1. DNS A-record: board.kzntsv.site → VDS IP
# 2. Traefik network `proxy` exists (it does — used by Gitea et al)
# 3. ./auth.toml contains gitea_url, gitea_token, board_viewer_repos
# 4. ./.htpasswd built via: htpasswd -nbB <user> <pass> >> ./.htpasswd
#
# Apply:
# cd /opt/stacks/board-viewer && docker compose up -d
#
# First build is local: see deploy/README.md for image build & push to
# registry.kzntsv.site, or build on host via `docker compose build`.
services:
build:
image: registry.kzntsv.site/board-viewer-build:latest
container_name: board-viewer-build
restart: unless-stopped
environment:
BOARD_CONFIG_PATH: /etc/board-viewer/auth.toml
BOARD_DIST_DIR: /output
BOARD_TICK_SEC: "300"
volumes:
- ./auth.toml:/etc/board-viewer/auth.toml:ro
- dist:/output
networks:
- proxy
web:
image: nginx:alpine
container_name: board-viewer-web
restart: unless-stopped
env_file:
- .env
volumes:
- dist:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- proxy
depends_on:
- build
labels:
traefik.enable: "true"
traefik.docker.network: "proxy"
traefik.http.routers.board-viewer.entrypoints: "websecure"
traefik.http.routers.board-viewer.rule: "Host(`board.kzntsv.site`)"
traefik.http.routers.board-viewer.tls.certresolver: "letsEncrypt"
traefik.http.routers.board-viewer.middlewares: "board-viewer-auth"
# Basic-auth users live in .env as BOARD_VIEWER_USERS
# (one or more `user:bcrypt-hash` entries, comma-separated, with $$ for $).
# See deploy/README.md "One-time setup" step 3.
traefik.http.middlewares.board-viewer-auth.basicauth.users: "${BOARD_VIEWER_USERS}"
traefik.http.services.board-viewer.loadbalancer.server.port: "80"
volumes:
dist:
networks:
proxy:
external: true