feat(docs): add Coolify deployment guide and integrate into setup documentation
CI / test (push) Successful in 1m39s
CI / docker (push) Successful in 5m25s

feat(scripts): add environment verification scripts for deployment checks
This commit is contained in:
zwitschi committed 2026-08-30 20:00:28 +02:00
1 parent 474da7418e
commit 9f60087a4c
4 files changed
+574

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env pwsh
# check-env.ps1 — Verify the deployment environment for Linear Void.
#
# Checks, against the target Docker host (default test lab 192.168.88.40):
# 1. Docker Engine reachability over the TCP socket (port 2375).
# 2. Coolify helper stack (proxy/Traefik, sentinel, cloudflared).
# 3. Linear Void containers (playout, chat, nginx, redis, db, prometheus, grafana).
# 4. Public endpoints (app, HLS manifest, admin).
# 5. Health endpoints exposed by the services.
#
# Usage:
# ./scripts/check-env.ps1
# ./scripts/check-env.ps1 -Target 192.168.88.40
# ./scripts/check-env.ps1 -Target 192.168.88.40 -DockerPort 2375 -OnlyContainers
#
# Exit code is 0 when all required checks pass, 1 otherwise.
param(
[string]$Target = '192.168.88.40',
[int]$DockerPort = 2375,
[switch]$OnlyContainers,
[switch]$OnlyEndpoints,
[int]$TimeoutSec = 10
)
$ErrorActionPreference = 'Stop'
$target = $Target
$dockerHost = "tcp://${target}:${DockerPort}"
$failed = $false
function Fail([string]$msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red; $script:failed = $true }
function Ok([string]$msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
function Warn([string]$msg) { Write-Host " [WARN] $msg" -ForegroundColor Yellow }
Write-Host "== Linear Void environment check ==" -ForegroundColor Cyan
Write-Host "Target Docker host : $dockerHost"
Write-Host ("Date : " + (Get-Date -Format s))
# ---------------------------------------------------------------------------
# 1. Docker Engine reachability
# ---------------------------------------------------------------------------
if (-not $OnlyEndpoints) {
Write-Host "`n-- Docker Engine (${target}:${DockerPort}) --" -ForegroundColor Cyan
$env:DOCKER_HOST = $dockerHost
try {
$ver = docker version --format '{{.Server.Version}}' 2>&1
if ($LASTEXITCODE -eq 0 -and $ver -match '\d') {
Ok "Docker Engine reachable (server version $ver)"
} else {
Fail "Docker Engine not reachable: $($ver -join ' ')"
}
} catch {
Fail "Docker Engine error: $($_.Exception.Message)"
}
# -----------------------------------------------------------------------
# 2. Coolify helper stack
# -----------------------------------------------------------------------
Write-Host "`n-- Coolify managed-server helpers --" -ForegroundColor Cyan
foreach ($c in @('coolify-proxy', 'coolify-sentinel', 'coolify-cloudflared')) {
try {
$state = docker inspect $c --format '{{.State.Status}} ({{.State.Health.Status}})' 2>&1
if ($LASTEXITCODE -eq 0) {
if ($state -match 'running.*(healthy|^running \(running\))') { Ok "coolify helper: $c -> $state" }
else { Warn "coolify helper: $c -> $state" }
} else {
Warn "coolify helper: $c -> NOT FOUND (is this a Coolify-managed server?)"
}
} catch { Warn "coolify helper: $c -> error $($_.Exception.Message)" }
}
# -----------------------------------------------------------------------
# 3. Linear Void containers
# -----------------------------------------------------------------------
Write-Host "`n-- Linear Void containers --" -ForegroundColor Cyan
$required = @('linearvoid-playout', 'linearvoid-chat', 'linearvoid-nginx',
'linearvoid-redis', 'linearvoid-db', 'linearvoid-prometheus', 'linearvoid-grafana')
try {
$running = @(docker ps --format '{{.Names}}' 2>&1)
$all = @(docker ps -a --format '{{.Names}}\t{{.Status}}' 2>&1)
foreach ($name in $required) {
if ($running -contains $name) {
Ok "container: $name is running"
} elseif (($all | Where-Object { $_ -match "^$name\s" })) {
$status = ($all | Where-Object { $_ -match "^$name\s" } | Select-Object -First 1)
Fail "container: $name EXISTS but not running -> $status"
} else {
Fail "container: $name MISSING"
}
}
} catch { Fail "container listing error: $($_.Exception.Message)" }
}
# ---------------------------------------------------------------------------
# 4. Health endpoints (via Docker host)
# ---------------------------------------------------------------------------
if (-not $OnlyContainers) {
Write-Host "`n-- Health endpoints --" -ForegroundColor Cyan
$checks = @(
@{ Name = 'chat /health'; Url = "http://${target}:3000/health" },
@{ Name = 'nginx /health'; Url = "http://${target}:8081/health" },
@{ Name = 'prometheus'; Url = "http://${target}:9090/-/healthy" },
@{ Name = 'grafana /api/health'; Url = "http://${target}:3001/api/health" }
)
foreach ($c in $checks) {
try {
$r = Invoke-WebRequest -Uri $c.Url -TimeoutSec $TimeoutSec -UseBasicParsing -ErrorAction Stop
if ($r.StatusCode -eq 200) { Ok "$($c.Name) -> 200" } else { Warn "$($c.Name) -> $($r.StatusCode)" }
} catch {
Fail "$($c.Name) -> $($_.Exception.Response.StatusCode.value__)"
}
}
# -----------------------------------------------------------------------
# 5. Public endpoints (routed through the Docker host for reliable TLS)
# -----------------------------------------------------------------------
Write-Host "`n-- Public endpoints (via Docker host) --" -ForegroundColor Cyan
$pub = @(
@{ Name = 'linearvoid.tv app'; Url = 'https://linearvoid.tv/' },
@{ Name = 'media HLS manifest'; Url = 'https://media.linearvoid.tv/hls/playlist.m3u8' },
@{ Name = 'coolify dashboard'; Url = 'https://coolify.allucanget.biz/' }
)
foreach ($c in $pub) {
try {
$code = (& docker run --rm curlimages/curl:latest -sS -o /dev/null -w '%{http_code}' --max-time $TimeoutSec -L $c.Url 2>&1 | Out-String).Trim()
if ($code -match '^\d{3}$') { Ok "$($c.Name) -> HTTP $code" }
else { Warn "$($c.Name) -> $code" }
} catch {
Warn "$($c.Name) -> error $($_.Exception.Message.Split("`n")[0])"
}
}
}
# ---------------------------------------------------------------------------
Write-Host ""
if ($failed) { Write-Host "RESULT: FAILED" -ForegroundColor Red; exit 1 }
Write-Host "RESULT: OK" -ForegroundColor Green; exit 0
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env pwsh
# check-tokens.ps1 — Verify that the credentials in the environment actually work.
#
# Reads .env (or a supplied file) and validates access against live services:
# 1. Gitea container registry (CI_REGISTRY_TOKEN / CI_REGISTRY_USER).
# 2. Cloudflare API token(s) (CLOUDFLARE_API_TOKEN, and any R2 admin token).
# 3. R2/S3 bucket access (R2_ACCESS_KEY_ID + R2_SECRET_ACCESS_KEY) — lists buckets.
# 4. Linear Void admin login (ADMIN_USERNAME / ADMIN_PASSWORD) against the app origin.
#
# NOTE: Runs the checks through the target Docker host (default 192.168.88.40) so
# network + TLS work regardless of this machine's proxy/firewall. Uses the
# curlimages/curl and amazon/aws-cli images, which are pulled on demand.
#
# Usage:
# ./scripts/check-tokens.ps1
# ./scripts/check-tokens.ps1 -EnvFile .env
# ./scripts/check-tokens.ps1 -Target 192.168.88.40
#
# Exit code 0 = all credentials valid; 1 = at least one check failed.
param(
[string]$EnvFile = '',
[string]$Target = '192.168.88.40',
[int]$DockerPort = 2375,
[int]$TimeoutSec = 20
)
$ErrorActionPreference = 'Stop'
$target = $Target
$dockerHost = "tcp://${target}:${DockerPort}"
$env:DOCKER_HOST = $dockerHost
$failed = $false
function Fail([string]$msg) { Write-Host " [FAIL] $msg" -ForegroundColor Red; $script:failed = $true }
function Ok([string]$msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
function Warn([string]$msg) { Write-Host " [WARN] $msg" -ForegroundColor Yellow }
# Resolve env file: explicit, or $PWD/.env, or repo root.
if (-not $EnvFile) {
foreach ($cand in @((Join-Path $PWD '.env'), (Join-Path (Split-Path $PWD) '.env'))) {
if (Test-Path $cand) { $EnvFile = $cand; break }
}
}
if (-not $EnvFile -or -not (Test-Path $EnvFile)) {
Write-Host "No .env file found. Pass one with -EnvFile." -ForegroundColor Yellow
exit 1
}
# Parse KEY=VALUE lines into a hashtable.
$envs = @{}
Get-Content $EnvFile | ForEach-Object {
if ($_ -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$') {
$envs[$Matches[1]] = $Matches[2]
}
}
function Get-Val([string]$key) {
if ($envs.ContainsKey($key)) { return $envs[$key] }
return $null
}
# Pull helper images once.
Write-Host "Preparing helper images (curl, aws-cli) on $target..." -ForegroundColor Cyan
docker pull curlimages/curl:latest 2>&1 | Out-Null
docker pull amazon/aws-cli:latest 2>&1 | Out-Null
function Curl([string]$url, [string]$bearer, [string]$tokenHeader) {
$a = @('run', '--rm', 'curlimages/curl:latest', '-sS', '--max-time', "$TimeoutSec")
if ($bearer) { $a += @('-H', "Authorization: Bearer $bearer") }
if ($tokenHeader) { $a += @('-H', "Authorization: token $tokenHeader") }
$a += $url
return (& docker @a 2>&1 | Out-String).Trim()
}
# ---------------------------------------------------------------------------
Write-Host "== Linear Void credential / token check ==" -ForegroundColor Cyan
Write-Host "Env file : $EnvFile"
Write-Host "Docker host: $dockerHost"
# --- 1. Gitea registry token ---
Write-Host "`n-- Gitea container registry --" -ForegroundColor Cyan
$regUser = Get-Val 'CI_REGISTRY_USER'
$regToken = Get-Val 'CI_REGISTRY_TOKEN'
if ($regUser -and $regToken) {
# Non-sensitive probe: /api/v1/user returns a scope message for token-based creds.
$out = Curl 'https://git.allucanget.biz/api/v1/user' $null $regToken
if ($out -match 'token does not have at least one of required scope' -or $out -match '"login"' -or $out -match 'invalid_grant') {
if ($out -match 'invalid_grant|Unauthorized|invalid') {
Fail "Gitea registry token rejected ($regUser): $out"
} else {
Ok "Gitea registry token valid for '$regUser' (scope: write:package/write:repository)"
}
} else {
Warn "Gitea registry token response unexpected: $out"
}
} else {
Warn "CI_REGISTRY_USER / CI_REGISTRY_TOKEN not set; skipping Gitea registry check"
}
# --- 2. Cloudflare API token ---
Write-Host "`n-- Cloudflare API tokens --" -ForegroundColor Cyan
$cfToken = Get-Val 'CLOUDFLARE_API_TOKEN'
if ($cfToken) {
$out = Curl 'https://api.cloudflare.com/client/v4/user/tokens/verify' $cfToken $null
if ($out -match '"success":true') {
if ($out -match '"status":"active"') { Ok 'Cloudflare API token valid and ACTIVE' }
else { Warn "Cloudflare API token valid but status: $out" }
} else {
Fail "Cloudflare API token rejected: $out"
}
} else { Warn "CLOUDFLARE_API_TOKEN not set; skipping Cloudflare check" }
# --- 3. Cloudflare R2 / S3 bucket access ---
Write-Host "`n-- Cloudflare R2 (S3-compatible) --" -ForegroundColor Cyan
$r2Key = Get-Val 'R2_ACCESS_KEY_ID'
$r2Secret = Get-Val 'R2_SECRET_ACCESS_KEY'
$r2Ep = Get-Val 'R2_ENDPOINT'
$bucket = Get-Val 'R2_BUCKET_NAME'
# Use the admin key pair (which has ListBuckets) when present, else the regular key.
$listKey = if ((Get-Val 'R2_ADMIN_ACCESS_KEY_ID') -and (Get-Val 'R2_ADMIN_SECRET_ACCESS_KEY')) {
@{ k = Get-Val 'R2_ADMIN_ACCESS_KEY_ID'; s = Get-Val 'R2_ADMIN_SECRET_ACCESS_KEY' }
} else { @{ k = $r2Key; s = $r2Secret } }
if ($listKey.k -and $listKey.s -and $r2Ep) {
$script = @"
export AWS_ACCESS_KEY_ID='$($listKey.k)'
export AWS_SECRET_ACCESS_KEY='$($listKey.s)'
aws s3 ls --endpoint-url '$r2Ep' --region auto 2>&1
"@
$out = (& docker run --rm --entrypoint sh amazon/aws-cli:latest -c $script 2>&1 | Out-String).Trim()
if ($out -match $bucket) { Ok "R2 bucket listing OK, '$bucket' present" }
elseif ($out -match 'AccessDenied') { Fail "R2 listing denied (no ListBuckets permission): $out" }
elseif ($out -match '^\S+\s+') { Warn "R2 listing returned (unexpected): $out" }
else { Fail "R2 access failed: $out" }
} else { Warn "R2 credentials / endpoint not fully set; skipping R2 check" }
# Verify the specific application bucket is reachable with the primary key (read).
if ($r2Key -and $r2Secret -and $r2Ep -and $bucket) {
$script = @"
export AWS_ACCESS_KEY_ID='$r2Key'
export AWS_SECRET_ACCESS_KEY='$r2Secret'
aws s3 ls "s3://$bucket/" --endpoint-url '$r2Ep' --region auto --max-keys 5 2>&1 | head -5
"@
$out = (& docker run --rm --entrypoint sh amazon/aws-cli:latest -c $script 2>&1 | Out-String).Trim()
if ($out -match 'AccessDenied') { Fail "Bucket '$bucket' read denied with primary key" }
elseif ($out -eq '') { Ok "Bucket '$bucket' reachable (empty listing, expected)" }
elseif ($out -match 'NoSuchBucket') { Fail "Bucket '$bucket' NOT FOUND" }
else { Ok "Bucket '$bucket' reachable" }
} elseif (-not $bucket) { Warn "R2_BUCKET_NAME not set; skipping bucket-level check" }
# --- 4. Linear Void admin login ---
Write-Host "`n-- Linear Void admin login --" -ForegroundColor Cyan
$adminUser = Get-Val 'ADMIN_USERNAME'
$adminPass = Get-Val 'ADMIN_PASSWORD'
if ($adminUser -and $adminPass) {
# Hit the chat backend directly (public origin is caught by nginx SPA fallback).
$url = "http://${target}:3000/api/admin/login"
$body = ('{"username":"' + $adminUser + '","password":"' + $adminPass + '"}')
$wrongBody = ('{"username":"' + $adminUser + '","password":"__definitely_wrong__"}')
$a = @('run', '--rm', 'curlimages/curl:latest', '-sS', '--max-time', "$TimeoutSec",
'-o', '/dev/null', '-w', '%{http_code}', '-X', 'POST',
'-H', 'Content-Type: application/json', '-d', $body, $url)
$okCode = (& docker @a 2>&1 | Out-String).Trim()
$b = @('run', '--rm', 'curlimages/curl:latest', '-sS', '--max-time', "$TimeoutSec",
'-o', '/dev/null', '-w', '%{http_code}', '-X', 'POST',
'-H', 'Content-Type: application/json', '-d', $wrongBody, $url)
$wrongCode = (& docker @b 2>&1 | Out-String).Trim()
if ($okCode -eq '200' -and $wrongCode -eq '401') { Ok "Admin credentials valid (correct=200, wrong=401)" }
else { Fail "Admin login unexpected (correct=$okCode, wrong=$wrongCode)" }
} else { Warn "ADMIN_USERNAME / ADMIN_PASSWORD not set; skipping admin check" }
# ---------------------------------------------------------------------------
Write-Host ""
if ($failed) { Write-Host "RESULT: FAILED" -ForegroundColor Red; exit 1 }
Write-Host "RESULT: OK (no failing checks)" -ForegroundColor Green; exit 0