Replace the ad-hoc `worker.js` with a self-contained `linearvoid-media` worker under `infrastructure/cloudflare/` that owns `media.linearvoid.tv`. It serves HLS objects from the R2 bucket, enforcing immutable Cache-Control on segments, a short TTL on live manifests, plus HLS CORS, OPTIONS preflight, Range support, and a `/health` probe. Includes wrapper config with production domain, Vitest unit tests, Miniflare R2 integration tests, automated deploy workflow, and docs updates.
8.8 KiB
Linear Void Setup Guide
Last Updated: 2026-08-03
Prerequisites, environment setup, and running the app locally and in production. Architecture: docs/architecture/README.md.
Prerequisites
- Docker + Docker Compose — runs the full stack (playout, chat, nginx, redis, db, prometheus, grafana).
- Node.js 22+ / npm — frontend dev and builds (
cd frontend && npm install && npm run build). - Go 1.25+ — working on the Go services outside Docker.
- FFmpeg — playout HLS generation (
apt install ffmpeg). - Git.
- Cloudflare account — R2 storage + CDN.
- Gitea account — image registry + CI/CD. Set Actions secrets at
https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets:CI_REGISTRY_USER,CI_REGISTRY_TOKEN(scopewrite:packages),ADMIN_USERNAME,ADMIN_PASSWORD,DATABASE_URL. - Hetzner Cloud (optional, prod) — US-East VPS (Ashburn, VA per ADR-001).
Network Environments
Two distinct targets; know which one you're using.
flowchart LR
subgraph Test_Lab["Test Lab (192.168.88.0/24)"]
TEST_HOST["192.168.88.40<br/>Docker Engine :2375"]
end
subgraph Prod["Production (Hetzner — NOT provisioned)"]
PROD_HOST["Hetzner CX22 Ashburn, VA<br/>Public IP TBD"]
end
subgraph Gitea["Gitea CI/CD"]
CI["CI: build + push images"]
CD["CD: deploy via DOCKER_HOST"]
end
subgraph CF["Cloudflare"]
DNS["linearvoid.tv"]
R2["R2 Bucket"]
end
CD -->|"DOCKER_HOST=tcp://192.168.88.40:2375"| TEST_HOST
CD -.->|"DOCKER_HOST=tcp://<hetzner-ip>:2375 (when provisioned)"| PROD_HOST
TEST_HOST -->|"test traffic"| DNS
PROD_HOST -.->|"production traffic"| DNS
TEST_HOST --> R2
PROD_HOST -.-> R2
- Test Lab (active): internal
192.168.88.0/24, host192.168.88.40; receives automated CD deploys from Gitea Actions. Used for staging validation. - Production (pending): Hetzner CX22, Ashburn VA — not yet provisioned. Compose/CD are ready; only
DOCKER_HOSTneeds changing. See DEPLOYMENT.md.
Environment Setup
git clone https://git.allucanget.biz/zwitschi/linearvoid.git && cd linearvoidcp .env.example .envthen replace placeholders.- Compose overrides:
docker-compose.ymlbuilds images locally and mountsfrontend/distinto nginx;docker-compose.prod.ymlpulls prebuilt images from Gitea.- Internal URLs use compose hostnames:
REDIS_URL=redis://redis:6379/0,DATABASE_URL=postgres://linearvoid:<pw>@db:5432/linearvoid?sslmode=disable. - Public routing:
VITE_PUBLIC_APP_URL=https://linearvoid.tv,VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws,VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8.
- Internal URLs use compose hostnames:
- Create data dirs:
mkdir -p data/playlist data/media data/hls data/chat - Optional: place normalized MP4s in
data/media/and createdata/playlist/playlist.txt(see ADR-001 for normalization).
Gitea Setup
- User with repo + registry access; generate a token (scope
write:packages) stored asCI_REGISTRY_TOKEN. - Set Actions secrets (see Prerequisites) to enable automated CI/CD and deploys.
- Set registry vars:
CI_REGISTRY_URL,CI_REGISTRY_NAMESPACE(e.g.zwitschi/linearvoid),CI_REGISTRY_USER. - Auth:
docker login git.allucanget.biz. - Local dev builds from source; prod pulls
playout,chat,nginximages from the registry namespace. Verify:latesttags exist before rollout.
Cloudflare R2 Setup
- Create R2 bucket (e.g.
linearvoid-media, matchingR2_BUCKET_NAME); generate an access key pair →R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_REGION=auto. - S3 endpoint:
https://<account-id>.r2.cloudflarestorage.com→R2_ENDPOINT. - Attach
media.linearvoid.tvas custom media domain →MEDIA_DOMAIN; add CORS allowinghttps://linearvoid.tvGET for HLS delivery. - Point
linearvoid.tv(+wwwredirect) at the app entrypoint; keepmedia.*on R2. Proxied DNS, TLS Full (strict). - Apply the
.m4s/.m3u8cache rules fromcache-rules.md; enable Smart Tiered Cache; deploy thelinearvoid-mediaworker onmedia.linearvoid.tv(replace the R2 bucket's direct custom-domain binding on that host) to enforce immutable segment caching + HLS CORS. - Verify:
aws s3 ls --endpoint-url $R2_ENDPOINT; check cache hit ratio after serving traffic.
Running the Application
Local development
docker compose up --build # start all services
docker compose down # stop
Access: app/admin https://linearvoid.tv (via proxy) or http://localhost; WS wss://linearvoid.tv/ws / ws://localhost/ws. Health: playout :8081/health, chat :3000/health. Monitoring: Prometheus :9090, Grafana :3001 (default admin/admin).
Frontend dev (optional): cd frontend && npm install && npm run dev (http://localhost:5173, proxies API). For nginx-backed local runs: npm run build (nginx serves frontend/dist).
Test lab (remote Docker host)
Deploy to 192.168.88.40 over Docker's TCP socket (port 2375).
Enable TCP on Alpine host — edit /etc/conf.d/docker:
DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375"
Then rc-service docker restart; verify with netstat -tlnp | grep 2375; allow port 2375 from 192.168.88.0/24 in the firewall.
Deploy from a machine with the Docker CLI:
export DOCKER_HOST="tcp://192.168.88.40:2375"
docker info # confirm remote host
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans # registry images
# or build from source: docker compose up -d --build
unset DOCKER_HOST # back to local
Health: http://192.168.88.40:8081/health (playout), :3000/health (chat), nginx entrypoint :8080.
Coolify deployment
Coolify replaces the Gitea CD DOCKER_HOST orchestration (Cloudflare DNS/R2/cache unchanged). Install (curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash), add the target server via SSH, create a Docker Compose resource from docker-compose.prod.yml (or the repo), configure env vars in Coolify's store, map persistent data/ and named volumes, and assign linearvoid.tv to nginx for Traefik TLS. Full walkthrough: COOLIFY.md.
Production setup
- Provision a Linux VPS (Docker, Compose, Git); open HTTP/HTTPS ports.
git clone … && cd linearvoid && cp .env.example .env.- Set production
.env: strongPOSTGRES_PASSWORD/GRAFANA_PASSWORD, compose-hostname internal URLs,MEDIA_DOMAIN=media.linearvoid.tv, publicVITE_PUBLIC_*URLs. For local-only testing bypassing the proxy, usehttp://localhost/ws://localhost/ws/http://localhost/hls/playlist.m3u8. mkdir -p data/playlist data/media data/hls data/chat.- Ensure
playout,chat,nginximages exist in the registry;docker loginfrom the VPS. - Deploy:
docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d. - Verify:
docker compose -f docker-compose.prod.yml ps, health endpoints, HLS written todata/hls, media domain serves via Cloudflare. - Harden: HTTPS in front of nginx (Cloudflare,
www→ apex), restrict Grafana/Prometheus to trusted IPs, run under systemd for auto-start.
Production (Hetzner VPS + Cloudflare)
- Provision CX22 in Ashburn, VA (ADR-001).
- Cloudflare: R2 bucket (
enamhint), deploy thelinearvoid-mediaworker onmedia.linearvoid.tv+ cache rules,linearvoid.tv/wwwat the app entrypoint withwww→apex redirect, Full (strict) TLS. - Deploy from the project:
docker compose -f docker-compose.prod.yml up -d(or pull from Gitea registry); manage under systemd. - Monitoring: Grafana dashboards from
monitoring/grafana/.
See docs/architecture/README.md (arc42) and the ADRs for decisions and cost/scaling guidance (ADR-001).
Troubleshooting
- R2 upload fails — check
.envcreds/bucket;aws s3 ls --endpoint-url=$R2_ENDPOINT. - Normalization errors — run
backend/playout/start-playout.shto see ffprobe output. Video must be H.264/AAC, 30 fps constant, 2s keyframes (ADR-001). - Chat won't connect — check Redis and WS URL;
/healthshould return 200. - Cache not working — verify
.m4slong-TTL /.m3u8short-TTL cache rules and hit ratio. - Docker issues —
docker compose logs; check port conflicts. - Admin interface —
ADMIN_USERNAME,ADMIN_PASSWORD,ADMIN_SESSION_TTL_HOURSset; session-based login protects/admin/and/api/admin/*. - Prod pull fails —
docker loginto Gitea succeeded and image tags exist.
Further Reading
Architecture: docs/architecture/README.md · Decisions: docs/architecture/adr/ (ADR-001…007) · Contributing: CONTRIBUTING.md · Overview: root README.md.