14 KiB
Linear Void Setup Guide
This guide covers prerequisites, environment setup, running the application locally and in production, and troubleshooting. It references the architecture documentation(docs/architecture/README.md).
Prerequisites
- Docker & Docker Compose: Required for running the full stack (playout, chat, nginx, redis, db, prometheus, grafana). Install from docker.com.
- Node.js 20+: Required for frontend development and production builds (
cd frontend && npm install && npm run build). - npm: Installed with Node.js and used for the frontend dependency installation and Vite workflow.
- Go 1.23+: Recommended when working on the Go services outside Docker (
backend/chat,backend/playout). - FFmpeg: Used by the playout service for HLS generation. Install via
apt install ffmpeg(Linux), Homebrew (macOS), or Winget (Windows). - Git: For cloning and version control.
- Cloudflare Account: For R2 storage and CDN (free tier sufficient for most use cases).
- Gitea Account: Required if you want to push images to the self-hosted registry or use the repository's CI/CD flow.
- Hetzner Cloud Account (optional for production): For the US-East VPS (Ashburn, VA recommended per ADR-001).
Development Prerequisites
- Frontend dependencies: Run
cd frontend && npm installbeforenpm run devornpm run build. - Docker build support: Local development expects Docker to be able to build the
chat,playout, andnginximages referenced indocker-compose.yml. - Data directories: Create
data/playlist,data/media,data/hls, anddata/chatbefore the first run so bind mounts resolve cleanly. - Database choice: The stack defaults to PostgreSQL in
docker-compose.yml. If you want SQLite for local-only development, updateDATABASE_URLin.envaccordingly. - Redis availability: Chat presence, rate limiting, and pub/sub require Redis. The compose stack provides it automatically.
- Video inputs: Source videos should be normalized before playout. If the playlist contains invalid files, the playout container will fail its validation flow.
Environment Setup
-
Clone the Repository (if not already done):
git clone https://git.allucanget.biz/zwitschi/linearvoid.git cd linearvoid -
Copy Environment File:
cp .env.example .env -
Configure
.env(edit with your values):# Cloudflare R2 (required for video storage and $0 egress) R2_ACCESS_KEY_ID=your-r2-access-key R2_SECRET_ACCESS_KEY=your-r2-secret-key R2_BUCKET_NAME=linearvoid-media R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com R2_REGION=auto # Cloudflare (for custom domain and cache rules) CLOUDFLARE_API_TOKEN=your-cloudflare-api-token CLOUDFLARE_ACCOUNT_ID=your-account-id MEDIA_DOMAIN=media.linearvoid.tv # Public frontend URLs # Preferred local setup uses the real public domains through Cloudflare and Nginx Proxy Manager. 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 # Optional direct-local fallback when bypassing the public proxy: # VITE_PUBLIC_APP_URL=http://localhost # VITE_PUBLIC_WS_URL=ws://localhost/ws # VITE_PUBLIC_HLS_URL=http://localhost/hls/playlist.m3u8 # Gitea Container Registry (for self-hosted Docker images) GITEA_REGISTRY_URL=git.allucanget.biz GITEA_USERNAME=your-gitea-username GITEA_TOKEN=your-gitea-token GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid # Chat / Database DATABASE_URL=postgres://linearvoid:linearvoid@db:5432/linearvoid?sslmode=disable # or for SQLite-only local development: # DATABASE_URL=sqlite://./data/chat.db REDIS_URL=redis://redis:6379/0 # Ports (for local development) PLAYOUT_PORT=8080 CHAT_PORT=3000 NGINX_PORT=80 # Playout Configuration PLAYLIST_PATH=/app/playlist.txt HLS_SEGMENT_DURATION=6 HLS_WINDOW_SIZE=24 # Logging / Monitoring LOG_LEVEL=info PROMETHEUS_PORT=9090 GRAFANA_PORT=3001 # Development flags NODE_ENV=development GO_ENV=development # Gitea API access (optional, for CI/release automation) GITEA_SERVER_URL=https://git.allucanget.biz GITEA_TOKEN=your-gitea-token GITEA_USERNAME=your-gitea-username -
Check local value overrides for Docker Compose:
docker-compose.ymlbuilds images locally and maps the frontend build output fromfrontend/distinto nginx.docker-compose.prod.ymlpulls prebuilt images from the Gitea registry.- Internal container-to-container URLs should use compose hostnames, for example
REDIS_URL=redis://redis:6379/0andDATABASE_URL=postgres://linearvoid:<password>@db:5432/linearvoid?sslmode=disable. - For production public routing, set
VITE_PUBLIC_APP_URL=https://linearvoid.tv,VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws, andVITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8.
-
Create Data Directories:
mkdir -p data/playlist data/media data/hls data/chat -
Prepare Sample Videos (optional):
- Place normalized MP4 files in
data/media/. - Create a
data/playlist/playlist.txtwith paths (see ADR-001 for normalization requirements).
- Place normalized MP4 files in
Cloudflare R2 Setup
-
Create an R2 bucket:
- Create a bucket such as
linearvoid-mediain Cloudflare R2. - Keep the bucket name aligned with
R2_BUCKET_NAMEin.env.
- Create a bucket such as
-
Generate R2 API credentials:
- Create an access key pair with read/write permissions for the bucket.
- Set
R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY, andR2_REGION=autoin.env.
-
Find the S3 endpoint:
- Use the account-scoped endpoint format
https://<account-id>.r2.cloudflarestorage.com. - Store the account ID in both
R2_ENDPOINTandCLOUDFLARE_ACCOUNT_IDwhere applicable.
- Use the account-scoped endpoint format
-
Bind a custom media domain:
- Attach a domain such as
media.linearvoid.tvto the bucket. - Set
MEDIA_DOMAINto that hostname.
- Attach a domain such as
-
Configure app-domain DNS and TLS:
- Point
linearvoid.tvto the public entrypoint serving nginx. - Point
www.linearvoid.tvto the same target and redirect it tohttps://linearvoid.tv. - Keep
media.linearvoid.tvon the R2 custom domain path so HLS delivery stays split from the app origin. - Use Cloudflare proxied DNS with Full (strict) TLS so browser traffic uses
https://linearvoid.tv,wss://linearvoid.tv/ws, andhttps://media.linearvoid.tv.
- Point
-
Apply cache behavior:
- Configure the
.tsand.m3u8cache rules frominfrastructure/cloudflare/cache-rules.md. - Enable Smart Tiered Cache for the R2-backed hostname.
- If you need header normalization for immutable segment caching, deploy
infrastructure/cloudflare/worker.jsto the same zone.
- Configure the
-
Verify access:
- Confirm uploads with an S3-compatible client such as
aws s3 ls --endpoint-url <R2_ENDPOINT>. - After deployment, check Cloudflare analytics for cache hit ratio and origin request volume.
- Confirm uploads with an S3-compatible client such as
Gitea Setup
-
Create or verify a Gitea user:
- The user should have access to the repository and permission to read from the container registry.
-
Create an access token:
- Generate a token with the scopes needed for package/registry access and, if applicable, repository automation.
- Store it as
GITEA_TOKENin.env.
-
Configure registry variables:
- Set
GITEA_REGISTRY_URLto the Gitea host. - Set
GITEA_REGISTRY_NAMESPACEto the image namespace, for examplezwitschi/linearvoid. - Set
GITEA_USERNAMEto the account that will push or pull images.
- Set
-
Authenticate Docker to the registry:
docker login git.allucanget.biz -
Build and publish images when needed:
- Local development uses
docker-compose.yml, which builds from source. - Production uses
docker-compose.prod.yml, which expects theplayout,chat, andnginximages to exist in the configured Gitea registry namespace.
- Local development uses
-
Validate image availability:
- Before a production rollout, verify that the expected
:latesttags can be pulled from the registry on the target server.
- Before a production rollout, verify that the expected
Running the Application
Local Development
-
Start All Services:
docker compose up --build -
Access the Application:
- Preferred local access with the documented proxy setup: https://linearvoid.tv
- Admin Interface: https://linearvoid.tv/admin
- Chat WebSocket:
wss://linearvoid.tv/ws - Direct-local fallback when bypassing Cloudflare/Nginx Proxy Manager: http://localhost, http://localhost/admin, and
ws://localhost/ws - Health Checks:
- Playout: http://localhost:8081/health
- Chat: http://localhost:3000/health
- Monitoring:
- Prometheus: http://localhost:9090
- Grafana: http://localhost:3001 (default login: admin/admin)
-
Frontend Development (optional, for UI changes):
cd frontend npm install npm run devThe dev server runs on http://localhost:5173. It proxies API calls to the backend services.
-
Build frontend assets for nginx-backed local runs:
cd frontend npm install npm run buildThe local nginx container serves
frontend/dist, so rebuilding the frontend is required before validating production-like static output. -
Stop Services:
docker compose down
Production Setup Guide
-
Prepare the target host:
- Provision a Linux VPS with Docker Engine, Docker Compose, Git, and enough disk for media, HLS output, PostgreSQL, Redis persistence, and monitoring data.
- Open the ports required for HTTP/HTTPS and any restricted monitoring access you intend to expose.
-
Clone the repository and create the environment file:
git clone https://git.allucanget.biz/zwitschi/linearvoid.git cd linearvoid cp .env.example .env -
Set production-safe
.envvalues:- Replace placeholder secrets.
- Use compose-network hostnames for internal services, for example
redisanddb. - Set strong values for
POSTGRES_PASSWORDandGRAFANA_PASSWORD. - Keep
MEDIA_DOMAIN, R2 credentials, and Gitea registry values consistent with the external infrastructure. - Set
MEDIA_DOMAIN=media.linearvoid.tv. - Set
VITE_PUBLIC_APP_URL=https://linearvoid.tv,VITE_PUBLIC_WS_URL=wss://linearvoid.tv/ws, andVITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8. - If you intentionally bypass the public proxy for local-only testing, switch those values to
http://localhost,ws://localhost/ws, andhttp://localhost/hls/playlist.m3u8.
-
Create runtime directories on the server:
mkdir -p data/playlist data/media data/hls data/chat -
Publish or verify required images in Gitea:
- Ensure the
playout,chat, andnginximages exist under${GITEA_REGISTRY_URL}/${GITEA_REGISTRY_NAMESPACE}. - Log in to the registry from the VPS before pulling.
- Ensure the
-
Deploy the production stack:
docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d -
Run post-deploy verification:
- Check
docker compose -f docker-compose.prod.yml ps. - Confirm nginx, chat, and playout health endpoints respond.
- Verify that HLS output is being written to
data/hlsand that the media domain serves manifests and segments through Cloudflare.
- Check
-
Harden and operate the deployment:
- Put HTTPS in front of nginx using Cloudflare or another reverse proxy, with
linearvoid.tvas canonical app host andwww.linearvoid.tvredirected. - Restrict Grafana and Prometheus exposure to trusted IPs or a private network.
- Run the stack under systemd or another service manager if you need auto-start behavior outside Docker restart policies.
- Put HTTPS in front of nginx using Cloudflare or another reverse proxy, with
Production Deployment (Hetzner VPS + Cloudflare)
- Provision VPS: Use Hetzner Cloud CX22 in Ashburn, VA (per ADR-001).
- Configure Cloudflare:
- Create R2 bucket with
enamlocation hint. - Set up custom media domain (
media.linearvoid.tv) with the cache rules frominfrastructure/cloudflare/cache-rules.md. - Point
linearvoid.tvandwww.linearvoid.tvat the app entrypoint, then redirectwwwto the apex domain. - Use Full (strict) TLS for both app and media hostnames.
- Apply the Worker from
infrastructure/cloudflare/worker.jsfor advanced header management.
- Create R2 bucket with
- Deploy:
- Copy the project to the VPS.
- Use production compose:
docker compose -f docker-compose.prod.yml up -d - Or pull from Gitea registry:
docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d - Configure systemd to manage the Docker Compose stack.
- Monitoring: Access Grafana at the exposed port for dashboards (included in
monitoring/grafana/dashboard.json).
See docs/architecture/README.md for the full arc42 documentation and the ADRs for architectural decisions.
Troubleshooting
- R2 Upload Fails: Check credentials in
.envand ensure the bucket exists. Verify withaws s3 ls --endpoint-url=$R2_ENDPOINT. - Normalization Errors: Run
backend/playout/start-playout.shmanually to see ffprobe output. All videos must be H.264/AAC, 30 fps constant, 2s keyframes (ADR-001). - Chat Not Connecting: Check Redis connection and WebSocket URL. The health check at
/healthshould return 200. - Cache Not Working: Verify Cloudflare cache rules (
.ts= long TTL immutable,.m3u8= short TTL). Check cache hit ratio in Cloudflare dashboard. - Docker Issues: Run
docker compose logsfor service-specific errors. Ensure ports are not in use. - Admin Interface: Ensure the protected route is configured in nginx and authentication is set up (default is open for local development).
- Production Pull Fails: Confirm
docker loginto Gitea succeeded on the server and the image tags referenced bydocker-compose.prod.ymlexist.
Further Reading
- Architecture:
docs/architecture/README.md(arc42 template with diagrams). - Decisions:
docs/architecture/adr/(ADR-001 to ADR-007). - Contributing:
CONTRIBUTING.md. - Project Structure: Root
README.md.
The system is ready for use. For production, review ADR-001 for cost estimates and scaling guidance.
Last Updated: 2026-08-03