update environment configuration, enhance setup guide, and align documentation with new domain structure
This commit is contained in:
1 parent
c484b463e3
commit
2e061dff28
13 files changed
+267
-46
No files matched your search
+11
-2
@@ -12,7 +12,16 @@ R2_REGION=auto
|
|||||||
# Cloudflare (for custom domain, cache rules, Workers if used)
|
# Cloudflare (for custom domain, cache rules, Workers if used)
|
||||||
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
||||||
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
||||||
MEDIA_DOMAIN=media.example.com
|
MEDIA_DOMAIN=media.linearvoid.tv
|
||||||
|
|
||||||
|
# Public frontend URLs (Vite reads these from the repository root via frontend/vite.config.ts)
|
||||||
|
# Local defaults keep development on localhost. Production should use:
|
||||||
|
# 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_APP_URL=http://localhost:8080
|
||||||
|
VITE_PUBLIC_WS_URL=ws://localhost:3000/ws
|
||||||
|
VITE_PUBLIC_HLS_URL=http://localhost:8080/hls/playlist.m3u8
|
||||||
|
|
||||||
# Gitea Container Registry (for self-hosted Docker images)
|
# Gitea Container Registry (for self-hosted Docker images)
|
||||||
GITEA_REGISTRY_URL=git.allucanget.biz
|
GITEA_REGISTRY_URL=git.allucanget.biz
|
||||||
@@ -34,7 +43,7 @@ CHAT_PORT=3000
|
|||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|
||||||
# Playout configuration
|
# Playout configuration
|
||||||
PLAYLIST_PATH=/app/playlist.txt
|
PLAYLIST_PATH=/app/playlist/playlist.txt
|
||||||
HLS_SEGMENT_DURATION=6
|
HLS_SEGMENT_DURATION=6
|
||||||
HLS_WINDOW_SIZE=24
|
HLS_WINDOW_SIZE=24
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -1,8 +1,17 @@
|
|||||||
# Change Log for linearvoid
|
# Change Log for linearvoid
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-08-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Expanded the setup guide with current `.env.example` variables, development prerequisites, Cloudflare R2 setup, Gitea registry setup, and a production deployment guide.
|
||||||
|
- Aligned frontend, nginx, and deployment docs with the `linearvoid.tv` app domain and `media.linearvoid.tv` HLS domain model.
|
||||||
|
|
||||||
## [1.0.0] - 2026-08-02
|
## [1.0.0] - 2026-08-02
|
||||||
|
|
||||||
**Full Backend and Frontend Implementation Complete**
|
### Added
|
||||||
|
|
||||||
|
Full backend and frontend implementation complete.
|
||||||
|
|
||||||
- **Playout Service** (`backend/playout/`): Go-based playlist management, ffprobe normalization validation, continuous HLS with immutable segment naming, automatic R2 sync, health check endpoint, and Docker integration (ADR-001, ADR-003).
|
- **Playout Service** (`backend/playout/`): Go-based playlist management, ffprobe normalization validation, continuous HLS with immutable segment naming, automatic R2 sync, health check endpoint, and Docker integration (ADR-001, ADR-003).
|
||||||
- **Chat Backend** (`backend/chat/`): Go + Gorilla WebSocket, Redis for presence/rate limiting/pub/sub, SQLite persistence for message history and ModerationLog (ADR-006/ADR-007), public rooms, private DM routing, moderation commands with immutable audit logging, health check endpoint, and docker-compose integration.
|
- **Chat Backend** (`backend/chat/`): Go + Gorilla WebSocket, Redis for presence/rate limiting/pub/sub, SQLite persistence for message history and ModerationLog (ADR-006/ADR-007), public rooms, private DM routing, moderation commands with immutable audit logging, health check endpoint, and docker-compose integration.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Linear Void
|
# Linear Void
|
||||||
|
|
||||||
A self-managed **24/7 linear video channel** for a primarily US audience. It runs a continuous playlist of self-owned videos with true/near-true synchronization, a custom lightweight viewer SPA (hls.js player + chat), and full independence from third-party platforms (YouTube, Twitch, etc.).
|
[Linear Void](https://linearvoid.tv) is a self-managed **24/7 linear video channel** for a primarily US audience. It runs a continuous playlist of self-owned videos with true/near-true synchronization, a custom lightweight viewer SPA (hls.js player + chat), and full independence from third-party platforms (YouTube, Twitch, etc.).
|
||||||
|
|
||||||
**Repository**: [git.allucanget.biz/zwitschi/linearvoid](https://git.allucanget.biz/zwitschi/linearvoid)
|
**Repository**: [git.allucanget.biz/zwitschi/linearvoid](https://git.allucanget.biz/zwitschi/linearvoid)
|
||||||
|
|
||||||
@@ -107,6 +107,7 @@ See the [architecture docs](docs/architecture/README.md) for detailed architectu
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
- Use Docker Compose for local development (playout + chat + frontend).
|
- Use Docker Compose for local development (playout + chat + frontend).
|
||||||
|
- Public production host model: `https://linearvoid.tv` for viewer/admin/chat and `https://media.linearvoid.tv` for HLS delivery.
|
||||||
- Normalize all source videos before adding to playlist (see ADR-001 for exact codec/fps/keyframe requirements).
|
- Normalize all source videos before adding to playlist (see ADR-001 for exact codec/fps/keyframe requirements).
|
||||||
- Monitor cache hit ratio and R2 operations closely — they determine real cost.
|
- Monitor cache hit ratio and R2 operations closely — they determine real cost.
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,24 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
## Prerequisites
|
## 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).
|
- **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+**: For frontend development (`cd frontend && npm run dev`).
|
- **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).
|
- **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.
|
- **Git**: For cloning and version control.
|
||||||
- **Cloudflare Account**: For R2 storage and CDN (free tier sufficient for most use cases).
|
- **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).
|
- **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 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
|
## Environment Setup
|
||||||
|
|
||||||
1. **Clone the Repository** (if not already done):
|
1. **Clone the Repository** (if not already done):
|
||||||
@@ -34,10 +46,17 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
R2_SECRET_ACCESS_KEY=your-r2-secret-key
|
R2_SECRET_ACCESS_KEY=your-r2-secret-key
|
||||||
R2_BUCKET_NAME=linearvoid-media
|
R2_BUCKET_NAME=linearvoid-media
|
||||||
R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
|
R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
|
||||||
|
R2_REGION=auto
|
||||||
|
|
||||||
# Cloudflare (for custom domain and cache rules)
|
# Cloudflare (for custom domain and cache rules)
|
||||||
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
|
||||||
MEDIA_DOMAIN=media.example.com
|
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
||||||
|
MEDIA_DOMAIN=media.linearvoid.tv
|
||||||
|
|
||||||
|
# Public frontend URLs
|
||||||
|
VITE_PUBLIC_APP_URL=http://localhost:8080
|
||||||
|
VITE_PUBLIC_WS_URL=ws://localhost:3000/ws
|
||||||
|
VITE_PUBLIC_HLS_URL=http://localhost:8080/hls/playlist.m3u8
|
||||||
|
|
||||||
# Gitea Container Registry (for self-hosted Docker images)
|
# Gitea Container Registry (for self-hosted Docker images)
|
||||||
GITEA_REGISTRY_URL=git.allucanget.biz
|
GITEA_REGISTRY_URL=git.allucanget.biz
|
||||||
@@ -46,33 +65,112 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid
|
GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid
|
||||||
|
|
||||||
# Chat / Database
|
# Chat / Database
|
||||||
REDIS_URL=redis://redis:6379/0
|
DATABASE_URL=postgres://user:password@localhost:5432/linearvoid
|
||||||
DATABASE_URL=sqlite://./data/chat.db # or postgres://user:pass@db:5432/linearvoid
|
# or for SQLite in local development:
|
||||||
|
# DATABASE_URL=sqlite://./data/chat.db
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
# Ports (for local development)
|
# Ports (for local development)
|
||||||
|
PLAYOUT_PORT=8080
|
||||||
CHAT_PORT=3000
|
CHAT_PORT=3000
|
||||||
NGINX_PORT=8080
|
NGINX_PORT=80
|
||||||
PLAYOUT_HEALTH_PORT=8081
|
|
||||||
|
|
||||||
# Playout Configuration
|
# Playout Configuration
|
||||||
PLAYLIST_PATH=/app/playlist/playlist.txt
|
PLAYLIST_PATH=/app/playlist.txt
|
||||||
HLS_SEGMENT_DURATION=6
|
HLS_SEGMENT_DURATION=6
|
||||||
HLS_WINDOW_SIZE=24
|
HLS_WINDOW_SIZE=24
|
||||||
|
|
||||||
# Logging
|
# Logging / Monitoring
|
||||||
LOG_LEVEL=info
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Create Data Directories**:
|
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.
|
||||||
|
- For production on the VPS, change hostnames in `.env` so containers talk to each other over the compose network, for example `REDIS_URL=redis://redis:6379/0` and `DATABASE_URL=postgres://linearvoid:<password>@db:5432/linearvoid`.
|
||||||
|
- 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
|
```bash
|
||||||
mkdir -p data/playlist data/media data/hls data/chat
|
mkdir -p data/playlist data/media data/hls data/chat
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Prepare Sample Videos** (optional):
|
6. **Prepare Sample Videos** (optional):
|
||||||
- Place normalized MP4 files in `data/media/`.
|
- Place normalized MP4 files in `data/media/`.
|
||||||
- Create a `data/playlist/playlist.txt` with paths (see ADR-001 for normalization requirements).
|
- 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://<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 `.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 <R2_ENDPOINT>`.
|
||||||
|
- 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 `GITEA_TOKEN` in `.env`.
|
||||||
|
|
||||||
|
3. **Configure registry variables**:
|
||||||
|
- Set `GITEA_REGISTRY_URL` to the Gitea host.
|
||||||
|
- Set `GITEA_REGISTRY_NAMESPACE` to the image namespace, for example `zwitschi/linearvoid`.
|
||||||
|
- Set `GITEA_USERNAME` to the account that will push or pull images.
|
||||||
|
|
||||||
|
4. **Authenticate Docker to the registry**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login git.allucanget.biz
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **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.
|
||||||
|
|
||||||
|
6. **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
|
## Running the Application
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
@@ -85,7 +183,7 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
|
|
||||||
2. **Access the Application**:
|
2. **Access the Application**:
|
||||||
- Viewer: <http://localhost:8080>
|
- Viewer: <http://localhost:8080>
|
||||||
- Chat WebSocket: ws://localhost:3000/ws
|
- Chat WebSocket: `ws://localhost:3000/ws` when using the local env defaults, or `ws://localhost:8080/ws` if you switch to same-origin local routing.
|
||||||
- Admin Interface: <http://localhost:8080/admin> (use the credentials from `.env` or default admin/admin)
|
- Admin Interface: <http://localhost:8080/admin> (use the credentials from `.env` or default admin/admin)
|
||||||
- Health Checks:
|
- Health Checks:
|
||||||
- Playout: <http://localhost:8081/health>
|
- Playout: <http://localhost:8081/health>
|
||||||
@@ -98,23 +196,85 @@ This guide covers prerequisites, environment setup, running the application loca
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The dev server runs on <http://localhost:5173>. It proxies API calls to the backend services.
|
The dev server runs on <http://localhost:5173>. It proxies API calls to the backend services.
|
||||||
|
|
||||||
4. **Stop 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
|
```bash
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
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 `${GITEA_REGISTRY_URL}/${GITEA_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)
|
### Production Deployment (Hetzner VPS + Cloudflare)
|
||||||
|
|
||||||
1. **Provision VPS**: Use Hetzner Cloud CX22 in Ashburn, VA (per ADR-001).
|
1. **Provision VPS**: Use Hetzner Cloud CX22 in Ashburn, VA (per ADR-001).
|
||||||
2. **Configure Cloudflare**:
|
2. **Configure Cloudflare**:
|
||||||
- Create R2 bucket with `enam` location hint.
|
- Create R2 bucket with `enam` location hint.
|
||||||
- Set up custom domain (`media.example.com`) with the cache rules from `infrastructure/cloudflare/cache-rules.md`.
|
- 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.
|
- Apply the Worker from `infrastructure/cloudflare/worker.js` for advanced header management.
|
||||||
3. **Deploy**:
|
3. **Deploy**:
|
||||||
- Copy the project to the VPS.
|
- Copy the project to the VPS.
|
||||||
@@ -133,6 +293,7 @@ See `docs/architecture/README.md` for the full arc42 documentation and the ADRs
|
|||||||
- **Cache Not Working**: Verify Cloudflare cache rules (`.ts` = long TTL immutable, `.m3u8` = short TTL). Check cache hit ratio in Cloudflare dashboard.
|
- **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.
|
- **Docker Issues**: Run `docker compose logs` for 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).
|
- **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 login` to Gitea succeeded on the server and the image tags referenced by `docker-compose.prod.yml` exist.
|
||||||
|
|
||||||
## Further Reading
|
## Further Reading
|
||||||
|
|
||||||
@@ -143,4 +304,4 @@ See `docs/architecture/README.md` for the full arc42 documentation and the ADRs
|
|||||||
|
|
||||||
The system is ready for use. For production, review ADR-001 for cost estimates and scaling guidance.
|
The system is ready for use. For production, review ADR-001 for cost estimates and scaling guidance.
|
||||||
|
|
||||||
**Last Updated**: 2026-08-02
|
**Last Updated**: 2026-08-03
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
RUN apk add --no-cache gcc musl-dev
|
||||||
COPY src/ ./src/
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
|
|
||||||
RUN go mod download
|
WORKDIR /app
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /chat-server ./src/main.go
|
COPY go.mod ./
|
||||||
|
COPY src/ ./src/
|
||||||
|
RUN go mod tidy
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -o /chat-server ./src/main.go
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
RUN apk --no-cache add ca-certificates
|
RUN apk --no-cache add ca-certificates
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /chat-server .
|
COPY --from=builder /chat-server /usr/local/bin/chat-server
|
||||||
COPY config/ ./config/
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["./chat-server"]
|
CMD ["/usr/local/bin/chat-server"]
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY go.mod go.sum ./
|
|
||||||
|
|
||||||
# Initialize Go module if not present
|
# Initialize Go module if not present
|
||||||
RUN go mod init playout || true
|
RUN go mod init playout || true
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func ValidateVideo(videoPath string) (VideoFile, error) {
|
|||||||
v := VideoFile{Path: videoPath}
|
v := VideoFile{Path: videoPath}
|
||||||
|
|
||||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", videoPath)
|
cmd := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", videoPath)
|
||||||
output, err := cmd.Output()
|
_, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.Validation = "ffprobe failed: " + err.Error()
|
v.Validation = "ffprobe failed: " + err.Error()
|
||||||
return v, err
|
return v, err
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${NGINX_PORT:-8080}:80"
|
- "${NGINX_PORT:-8080}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/hls:/usr/share/nginx/html/hls:ro
|
- ./data/hls:/srv/hls:ro
|
||||||
networks:
|
networks:
|
||||||
- linearvoid-net
|
- linearvoid-net
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
+2
-4
@@ -1,5 +1,3 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
playout:
|
playout:
|
||||||
image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/playout:latest
|
image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/playout:latest
|
||||||
@@ -60,8 +58,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${NGINX_PORT:-8080}:80"
|
- "${NGINX_PORT:-8080}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend/dist:/usr/share/nginx/html:ro
|
- ./frontend/dist:/srv/frontend:ro
|
||||||
- ./data/hls:/usr/share/nginx/html/hls:ro
|
- ./data/hls:/srv/hls:ro
|
||||||
networks:
|
networks:
|
||||||
- linearvoid-net
|
- linearvoid-net
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
+26
-5
@@ -16,13 +16,34 @@ let ws: WebSocket | null = null;
|
|||||||
let currentRoom = "main";
|
let currentRoom = "main";
|
||||||
let username = "viewer" + Math.floor(Math.random() * 1000);
|
let username = "viewer" + Math.floor(Math.random() * 1000);
|
||||||
|
|
||||||
|
function resolveWebSocketBaseUrl() {
|
||||||
|
const configuredUrl = import.meta.env.VITE_PUBLIC_WS_URL?.trim();
|
||||||
|
|
||||||
|
if (configuredUrl) {
|
||||||
|
return configuredUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
return `${protocol}//${window.location.host}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveManifestUrl() {
|
||||||
|
const configuredUrl = import.meta.env.VITE_PUBLIC_HLS_URL?.trim();
|
||||||
|
|
||||||
|
if (configuredUrl) {
|
||||||
|
return configuredUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL("/hls/playlist.m3u8", window.location.origin).toString();
|
||||||
|
}
|
||||||
|
|
||||||
function connectWebSocket(room: string) {
|
function connectWebSocket(room: string) {
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const socketUrl = new URL(resolveWebSocketBaseUrl());
|
||||||
ws = new WebSocket(
|
socketUrl.searchParams.set("username", username);
|
||||||
`${protocol}//localhost:3000/ws?username=${username}&room=${room}`,
|
socketUrl.searchParams.set("room", room);
|
||||||
);
|
ws = new WebSocket(socketUrl);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log(`Connected to chat room: ${room}`);
|
console.log(`Connected to chat room: ${room}`);
|
||||||
@@ -113,7 +134,7 @@ window.app = () => {
|
|||||||
|
|
||||||
// Initialize HLS Player
|
// Initialize HLS Player
|
||||||
const video = document.getElementById("player") as HTMLVideoElement;
|
const video = document.getElementById("player") as HTMLVideoElement;
|
||||||
const manifestUrl = "http://localhost:8080/hls/playlist.m3u8"; // from nginx/pla yout
|
const manifestUrl = resolveManifestUrl();
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
hls = new Hls();
|
hls = new Hls();
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
envDir: "..",
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ FROM nginx:alpine
|
|||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# Create directories for HLS and frontend
|
# Create directories for HLS and frontend
|
||||||
RUN mkdir -p /usr/share/nginx/html/hls /usr/share/nginx/html/admin
|
RUN mkdir -p /srv/frontend /srv/hls /srv/frontend/admin
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -3,11 +3,25 @@ events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
resolver 127.0.0.11 ipv6=off valid=30s;
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name www.linearvoid.tv;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
return 301 https://linearvoid.tv$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name linearvoid.tv localhost 127.0.0.1;
|
||||||
|
|
||||||
|
root /srv/frontend;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# Serve static frontend
|
# Serve static frontend
|
||||||
@@ -17,16 +31,20 @@ http {
|
|||||||
|
|
||||||
# Proxy chat WebSocket
|
# Proxy chat WebSocket
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://chat:3000;
|
set $chat_upstream http://chat:3000;
|
||||||
|
proxy_pass $chat_upstream;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection $connection_upgrade;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Serve HLS from playout (with cache headers for Cloudflare)
|
# Local/dev fallback HLS origin. Production media should use MEDIA_DOMAIN.
|
||||||
location /hls/ {
|
location /hls/ {
|
||||||
alias /usr/share/nginx/html/hls/;
|
alias /srv/hls/;
|
||||||
add_header Cache-Control "public, max-age=3600";
|
add_header Cache-Control "public, max-age=3600";
|
||||||
add_header Access-Control-Allow-Origin "*";
|
add_header Access-Control-Allow-Origin "*";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user