chore(cd): publish nginx on port 8082 to avoid Coolify port conflicts
CI / test (push) Successful in 3m4s
CI / docker (push) Successful in 34s

This commit is contained in:
zwitschi committed 2026-09-02 21:07:06 +02:00
1 parent 11d48c8e0d
commit 5f1f796a20
1 file changed
+289 -288
+289 -288
View File
@@ -1,288 +1,289 @@
# CD workflow — TEST LAB DEPLOYMENT # CD workflow — TEST LAB DEPLOYMENT
# Deploys to test lab host 192.168.88.40:2375. # Deploys to test lab host 192.168.88.40:2375.
# Production Hetzner target TBD — switch DOCKER_HOST when provisioned. # Production Hetzner target TBD — switch DOCKER_HOST when provisioned.
# See DEPLOYMENT.md Production Deployment section for cutover plan. # See DEPLOYMENT.md Production Deployment section for cutover plan.
name: CD name: CD
on: on:
workflow_run: workflow_run:
workflows: [CI] workflows: [CI]
types: [completed] types: [completed]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
deploy: deploy:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: [ubuntu-latest] runs-on: [ubuntu-latest]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to registry on remote host - name: Login to registry on remote host
env: env:
DOCKER_HOST: tcp://192.168.88.40:2375 DOCKER_HOST: tcp://192.168.88.40:2375
run: | run: |
echo "${{ secrets.CI_REGISTRY_TOKEN }}" | docker login git.allucanget.biz -u "${{ secrets.CI_REGISTRY_USER }}" --password-stdin echo "${{ secrets.CI_REGISTRY_TOKEN }}" | docker login git.allucanget.biz -u "${{ secrets.CI_REGISTRY_USER }}" --password-stdin
- name: Prepare environment - name: Prepare environment
run: | run: |
cat > .env << 'EOF' cat > .env << 'EOF'
# Database & Redis # Database & Redis
DATABASE_URL="${{ secrets.DATABASE_URL }}" DATABASE_URL="${{ secrets.DATABASE_URL }}"
REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0
# Playout # Playout
PLAYLIST_PATH=/app/playlist/playlist.txt PLAYLIST_PATH=/app/playlist/playlist.txt
HLS_SEGMENT_DURATION=2 HLS_SEGMENT_DURATION=2
HLS_WINDOW_SIZE=12 HLS_WINDOW_SIZE=12
LOG_LEVEL=info LOG_LEVEL=info
# Cloudflare R2 (required for HLS segment sync) # Cloudflare R2 (required for HLS segment sync)
R2_ACCESS_KEY_ID="${{ secrets.R2_ACCESS_KEY_ID }}" R2_ACCESS_KEY_ID="${{ secrets.R2_ACCESS_KEY_ID }}"
R2_SECRET_ACCESS_KEY="${{ secrets.R2_SECRET_ACCESS_KEY }}" R2_SECRET_ACCESS_KEY="${{ secrets.R2_SECRET_ACCESS_KEY }}"
R2_BUCKET_NAME="${{ secrets.R2_BUCKET_NAME }}" R2_BUCKET_NAME="${{ secrets.R2_BUCKET_NAME }}"
R2_ENDPOINT="${{ secrets.R2_ENDPOINT }}" R2_ENDPOINT="${{ secrets.R2_ENDPOINT }}"
R2_REGION=auto R2_REGION=auto
# Cloudflare # Cloudflare
CLOUDFLARE_API_TOKEN="${{ secrets.CLOUDFLARE_API_TOKEN }}" CLOUDFLARE_API_TOKEN="${{ secrets.CLOUDFLARE_API_TOKEN }}"
CLOUDFLARE_ACCOUNT_ID="${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" CLOUDFLARE_ACCOUNT_ID="${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"
MEDIA_DOMAIN=${{ secrets.MEDIA_DOMAIN }} MEDIA_DOMAIN=${{ secrets.MEDIA_DOMAIN }}
# Frontend public URLs # Frontend public URLs
VITE_PUBLIC_APP_URL=${{ secrets.VITE_PUBLIC_APP_URL }} VITE_PUBLIC_APP_URL=${{ secrets.VITE_PUBLIC_APP_URL }}
VITE_PUBLIC_WS_URL=${{ secrets.VITE_PUBLIC_WS_URL }} VITE_PUBLIC_WS_URL=${{ secrets.VITE_PUBLIC_WS_URL }}
VITE_PUBLIC_HLS_URL=${{ secrets.VITE_PUBLIC_HLS_URL }} VITE_PUBLIC_HLS_URL=${{ secrets.VITE_PUBLIC_HLS_URL }}
VITE_PUBLIC_ADMIN_DEFAULT_MODERATOR=${{ secrets.VITE_PUBLIC_ADMIN_DEFAULT_MODERATOR }} VITE_PUBLIC_ADMIN_DEFAULT_MODERATOR=${{ secrets.VITE_PUBLIC_ADMIN_DEFAULT_MODERATOR }}
VITE_PUBLIC_ADMIN_DEFAULT_ROOM=${{ secrets.VITE_PUBLIC_ADMIN_DEFAULT_ROOM }} VITE_PUBLIC_ADMIN_DEFAULT_ROOM=${{ secrets.VITE_PUBLIC_ADMIN_DEFAULT_ROOM }}
VITE_PUBLIC_ADMIN_MODERATION_LOG_LIMIT=50 VITE_PUBLIC_ADMIN_MODERATION_LOG_LIMIT=50
# Admin auth # Admin auth
ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}" ADMIN_USERNAME="${{ secrets.ADMIN_USERNAME }}"
ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}" ADMIN_PASSWORD="${{ secrets.ADMIN_PASSWORD }}"
ADMIN_SESSION_TTL_HOURS=12 ADMIN_SESSION_TTL_HOURS=12
# Nginx # Nginx
NGINX_PORT=80 # Host port 80/443 (Coolify Traefik) and 8080 are taken; nginx publishes on 8082.
EOF NGINX_PORT=8082
EOF
- name: Seed Prometheus config on remote host
env: - name: Seed Prometheus config on remote host
DOCKER_HOST: tcp://192.168.88.40:2375 env:
run: | DOCKER_HOST: tcp://192.168.88.40:2375
docker run --rm \ run: |
-v linearvoid_prometheus-config:/etc/prometheus \ docker run --rm \
alpine:latest sh -c ' -v linearvoid_prometheus-config:/etc/prometheus \
cat > /etc/prometheus/prometheus.yml << "PROMEOF" alpine:latest sh -c '
global: cat > /etc/prometheus/prometheus.yml << "PROMEOF"
scrape_interval: 15s global:
evaluation_interval: 15s scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "/etc/prometheus/alerts.yml" rule_files:
- "/etc/prometheus/alerts.yml"
scrape_configs:
- job_name: "chat" scrape_configs:
static_configs: - job_name: "chat"
- targets: ["chat:3000"] static_configs:
- job_name: "playout" - targets: ["chat:3000"]
static_configs: - job_name: "playout"
- targets: ["playout:8081"] static_configs:
- job_name: "nginx" - targets: ["playout:8081"]
static_configs: - job_name: "nginx"
- targets: ["nginx-exporter:9113"] static_configs:
- job_name: "redis" - targets: ["nginx-exporter:9113"]
static_configs: - job_name: "redis"
- targets: ["redis-exporter:9121"] static_configs:
- job_name: "postgres" - targets: ["redis-exporter:9121"]
static_configs: - job_name: "postgres"
- targets: ["postgres-exporter:9187"] static_configs:
- job_name: "node" - targets: ["postgres-exporter:9187"]
static_configs: - job_name: "node"
- targets: ["node-exporter:9100"] static_configs:
- job_name: "prometheus" - targets: ["node-exporter:9100"]
static_configs: - job_name: "prometheus"
- targets: ["localhost:9090"] static_configs:
PROMEOF - targets: ["localhost:9090"]
cat > /etc/prometheus/alerts.yml << "ALERTSEOF" PROMEOF
groups: cat > /etc/prometheus/alerts.yml << "ALERTSEOF"
- name: linearvoid groups:
rules: - name: linearvoid
- alert: ChatDown rules:
expr: up{job="chat"} == 0 - alert: ChatDown
for: 2m expr: up{job="chat"} == 0
labels: for: 2m
severity: critical labels:
annotations: severity: critical
summary: "Chat service is down" annotations:
- alert: PlayoutDown summary: "Chat service is down"
expr: up{job="playout"} == 0 - alert: PlayoutDown
for: 2m expr: up{job="playout"} == 0
labels: for: 2m
severity: critical labels:
annotations: severity: critical
summary: "Playout service is down" annotations:
- alert: RedisDown summary: "Playout service is down"
expr: linearvoid_chat_redis_up == 0 - alert: RedisDown
for: 2m expr: linearvoid_chat_redis_up == 0
labels: for: 2m
severity: warning labels:
annotations: severity: warning
summary: "Redis is unreachable" annotations:
- alert: PlayoutSlateActive summary: "Redis is unreachable"
expr: linearvoid_playout_mode == 1 - alert: PlayoutSlateActive
for: 1m expr: linearvoid_playout_mode == 1
labels: for: 1m
severity: warning labels:
annotations: severity: warning
summary: "Playout in slate fallback" annotations:
- alert: HighChatLatency summary: "Playout in slate fallback"
expr: rate(linearvoid_chat_messages_total[5m]) < 0.01 - alert: HighChatLatency
for: 5m expr: rate(linearvoid_chat_messages_total[5m]) < 0.01
labels: for: 5m
severity: warning labels:
annotations: severity: warning
summary: "Unusually low chat message rate" annotations:
- alert: PrometheusWALFailing summary: "Unusually low chat message rate"
expr: increase(prometheus_tsdb_wal_writes_failed_total[5m]) > 0 - alert: PrometheusWALFailing
for: 5m expr: increase(prometheus_tsdb_wal_writes_failed_total[5m]) > 0
labels: for: 5m
severity: warning labels:
annotations: severity: warning
summary: "Prometheus WAL writes failing" annotations:
- alert: HighCPU summary: "Prometheus WAL writes failing"
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 - alert: HighCPU
for: 10m expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
labels: for: 10m
severity: warning labels:
annotations: severity: warning
summary: "High CPU usage" annotations:
- alert: LowDiskSpace summary: "High CPU usage"
expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 10 - alert: LowDiskSpace
for: 5m expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 10
labels: for: 5m
severity: warning labels:
annotations: severity: warning
summary: "Low disk space" annotations:
ALERTSEOF summary: "Low disk space"
' ALERTSEOF
'
- name: Seed Grafana provisioning on remote host
env: - name: Seed Grafana provisioning on remote host
DOCKER_HOST: tcp://192.168.88.40:2375 env:
run: | DOCKER_HOST: tcp://192.168.88.40:2375
docker run --rm \ run: |
-v linearvoid_grafana-provisioning:/etc/grafana/provisioning \ docker run --rm \
alpine:latest sh -c ' -v linearvoid_grafana-provisioning:/etc/grafana/provisioning \
mkdir -p /etc/grafana/provisioning/datasources /etc/grafana/provisioning/dashboards alpine:latest sh -c '
cat > /etc/grafana/provisioning/datasources/datasource.yml << "DSEOF" mkdir -p /etc/grafana/provisioning/datasources /etc/grafana/provisioning/dashboards
apiVersion: 1 cat > /etc/grafana/provisioning/datasources/datasource.yml << "DSEOF"
datasources: apiVersion: 1
- name: Prometheus datasources:
type: prometheus - name: Prometheus
access: proxy type: prometheus
url: http://prometheus:9090 access: proxy
isDefault: true url: http://prometheus:9090
editable: true isDefault: true
DSEOF editable: true
cat > /etc/grafana/provisioning/dashboards/dashboard.yml << "PROVEOF" DSEOF
apiVersion: 1 cat > /etc/grafana/provisioning/dashboards/dashboard.yml << "PROVEOF"
providers: apiVersion: 1
- name: "default" providers:
orgId: 1 - name: "default"
folder: "" orgId: 1
type: file folder: ""
disableDeletion: false type: file
updateIntervalSeconds: 10 disableDeletion: false
allowUiUpdates: true updateIntervalSeconds: 10
options: allowUiUpdates: true
path: /etc/grafana/provisioning/dashboards options:
PROVEOF path: /etc/grafana/provisioning/dashboards
cat > /etc/grafana/provisioning/dashboards/linearvoid-app.json << "DBEOF" PROVEOF
{ cat > /etc/grafana/provisioning/dashboards/linearvoid-app.json << "DBEOF"
"title": "Linear Void Monitoring", {
"panels": [ "title": "Linear Void Monitoring",
{ "panels": [
"title": "Concurrent Viewers", {
"type": "timeseries", "title": "Concurrent Viewers",
"targets": [{ "expr": "linearvoid_chat_connected_clients" }] "type": "timeseries",
}, "targets": [{ "expr": "linearvoid_chat_connected_clients" }]
{ },
"title": "Stored Messages", {
"type": "stat", "title": "Stored Messages",
"targets": [{ "expr": "linearvoid_chat_messages_total" }] "type": "stat",
}, "targets": [{ "expr": "linearvoid_chat_messages_total" }]
{ },
"title": "Redis Reachable", {
"type": "stat", "title": "Redis Reachable",
"targets": [{ "expr": "linearvoid_chat_redis_up" }] "type": "stat",
}, "targets": [{ "expr": "linearvoid_chat_redis_up" }]
{ },
"title": "Playout Mode", {
"type": "stat", "title": "Playout Mode",
"targets": [{ "expr": "linearvoid_playout_mode" }] "type": "stat",
} "targets": [{ "expr": "linearvoid_playout_mode" }]
] }
} ]
DBEOF }
' DBEOF
'
- name: Deploy with docker-compose
env: - name: Deploy with docker-compose
DOCKER_HOST: tcp://192.168.88.40:2375 env:
run: | DOCKER_HOST: tcp://192.168.88.40:2375
docker compose -f docker-compose.prod.yml pull run: |
docker compose -f docker-compose.prod.yml up -d --remove-orphans --force-recreate docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans --force-recreate
- name: Health check
env: - name: Health check
DOCKER_HOST: tcp://192.168.88.40:2375 env:
run: | DOCKER_HOST: tcp://192.168.88.40:2375
# Wait for services to stabilize run: |
sleep 30 # Wait for services to stabilize
sleep 30
# Check chat health (via docker exec — IP-agnostic, works on any DOCKER_HOST)
echo "Checking chat health..." # Check chat health (via docker exec — IP-agnostic, works on any DOCKER_HOST)
if ! docker exec linearvoid-chat curl -sf http://localhost:3000/health; then echo "Checking chat health..."
echo "Chat health check failed" if ! docker exec linearvoid-chat curl -sf http://localhost:3000/health; then
docker logs linearvoid-chat --tail 50 echo "Chat health check failed"
exit 1 docker logs linearvoid-chat --tail 50
fi exit 1
fi
# Check nginx health
echo "Checking nginx health..." # Check nginx health
if ! docker exec linearvoid-nginx curl -sf http://localhost/health; then echo "Checking nginx health..."
echo "Nginx health check failed" if ! docker exec linearvoid-nginx curl -sf http://localhost/health; then
docker logs linearvoid-nginx --tail 50 echo "Nginx health check failed"
exit 1 docker logs linearvoid-nginx --tail 50
fi exit 1
fi
# Retry playout health check since it may still be initializing
echo "Checking playout health..." # Retry playout health check since it may still be initializing
for i in $(seq 1 6); do echo "Checking playout health..."
if docker exec linearvoid-playout curl -sf http://localhost:8081/health; then for i in $(seq 1 6); do
echo "Playout is healthy" if docker exec linearvoid-playout curl -sf http://localhost:8081/health; then
break echo "Playout is healthy"
fi break
echo "Playout not ready yet (attempt $i/6), waiting 20s..." fi
sleep 20 echo "Playout not ready yet (attempt $i/6), waiting 20s..."
done sleep 20
if ! docker exec linearvoid-playout curl -sf http://localhost:8081/health; then done
echo "Playout failed to become healthy after 6 attempts" if ! docker exec linearvoid-playout curl -sf http://localhost:8081/health; then
docker logs linearvoid-playout --tail 50 echo "Playout failed to become healthy after 6 attempts"
exit 1 docker logs linearvoid-playout --tail 50
fi exit 1
fi
echo "All services healthy"
echo "All services healthy"
- name: Cleanup old images
env: - name: Cleanup old images
DOCKER_HOST: tcp://192.168.88.40:2375 env:
run: | DOCKER_HOST: tcp://192.168.88.40:2375
docker image prune -f --filter "until=24h" run: |
docker image prune -f --filter "until=24h"