chore(cd): publish nginx on port 8082 to avoid Coolify port conflicts
This commit is contained in:
1 parent
11d48c8e0d
commit
5f1f796a20
1 file changed
+289
-288
+289
-288
@@ -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"
|
||||||
Reference in new issue
Block a user