Files
linearvoid/docs/SETUP.md
T
zwitschi 9e56ff7859
CI / test (push) Successful in 2m55s
CI / docker (push) Successful in 46s
feat(ops): replace Cloudflare Pages with Workers for frontend hosting
2026-08-16 08:17:23 +02:00

22 KiB

Linear Void Setup Guide

Last Updated: 2026-08-03

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 22+: 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.25+: 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.
    • Set up the required Actions secrets in Gitea at: https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets
    • Add the following secrets:
      • CI_REGISTRY_USER: Your Gitea username (e.g., zwitschi)
      • CI_REGISTRY_TOKEN: A Gitea Personal Access Token with write:packages scope
      • ADMIN_USERNAME: The administrator username for the production admin panel (e.g., admin)
      • ADMIN_PASSWORD: A strong password for the production admin panel
      • DATABASE_URL: The production database connection string (e.g., postgres://linearvoid:<password>@db:5432/linearvoid?sslmode=disable)
  • Hetzner Cloud Account (optional for production): For the US-East VPS (Ashburn, VA recommended per ADR-001).

Network Environments

Linear Void operates in two distinct environments. Know which one you're targeting.

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://&lt;hetzner-ip&gt;:2375<br/>(when provisioned)"| PROD_HOST
    TEST_HOST -->|"test traffic"| DNS
    PROD_HOST -.->|"production traffic"| DNS
    TEST_HOST --> R2
    PROD_HOST -.-> R2

Test Lab (active): Internal network 192.168.88.0/24, host 192.168.88.40. Receives automated CD deploys from Gitea Actions. Used for integration testing and staging validation.

Production (pending): Hetzner Cloud VPS, Ashburn VA. Not yet provisioned. CD workflow and compose files are ready — only DOCKER_HOST needs changing. See Production Deployment section in DEPLOYMENT.md for cutover plan.

Development Prerequisites

  • Frontend dependencies: Run cd frontend && npm install before npm run dev or npm run build.
  • Docker build support: Local development expects Docker to be able to build the chat, playout, and nginx images referenced in docker-compose.yml.
  • Data directories: Create data/playlist, data/media, data/hls, and data/chat before 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, update DATABASE_URL in .env accordingly.
  • 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

  1. Clone the Repository (if not already done):

    git clone https://git.allucanget.biz/zwitschi/linearvoid.git
    cd linearvoid
    
  2. Copy Environment File:

    cp .env.example .env
    
  3. Configure .env (copy from .env.example and replace placeholders)

  4. Check local value overrides for Docker Compose:

    • docker-compose.yml builds images locally and maps the frontend build output from frontend/dist into nginx.
    • docker-compose.prod.yml pulls prebuilt images from the Gitea registry.
    • Internal container-to-container URLs should use compose hostnames, for example REDIS_URL=redis://redis:6379/0 and DATABASE_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, and VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8.
  5. Create Data Directories:

    mkdir -p data/playlist data/media data/hls data/chat
    
  6. Prepare Sample Videos (optional):

    • Place normalized MP4 files in data/media/.
    • Create a data/playlist/playlist.txt with paths (see ADR-001 for normalization requirements).

Gitea Setup

  1. Create or verify a Gitea user:

    • The user should have access to the repository and permission to read from the container registry.
  2. Create an access token:

    • Generate a token with the scopes needed for package/registry access and, if applicable, repository automation.
    • Store it as CI_REGISTRY_TOKEN in .env.
  3. Configure Gitea Actions Secrets:

    • Go to the repository settings page: https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets
    • Add the following secrets to enable automated CI/CD and production deployment:
      • CI_REGISTRY_USER: Your Gitea username (e.g., zwitschi).
      • CI_REGISTRY_TOKEN: Your Gitea Personal Access Token with write:packages scope.
      • ADMIN_USERNAME: The administrator username for the production admin panel (e.g., admin).
      • ADMIN_PASSWORD: A strong password for the production admin panel.
      • DATABASE_URL: The production database connection string (e.g., postgres://linearvoid:<password>@db:5432/linearvoid?sslmode=disable).
  4. Configure registry variables:

    • Set CI_REGISTRY_URL to the Gitea host.
    • Set CI_REGISTRY_NAMESPACE to the image namespace, for example zwitschi/linearvoid.
    • Set CI_REGISTRY_USER to the account that will push or pull images.
  5. Authenticate Docker to the registry:

    docker login git.allucanget.biz
    
  6. Build and publish images when needed:

    • Local development uses docker-compose.yml, which builds from source.
    • Production uses docker-compose.prod.yml, which expects the playout, chat, and nginx images to exist in the configured Gitea registry namespace.
  7. Validate image availability:

    • Before a production rollout, verify that the expected :latest tags can be pulled from the registry on the target server.

Cloudflare R2 Setup

  1. Create an R2 bucket:

    • Create a bucket such as linearvoid-media in Cloudflare R2.
    • Keep the bucket name aligned with R2_BUCKET_NAME in .env.
  2. 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, and R2_REGION=auto in .env.
  3. Find the S3 endpoint:

    • Use the account-scoped endpoint format https://<account-id>.r2.cloudflarestorage.com.
    • Store the account ID in both R2_ENDPOINT and CLOUDFLARE_ACCOUNT_ID where applicable.
  4. Bind a custom media domain:

    • Attach a domain such as media.linearvoid.tv to the bucket.
    • Set MEDIA_DOMAIN to that hostname.
  5. Configure app-domain DNS and TLS:

    • Point linearvoid.tv to the public entrypoint serving nginx.
    • Point www.linearvoid.tv to the same target and redirect it to https://linearvoid.tv.
    • Keep media.linearvoid.tv on 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, and https://media.linearvoid.tv.
  6. Apply cache behavior:

    • Configure the .m4s and .m3u8 cache rules from cache-rules.
    • Enable Smart Tiered Cache for the R2-backed hostname.
    • If you need header normalization for immutable segment caching, deploy infrastructure/cloudflare/worker.js to the same zone.
  7. Configure R2 CORS:

    • Add CORS rules to allow the app domain to access the R2 bucket for HLS delivery.
    • Example CORS configuration:
    [
      {
        "AllowedOrigins": ["https://linearvoid.tv"],
        "AllowedMethods": ["GET"],
        "AllowedHeaders": ["*"],
        "ExposeHeaders": ["ETag", "Cache-Control"],
        "MaxAgeSeconds": 3600
      }
    ]
    
  8. 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.

Cloudflare

Use this section for full edge setup (DNS, Workers, cache, security, and monitoring).

Execution tracker: docs/operations/cloudflare-uptimerobot-checklist.md.

1. DNS Setup

  1. Add apex record for linearvoid.tv pointing to the Cloudflare Worker.
  2. Add www CNAME to the Cloudflare Worker and configure redirect to https://linearvoid.tv.
  3. Add media.linearvoid.tv for split-domain HLS delivery.
  4. Keep all records proxied (orange cloud) unless troubleshooting requires DNS-only mode.

2. R2 Bucket

  1. Create bucket linearvoid-media.
  2. Set location hint to enam (US-East).
  3. Attach custom domain media.linearvoid.tv to the bucket.
  4. Configure CORS rules to allow https://linearvoid.tv for HLS delivery.
  5. Verify object read works from the custom domain.

3. Cache Rules

Configure Cloudflare Cache Rules:

  1. HLS segment files (*.ts, *.m4s):
    • Cache: eligible
    • Edge TTL: 31536000
    • Browser TTL: long-lived
    • Cache key: include full path
  2. HLS manifests (*.m3u8):
    • Cache: eligible
    • Edge TTL: 2s
    • Revalidation: enabled
  3. Static assets (*.js, *.css):
    • Cache: eligible
    • Edge TTL: 1y
    • Browser TTL: long-lived
    • Cache key: include full path
  4. Validate behavior using response headers:
    • Cf-Cache-Status should move from MISS to HIT for hot objects

Reference: cache-rules.

4. Cloudflare Workers

Also see the automated Gitea Actions deployment guide.

Manual setup:

  1. Create a Cloudflare Worker for the static SPA (Workers & Pages → Create → Worker).

  2. Build the frontend:

    cd frontend && npm run build
    
  3. Bind the build output (frontend/dist) as Worker static assets.

  4. Configure production domain linearvoid.tv on the Worker.

5. Tiered Cache

  1. Enable Smart Tiered Cache in Cloudflare dashboard.
  2. Warm a segment and manifest path.
  3. Confirm tiered behavior through repeated requests and Cf-Cache-Status: HIT.

6. WAF and Bot Protection

  1. Enable OWASP Managed Ruleset.
  2. Start in Simulate mode.
  3. Review events for 48 hours.
  4. Switch to Block once false positives are addressed.
  5. Enable Bot Fight Mode.

7. Rate Limiting

Create rules:

  1. Path: /api/admin/login
  2. Path: /api/admin/*
  3. Threshold: 10 requests / 10 seconds / IP
  4. Action: Managed Challenge

8. HTTPS and WSS

  1. Enable Always Use HTTPS.
  2. Enable HSTS with:
    • max-age=31536000
    • includeSubDomains
    • preload
  3. Minimum TLS version: 1.2.
  4. Keep SSL mode at Full (strict) when origin certs are valid.
  5. Enforce HTTPS redirect at both layers:
    • Cloudflare edge
    • nginx origin
  6. Validate websocket endpoint uses wss://linearvoid.tv/ws.

9. UptimeRobot Monitors

Create external monitors:

  1. HTTP monitor:
    • URL: https://linearvoid.tv/health
    • Interval: 5 minutes
  2. Keyword monitor:
    • URL: https://linearvoid.tv/hls/playlist.m3u8
    • Keyword: #EXTM3U
    • Interval: 5 minutes
  3. Alert policy:
    • Trigger on 2 consecutive failures
    • Notify via email and Discord/Slack webhook

10. Verification Checklist

  1. DNS resolves for linearvoid.tv, www.linearvoid.tv, and media.linearvoid.tv.
  2. Workers serve frontend for apex and www redirects correctly.
  3. HLS segments are cacheable long-term, manifests refresh every ~2s.
  4. Admin paths are rate-limited and WAF events are visible.
  5. HTTPS and WSS both succeed with valid certificates.
  6. UptimeRobot alerts fire as expected when endpoints are intentionally made unavailable.

Running the Application

Local Development

  1. Start All Services:

    docker compose up --build
    
  2. Access the Application:

  3. Frontend Development (optional, for UI changes):

    cd frontend
    npm install
    npm run dev
    

    The dev server runs on http://localhost:5173. It proxies API calls to the backend services.

  4. Build frontend assets for nginx-backed local runs:

    cd frontend
    npm install
    npm run build
    

    The local nginx container serves frontend/dist, so rebuilding the frontend is required before validating production-like static output.

  5. Stop Services:

    docker compose down
    

Local Test Environment Deployment (Remote Docker Host)

For testing deployments on a local network server without setting up a full production VPS, you can deploy directly to a remote Docker host (e.g., a home lab server or test machine) using Docker's TCP socket.

Test Environment Prerequisites

  • A remote server running Docker at IP 192.168.88.40 (running Alpine Linux).
  • Docker TCP socket enabled and listening on port 2375 on the remote server.
Enabling Docker TCP Socket on Alpine Linux

To allow the Gitea Actions runner (or your local machine) to deploy to the remote Alpine host, you must configure the Docker daemon to listen on the TCP socket:

  1. Edit /etc/conf.d/docker on the Alpine host:

    vi /etc/conf.d/docker
    
  2. Update the DOCKER_OPTS line to include both the unix socket and the TCP socket:

    DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375"
    
  3. Restart the Docker service:

    rc-service docker restart
    
  4. Verify that Docker is listening on port 2375:

    netstat -tlnp | grep 2375
    
  5. Ensure any firewall (like awall or iptables) on the Alpine host allows incoming traffic on port 2375 from your local network subnet (e.g., 192.168.88.0/24).

Deployment Steps

  1. Configure Docker CLI to use the Remote Host: You can point your local Docker CLI to the remote server by setting the DOCKER_HOST environment variable:

    • PowerShell (Windows):

      $env:DOCKER_HOST="tcp://192.168.88.40:2375"
      
    • Bash (Linux/macOS):

      export DOCKER_HOST="tcp://192.168.88.40:2375"
      
  2. Verify Connection: Run docker info to confirm your local CLI is communicating with the remote server at 192.168.88.40:

    docker info
    

    The output should show the remote server's hostname and system details.

  3. Deploy the Stack: With DOCKER_HOST set, any docker or docker compose commands you run locally will execute directly on the remote server.

    • Deploy from Registry (Production-like):

      docker compose -f docker-compose.prod.yml pull
      docker compose -f docker-compose.prod.yml up -d --remove-orphans
      
    • Build and Deploy from Source:

      docker compose up -d --build
      
  4. Access the Test Environment:

    • Playout Health: http://192.168.88.40:8081/health
    • Chat Health: http://192.168.88.40:3000/health
    • Nginx Entrypoint: http://192.168.88.40:8080 (or the port mapped to NGINX_PORT in .env)
  5. Reset to Local Docker Host: Once testing is complete, clear the DOCKER_HOST variable to point back to your local machine:

    • PowerShell:

      Remove-Item env:DOCKER_HOST
      
    • Bash:

      unset DOCKER_HOST
      

Production Setup Guide

  1. 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.
  2. Clone the repository and create the environment file:

    git clone https://git.allucanget.biz/zwitschi/linearvoid.git
    cd linearvoid
    cp .env.example .env
    
  3. Set production-safe .env values:

    • Replace placeholder secrets.
    • Use compose-network hostnames for internal services, for example redis and db.
    • Set strong values for POSTGRES_PASSWORD and GRAFANA_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, and VITE_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, and http://localhost/hls/playlist.m3u8.
  4. Create runtime directories on the server:

    mkdir -p data/playlist data/media data/hls data/chat
    
  5. Publish or verify required images in Gitea:

    • Ensure the playout, chat, and nginx images exist under ${CI_REGISTRY_URL}/${CI_REGISTRY_NAMESPACE}.
    • Log in to the registry from the VPS before pulling.
  6. Deploy the production stack:

    docker compose -f docker-compose.prod.yml pull
    docker compose -f docker-compose.prod.yml up -d
    
  7. 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/hls and that the media domain serves manifests and segments through Cloudflare.
  8. Harden and operate the deployment:

    • Put HTTPS in front of nginx using Cloudflare or another reverse proxy, with linearvoid.tv as canonical app host and www.linearvoid.tv redirected.
    • 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.

Production Deployment (Hetzner VPS + Cloudflare)

  1. Provision VPS: Use Hetzner Cloud CX22 in Ashburn, VA (per ADR-001).
  2. Configure Cloudflare:
    • Create R2 bucket with enam location hint.
    • Set up custom media domain (media.linearvoid.tv) with the cache rules from cache-rules.
    • Point linearvoid.tv and www.linearvoid.tv at the app entrypoint, then redirect www to the apex domain.
    • Use Full (strict) TLS for both app and media hostnames.
    • Apply the Worker from infrastructure/cloudflare/worker.js for advanced header management.
  3. 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.
  4. 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 .env and ensure the bucket exists. Verify with aws s3 ls --endpoint-url=$R2_ENDPOINT.
  • Normalization Errors: Run backend/playout/start-playout.sh manually 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 /health should return 200.
  • Cache Not Working: Verify Cloudflare cache rules (.m4s = long TTL immutable, .m3u8 = short TTL). Check cache hit ratio in Cloudflare dashboard.
  • Docker Issues: Run docker compose logs for service-specific errors. Ensure ports are not in use.
  • Admin Interface: Ensure ADMIN_USERNAME, ADMIN_PASSWORD, and ADMIN_SESSION_TTL_HOURS are set. /admin/ and /api/admin/* are protected by a session-based login flow.
  • Production Pull Fails: Confirm docker login to Gitea succeeded on the server and the image tags referenced by docker-compose.prod.yml exist.

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.