# 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)(`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](https://www.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. - Set up the required Actions secrets in Gitea at: [https://git.allucanget.biz/zwitschi/linearvoid/settings/actions/secrets](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:@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. ```mermaid flowchart LR subgraph Test_Lab["Test Lab (192.168.88.0/24)"] TEST_HOST["192.168.88.40
Docker Engine :2375"] end subgraph Prod["Production (Hetzner — NOT provisioned)"] PROD_HOST["Hetzner CX22 Ashburn, VA
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 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](./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): ```bash git clone https://git.allucanget.biz/zwitschi/linearvoid.git cd linearvoid ``` 2. **Copy Environment File**: ```bash cp .env.example .env ``` 3. **Configure `.env`** (edit with your values): ```env # 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 VITE_PUBLIC_ADMIN_DEFAULT_MODERATOR=admin VITE_PUBLIC_ADMIN_DEFAULT_ROOM=main VITE_PUBLIC_ADMIN_MODERATION_LOG_LIMIT=50 ADMIN_USERNAME=admin ADMIN_PASSWORD=change-me ADMIN_SESSION_TTL_HOURS=12 # 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) CI_REGISTRY_URL=git.allucanget.biz CI_REGISTRY_USER=your-gitea-username CI_REGISTRY_TOKEN=your-gitea-token CI_REGISTRY_NAMESPACE=zwitschi/linearvoid # Gitea API access (optional, for CI/release automation) CI_SERVER_URL=https://git.allucanget.biz # 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 ``` 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:@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**: ```bash 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). ## 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://.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 `.ts` and `.m3u8` cache rules from `infrastructure/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.js` to the same zone. 7. **Verify access**: - Confirm uploads with an S3-compatible client such as `aws s3 ls --endpoint-url `. - After deployment, check Cloudflare analytics for cache hit ratio and origin request volume. ## 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](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:@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**: ```bash 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. ## Running the Application ### Local Development 1. **Start All Services**: ```bash docker compose up --build ``` 2. **Access the Application**: - Preferred local access with the documented proxy setup: - Admin Interface: (redirects to `/admin/login/` until authenticated) - Chat WebSocket: `wss://linearvoid.tv/ws` - Direct-local fallback when bypassing Cloudflare/Nginx Proxy Manager: , , and `ws://localhost/ws` - Health Checks: - Playout: - Chat: - Monitoring: - Prometheus: - Grafana: (default login: admin/admin) 3. **Frontend Development** (optional, for UI changes): ```bash cd frontend npm install npm run dev ``` The dev server runs on . It proxies API calls to the backend services. 4. **Build frontend assets for nginx-backed local runs**: ```bash 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**: ```bash 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: ```bash vi /etc/conf.d/docker ``` 2. Update the `DOCKER_OPTS` line to include both the unix socket and the TCP socket: ```bash DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375" ``` 3. Restart the Docker service: ```bash rc-service docker restart ``` 4. Verify that Docker is listening on port `2375`: ```bash 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)**: ```powershell $env:DOCKER_HOST="tcp://192.168.88.40:2375" ``` - **Bash (Linux/macOS)**: ```bash 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`: ```bash 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)**: ```bash docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d --remove-orphans ``` - **Build and Deploy from Source**: ```bash 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**: ```powershell Remove-Item env:DOCKER_HOST ``` - **Bash**: ```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**: ```bash 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**: ```bash 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**: ```bash 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 `infrastructure/cloudflare/cache-rules.md`. - 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 (`.ts` = 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. **Last Updated**: 2026-08-03