3 Commits
Author SHA1 Message Date
zwitschi 2ed89760d6 docs: align CI/CD and release policy wording
CI / test (push) Successful in 51s
CD / deploy (push) Successful in 58s
CI / docker (push) Successful in 21s
2026-08-07 09:14:32 +02:00
zwitschi a8df164cc2 fix: seed correct monitoring volumes and verify monitoring deployment
CD / deploy (push) Successful in 2m16s
CI / test (push) Successful in 2m48s
CI / docker (push) Successful in 37s
2026-08-07 09:01:12 +02:00
zwitschi 0a8221cf83 fix: ignore reorg 2026-08-07 08:58:29 +02:00
6 changed files with 257 additions and 69 deletions

No files matched your search

+75 -1
View File
@@ -48,7 +48,7 @@ jobs:
DOCKER_HOST: tcp://192.168.88.40:2375 DOCKER_HOST: tcp://192.168.88.40:2375
run: | run: |
docker run --rm \ docker run --rm \
-v prometheus-config:/etc/prometheus \ -v linearvoid_prometheus-config:/etc/prometheus \
alpine:latest sh -c ' alpine:latest sh -c '
cat > /etc/prometheus/prometheus.yml << "PROMEOF" cat > /etc/prometheus/prometheus.yml << "PROMEOF"
global: global:
@@ -113,6 +113,20 @@ jobs:
severity: warning severity: warning
annotations: annotations:
summary: "Playout in slate fallback" summary: "Playout in slate fallback"
- alert: HighChatLatency
expr: rate(linearvoid_chat_messages_total[5m]) < 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "Unusually low chat message rate"
- alert: PrometheusWALFailing
expr: increase(prometheus_tsdb_wal_writes_failed_total[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Prometheus WAL writes failing"
- alert: HighCPU - alert: HighCPU
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 10m for: 10m
@@ -130,6 +144,66 @@ jobs:
ALERTSEOF ALERTSEOF
' '
- name: Seed Grafana provisioning on remote host
env:
DOCKER_HOST: tcp://192.168.88.40:2375
run: |
docker run --rm \
-v linearvoid_grafana-provisioning:/etc/grafana/provisioning \
alpine:latest sh -c '
mkdir -p /etc/grafana/provisioning/datasources /etc/grafana/provisioning/dashboards
cat > /etc/grafana/provisioning/datasources/datasource.yml << "DSEOF"
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
DSEOF
cat > /etc/grafana/provisioning/dashboards/dashboard.yml << "PROVEOF"
apiVersion: 1
providers:
- name: "default"
orgId: 1
folder: ""
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards
PROVEOF
cat > /etc/grafana/provisioning/dashboards/linearvoid-app.json << "DBEOF"
{
"title": "Linear Void Monitoring",
"panels": [
{
"title": "Concurrent Viewers",
"type": "timeseries",
"targets": [{ "expr": "linearvoid_chat_connected_clients" }]
},
{
"title": "Stored Messages",
"type": "stat",
"targets": [{ "expr": "linearvoid_chat_messages_total" }]
},
{
"title": "Redis Reachable",
"type": "stat",
"targets": [{ "expr": "linearvoid_chat_redis_up" }]
},
{
"title": "Playout Mode",
"type": "stat",
"targets": [{ "expr": "linearvoid_playout_mode" }]
}
]
}
DBEOF
'
- name: Deploy with docker-compose - name: Deploy with docker-compose
env: env:
DOCKER_HOST: tcp://192.168.88.40:2375 DOCKER_HOST: tcp://192.168.88.40:2375
+2 -4
View File
@@ -79,10 +79,8 @@ Thumbs.db
.terraform/ .terraform/
# Agent / Tool files # Agent / Tool files
.github/instructions/TODO.md .github/agents/
.github/instructions/DONE.md .github/instructions/
.github/instructions/BACKLOG.md
# (keep AGENTS.md, MEMORY.md, and other instruction files)
# Temporary architecture artifacts # Temporary architecture artifacts
**/*.tmp **/*.tmp
+1 -1
View File
@@ -227,7 +227,7 @@ Contributions are welcome! See [`CONTRIBUTING.md`](CONTRIBUTING.md) for guidelin
3. Open PRs on [Gitea](https://git.allucanget.biz/zwitschi/linearvoid). 3. Open PRs on [Gitea](https://git.allucanget.biz/zwitschi/linearvoid).
4. CI validates builds and tests automatically. 4. CI validates builds and tests automatically.
Since this a linear project, we also rely on a linear development and release model: Only main branch is used for releases, and all features are merged into main after review and CI validation. Releases are tagged with semantic versioning (1.0.3, 1.0.4, etc.) and automatically built/pushed to the container registry. Version numbers are declared in `frontend/package.json`, `CHANGELOG.md`, and the version badge in this README. For patches, we use even numbers for stable versions (1.0.2, 1.0.4) and odd numbers for pre-release/unstable versions (1.0.3, 1.0.5). Since this is a linear project, we also rely on a linear development and release model: only `main` branch is used for releases, and all features are merged into `main` after review and CI validation. Releases use semantic version tags in `*.*.*` format (for example `1.0.9`) and are automatically built/pushed to the container registry. Version numbers are declared in `frontend/package.json`, `CHANGELOG.md`, and the version badge in this README.
[MIT License](LICENSE) [MIT License](LICENSE)
+147 -33
View File
@@ -162,39 +162,6 @@ flowchart LR
- 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 ## Gitea Setup
1. **Create or verify a Gitea user**: 1. **Create or verify a Gitea user**:
@@ -231,6 +198,153 @@ flowchart LR
7. **Validate image availability**: 7. **Validate image availability**:
- Before a production rollout, verify that the expected `:latest` tags can be pulled from the registry on the target server. - 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 `.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.
## Cloudflare
Use this section for full edge setup (DNS, Pages, cache, security, and monitoring).
### 1. DNS Setup
1. Add apex record for `linearvoid.tv` pointing to Cloudflare Pages target.
2. Add `www` CNAME to Cloudflare Pages 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. 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. Validate behavior using response headers:
- `Cf-Cache-Status` should move from `MISS` to `HIT` for hot objects
Reference: `infrastructure/cloudflare/cache-rules.md`.
### 4. Cloudflare Pages
1. Connect Gitea repository to Cloudflare Pages.
2. Build command:
```bash
cd frontend && npm run build
```
3. Output directory:
```text
frontend/dist
```
4. Configure production domain to `linearvoid.tv`.
### 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. Pages serves 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 ## Running the Application
### Local Development ### Local Development
+2 -2
View File
@@ -180,8 +180,7 @@ services:
- "3001:3000" - "3001:3000"
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards - grafana-provisioning:/etc/grafana/provisioning
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
environment: environment:
- GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
@@ -201,3 +200,4 @@ volumes:
prometheus-config: prometheus-config:
prometheus-data: prometheus-data:
grafana-data: grafana-data:
grafana-provisioning:
+30 -28
View File
@@ -394,7 +394,7 @@ Notable aspects: Manifest freshness ensures all viewers stay on same timeline; c
### CI/CD Pipeline Architecture ### CI/CD Pipeline Architecture
The deployment follows a two-stage pipeline: **CI builds and pushes images**, then **CD pulls and deploys** them. Currently targeting the **test lab** (192.168.88.40). Production Hetzner VPS is planned but not yet provisioned — the same pipeline will deploy there by changing `DOCKER_HOST`. The deployment uses parallel workflows on each push to `main`: **CI builds and pushes images** while **CD deploys latest images** to the test lab. A dedicated release workflow handles semantic version tags. Current runtime target is the **test lab** (192.168.88.40). Production Hetzner VPS is planned but not yet provisioned — the same pipeline will deploy there by changing `DOCKER_HOST`.
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -431,18 +431,22 @@ flowchart LR
**Pipeline Sequence:** **Pipeline Sequence:**
1. **CI Workflow** (`.gitea/workflows/ci.yml`): 1. **CI Workflow** (`.gitea/workflows/ci.yml`):
- Triggers on `push` to `main` or `pull_request` - Triggers on `push` to `main` and `pull_request`
- Runs `test` job: frontend lint/build, Go validation, `docker compose build` - Runs `test` job: frontend lint/build and Go validation
- Runs `docker` job (after `test` passes, only on `main` push): - Runs `docker` job (after `test` passes, only on `main` push):
- Builds `playout`, `chat`, `nginx` images with Buildx - Builds `playout`, `chat`, `nginx` images with Buildx
- Pushes to Gitea Container Registry with `main` and `latest` tags - Pushes to Gitea Container Registry with `main` and `latest` tags
2. **CD Workflow** (`.gitea/workflows/cd.yml`): 2. **CD Workflow** (`.gitea/workflows/cd.yml`):
- Triggers on `workflow_run` when CI completes successfully, or manually via `workflow_dispatch` - Triggers on `push` to `main` (parallel to CI), or manually via `workflow_dispatch`
- Pulls latest images from registry - Logs in remote Docker host to private registry
- Generates `.env` from secrets - Generates `.env` from secrets
- Seeds Prometheus config into named volume - Seeds Prometheus and Grafana provisioning into named volumes
- Deploys via `docker compose -f docker-compose.prod.yml up -d` to remote host (`192.168.88.40:2375` — test lab; switch to Hetzner IP for production) - Deploys via `docker compose -f docker-compose.prod.yml up -d --remove-orphans --force-recreate` to remote host (`192.168.88.40:2375` — test lab; switch to Hetzner IP for production)
- Runs health checks with retry logic for playout initialization - Runs health checks with retry logic for playout initialization
3. **Release Workflow** (`.gitea/workflows/release.yml`):
- Triggers on semantic tags in `*.*.*` format (for example `1.0.9`)
- Builds and pushes versioned image tags (`X.Y.Z`, `X.Y`, `latest`)
- Creates release entry in Gitea
### Infrastructure Level 1 ### Infrastructure Level 1
@@ -524,14 +528,14 @@ Linear Void uses a two-layer monitoring model:
- Config file: `monitoring/prometheus/prometheus.yml` - Config file: `monitoring/prometheus/prometheus.yml`
- Default scrape interval: `15s` - Default scrape interval: `15s`
- Current scrape jobs: `chat` -> `chat:3000`, `prometheus` -> `prometheus:9090`. - Current scrape jobs (7 total):
- `chat` -> `chat:3000`
Recommended next-step scrape targets (if/when exporters are introduced): - `playout` -> `playout:8081`
- `nginx` -> `nginx-exporter:9113`
- `nginx` (via nginx-exporter) - `redis` -> `redis-exporter:9121`
- `redis` (via redis-exporter) - `postgres` -> `postgres-exporter:9187`
- `postgres` (via postgres-exporter) - `node` -> `node-exporter:9100`
- `node` (via node-exporter) - `prometheus` -> `localhost:9090`
#### Grafana Dashboards #### Grafana Dashboards
@@ -542,22 +546,20 @@ Use the app dashboard for service-level behavior and the Prometheus overview das
#### Alerting Rules (Prometheus Rule Groups) #### Alerting Rules (Prometheus Rule Groups)
Define alert rules as grouped policies (for example in `monitoring/prometheus/alerts.yml`) and include them from `prometheus.yml`. Rules are implemented in `monitoring/prometheus/alerts.yml` and loaded via `rule_files` from `prometheus.yml`.
Minimum recommended alert set: Current rule set (8):
- **Availability** - `ChatDown`
`up{job="chat"} == 0` for `2m`; `up{job="prometheus"} == 0` for `2m`. - `PlayoutDown`
- **Chat service health** - `RedisDown`
`linearvoid_chat_redis_up == 0` for `2m`; sudden disconnect spike / connected clients collapse. - `PlayoutSlateActive`
- **Playout continuity** - `HighChatLatency`
No HLS segment progression for `30-60s` (from playout-specific metric when available). - `PrometheusWALFailing`
- **Prometheus capacity and ingestion health** - `HighCPU`
Sustained growth in `prometheus_tsdb_wal_writes_failed_total` or `prometheus_tsdb_compactions_failed_total`, abnormal jump in `prometheus_tsdb_out_of_order_samples_total`, and high `prometheus_engine_queries` near concurrent limit. - `LowDiskSpace`
- **Latency and user-impact**
Elevated p99 handler latency from `prometheus_http_request_duration_seconds_bucket` and elevated p99 query execution latency from `prometheus_engine_query_duration_histogram_seconds_bucket`.
Route warning/critical severities to the team channel and keep runbook links attached to each alert definition. Route warning/critical severities to the operations channel and keep runbook links attached to each alert definition.
Operational resilience baseline: Operational resilience baseline: