Playout: slate fallback, strict ffprobe validation, Prometheus metrics Chat: history on join, DM offline delivery, env-config rate limits, Redis auto-reconnect, heartbeat, ban/mute enforcement, DM metrics Viewer: presence indicators, schedule overlay, public /api/schedule Admin: playlist drag-reorder, schedule overrides CRUD, user actions, inline mod buttons, CSV export, config editor, restart buttons, upload normalization gate, analytics time range nginx: CORS preflight, JSON health, stub_status Docs: test-lab vs production distinction across all configs/docs Infra: Redis maxmemory, CD health IP-agnostic, slate volume mount
6.8 KiB
Deployment Guide
Environments
Linear Void runs in two separate environments:
| Environment | Host | Docker Target | Purpose |
|---|---|---|---|
| Test Lab | 192.168.88.40 (internal) |
tcp://192.168.88.40:2375 |
Integration testing, staging, CI/CD validation |
| Production | Hetzner VPS (TBD) | tcp://<hetzner-ip>:2375 |
Public linearvoid.tv serving real viewers |
Current state (2026-08-06): Test lab is active and receives automated CD deploys. Production Hetzner VPS is not yet provisioned. The CD workflow (cd.yml) and docker-compose.prod.yml currently target the test lab. Switching to production requires only changing DOCKER_HOST — see Production Deployment below.
Production Target (Hetzner)
Status: Not provisioned. The production deployment plan is documented and the pipeline is ready — only the VPS needs to be created.
Planned Infrastructure
| Resource | Spec |
|---|---|
| Provider | Hetzner Cloud |
| Region | Ashburn, VA (us-east) |
| Instance | CX22 (2 vCPU, 4 GB RAM, 40 GB SSD) or equivalent |
| OS | Ubuntu 24.04 LTS or Debian 12 |
| Bandwidth | 20 TB included traffic/month |
| Estimated cost | ~€7–10/month base |
Cutover Plan
- Provision Hetzner VPS (see
TODO.md#92) - Add production secrets to Gitea (
CI_PROD_*prefix) - Update
DOCKER_HOSTin CD workflow to Hetzner public IP - Trigger deploy → validate health checks
- Update Cloudflare A record
linearvoid.tv→ Hetzner IP - Enable HTTPS, WAF, cache rules on production zone
- Decommission test lab as public target
Firewall / Security Group
| Port | Source | Purpose |
|---|---|---|
| 22 | Admin IPs | SSH management |
| 80 | 0.0.0.0/0 | HTTP (redirect to HTTPS) |
| 443 | 0.0.0.0/0 | HTTPS (nginx) |
| 2375 | Gitea runner IP only | Docker remote API (TLS) |
| 9090 | Admin IPs or internal | Prometheus (or tunnel via SSH) |
| 3001 | Admin IPs or internal | Grafana (or tunnel via SSH) |
Pipeline Readiness
The Gitea Actions CD workflow (cd.yml) already supports DOCKER_HOST as a configurable variable. To switch targets:
# Test lab (current)
DOCKER_HOST=tcp://192.168.88.40:2375
# Production (when provisioned)
DOCKER_HOST=tcp://<hetzner-public-ip>:2375
No code changes needed in the pipeline. See TODO.md #91 and #92–#99 for full production deployment checklist.
Target Topology
- Host: Linux server with Docker Engine
- Reverse proxy: nginx container (linearvoid-nginx)
- Core services: playout, chat, redis, postgres
- Observability: prometheus, grafana
- CDN/edge: Cloudflare
Prerequisites
- Docker Engine + Docker Compose plugin
- Network access to git.allucanget.biz
- A valid .env file with production values
- DNS configured for linearvoid.tv (and media.linearvoid.tv if split media domain is used)
Environment File
Use .env with at least:
- CI_REGISTRY_URL
- CI_REGISTRY_NAMESPACE
- CHAT_PORT
- NGINX_PORT
- DATABASE_URL (if overridden)
- REDIS_URL (if overridden)
- ADMIN_USERNAME
- ADMIN_PASSWORD
- VITE_PUBLIC_APP_URL
- VITE_PUBLIC_WS_URL
- VITE_PUBLIC_HLS_URL
- Cloudflare/R2 variables when media sync is enabled
See .env.example and SETUP.md for full variable reference.
Pull-and-Run Deployment
Images are built and pushed automatically by the CI and release workflows (see CONTRIBUTING.md for the release procedure).
Every push to main produces :main and :latest tags. Semantic version tags (v*) produce :X.Y.Z, :X.Y, and :latest tags.
From repository root:
docker login git.allucanget.biz
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans
First-Time or Recovery Provisioning
- Ensure host paths exist:
mkdir -p data/chat data/playlist data/media data/hls
- Confirm volumes and networks:
docker volume ls
docker network ls
- Start stack:
docker compose -f docker-compose.prod.yml up -d
Health Verification
After startup, check service status:
docker compose -f docker-compose.prod.yml ps
Endpoint checks:
- App: http://{host}:8080/
- Nginx health: http://{host}:8080/health
- Chat health: http://{host}:3000/health
- Playout health: http://{host}:8081/health
- Prometheus: http://{host}:9090
- Grafana: http://{host}:3001
Domain checks:
Zero/Low-Downtime Update Pattern
- Pull newest images.
- Recreate changed services only:
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d nginx chat playout
- Re-run health checks.
Rollback
If new images are unhealthy:
- Pin previous tags in docker-compose.prod.yml.
- Pull pinned tags.
- Restart impacted services.
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d nginx chat playout
Operational Tasks
View logs
docker compose -f docker-compose.prod.yml logs -f nginx chat playout
Restart one service
docker compose -f docker-compose.prod.yml restart chat
Stop stack
docker compose -f docker-compose.prod.yml down
Keep volumes (recommended) vs remove volumes (destructive)
# keep data
docker compose -f docker-compose.prod.yml down
# destructive (remove data)
docker compose -f docker-compose.prod.yml down -v
Nginx and Proxy Considerations
- If an external proxy (for example Nginx Proxy Manager) fronts the stack, align request body limits with internal nginx.
- For resumable admin uploads, ensure client_max_body_size supports chunk requests in both proxy layers.
Security Baseline
- Use strong ADMIN_PASSWORD and rotate regularly.
- Keep Docker host access restricted.
- Prefer HTTPS/WSS through Cloudflare.
- Restrict exposed ports if only proxy access is needed.
Common Deployment Pitfalls
- Missing frontend build artifacts in local-only compose workflows.
- Wrong registry credentials or missing login.
- Misaligned domain env vars (app/ws/hls) causing mixed-origin errors.
- Proxy-layer body size limits returning 413 on upload.
- Health checks failing before service warmup is complete.