Agrega scripts y runbooks: Cloudflare API, autostart Coolify, parche Chatwoot, deploy Solo Leveling + docs de casos
- scripts/Invoke-CloudflareApi.ps1: control de tunnel/DNS via API - scripts/Install-CoolifyAutostart.ps1 + scripts/host/: autostart de LXC 102 + tunnel tras corte - scripts/Apply-ChatwootEnterprisePatch.ps1: parche enterprise (autodetecta creds) - Deploy-SoloLeveling.ps1: deploy build-on-server verificado - docs/runbooks/cloudflare-tunnel.md y autostart-coolify.md - docs/casos/chatwoot-enterprise-patch.md (credenciales redactadas) - docs/AGENTS-coolify-apps.md + issues y reportes - deploy_skill/references/coolify-4.1.2-notes.md: hallazgos verificados (seccion 7) - package.json para verify-online.mjs del coolify-deploy skill - .gitignore: excluye .claude/ y .opencode/ (config local de herramientas)
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# Aplica el parche enterprise de Chatwoot alineado al comando original:
|
||||
#
|
||||
# docker exec -i "$(docker ps -q --filter 'name=pgvector')" psql \
|
||||
# -U postgres -d chatwoot -c "UPDATE ... WHERE name = '...';"
|
||||
#
|
||||
# Convencion del repo: Proxmox 192.168.0.200 -> LXC 102 (coolify) -> docker.
|
||||
#
|
||||
# Criterio de exito: psql imprime 3 lineas "UPDATE 1" (una por sentencia).
|
||||
# Tras aplicar, NO pulsar "Refresh" en /super_admin/settings.
|
||||
#
|
||||
# Uso:
|
||||
# .\scripts\Apply-ChatwootEnterprisePatch.ps1
|
||||
# .\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun
|
||||
# .\scripts\Apply-ChatwootEnterprisePatch.ps1 -Container "postgres-c11xzy2tx2cdapm32f5b89vy"
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$DryRun,
|
||||
[string]$Container = "",
|
||||
[string]$LxcId = "102",
|
||||
[string]$ProxmoxHost = $(if ($env:PROXMOX_HOST) { $env:PROXMOX_HOST } else { "192.168.0.200" }),
|
||||
[string]$SshKey = $(if ($env:PROXMOX_SSH_KEY) { $env:PROXMOX_SSH_KEY } else { "C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win" })
|
||||
)
|
||||
|
||||
# Encoding UTF-8 sin BOM (BOM rompe el shebang #!/bin/bash en Linux).
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SshKey)) {
|
||||
throw "No se encontro la clave SSH: $SshKey"
|
||||
}
|
||||
|
||||
function Invoke-Remote {
|
||||
param([string]$RemoteCmd)
|
||||
$args = @(
|
||||
"-o", "BatchMode=yes"
|
||||
"-o", "ConnectTimeout=15"
|
||||
"-o", "StrictHostKeyChecking=no"
|
||||
"-i", $SshKey
|
||||
"root@$ProxmoxHost"
|
||||
$RemoteCmd
|
||||
)
|
||||
return & ssh @args
|
||||
}
|
||||
|
||||
# --- 1) Resolver contenedor Postgres de Chatwoot --------------------------
|
||||
if (-not $Container) {
|
||||
$detect = @'
|
||||
#!/bin/bash
|
||||
pct exec __LXC__ -- bash -lc 'docker ps --format "{{.Names}}" | grep -Ei "c11xzy2tx2cdapm32f5b89vy.*(pgvector|postgres|db)" | head -n1'
|
||||
'@
|
||||
$detect = $detect.Replace('__LXC__', $LxcId)
|
||||
|
||||
$tmpDetect = [IO.Path]::GetTempFileName() + ".sh"
|
||||
[IO.File]::WriteAllText($tmpDetect, $detect, $utf8NoBom)
|
||||
try {
|
||||
$localName = Split-Path -Leaf $tmpDetect
|
||||
$scpArgs = @(
|
||||
"-o", "BatchMode=yes"
|
||||
"-o", "ConnectTimeout=15"
|
||||
"-o", "StrictHostKeyChecking=no"
|
||||
"-i", $SshKey
|
||||
$tmpDetect
|
||||
"root@${ProxmoxHost}:/tmp/$localName"
|
||||
)
|
||||
& scp @scpArgs | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp detect fallo (exit $LASTEXITCODE)." }
|
||||
Write-Host "[*] Buscando contenedor Postgres de Chatwoot en LXC $LxcId ..."
|
||||
$cand = Invoke-Remote "bash /tmp/$localName"
|
||||
Invoke-Remote "rm -f /tmp/$localName" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Listado remoto fallo (exit $LASTEXITCODE)." }
|
||||
$Container = @($cand | Where-Object { $_ -match '\S' })[0]
|
||||
if (-not $Container) {
|
||||
throw "No se encontro contenedor. Pasa -Container explicito (ej: postgres-c11xzy2tx2cdapm32f5b89vy)."
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tmpDetect -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
Write-Host "[+] Contenedor: $Container"
|
||||
|
||||
# --- 1b) Detectar credenciales reales desde docker inspect ----------------
|
||||
$inspectScript = @'
|
||||
#!/bin/bash
|
||||
pct exec __LXC__ -- docker inspect __CT__ --format '{{range .Config.Env}}{{println .}}{{end}}' | awk -F= '
|
||||
/^POSTGRES_USER=/ { u=$2 }
|
||||
/^POSTGRES_DB=/ { d=$2 }
|
||||
/^POSTGRES_PASSWORD=/ { p=$2 }
|
||||
/^POSTGRES_HOST=/ { h=$2 }
|
||||
/^SERVICE_NAME_POSTGRES_5432_TCP=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES=/ { skip=1; next }
|
||||
/^SERVICE_USER_POSTGRES=/ { skip=1; next }
|
||||
/^SERVICE_PASSWORD_POSTGRES=/ { skip=1; next }
|
||||
/^COOLIFY_CONTAINER_NAME=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_5432_TCP_PORT=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_PORT=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_PORT_5432_TCP=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_PORT_5432_TCP_PROTO=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_PORT_5432_TCP_ADDR=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_PORT_5432_TCP_PORT=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_HOST=/ { skip=1; next }
|
||||
/^SERVICE_NAME_POSTGRES_/ { skip=1; next }
|
||||
/^POSTGRES_HOST=/ { skip=1; next }
|
||||
/^POSTGRES_PORT=/ { skip=1; next }
|
||||
/^POSTGRES_USERNAME=/ { skip=1; next }
|
||||
/^POSTGRES_PASSWORD=/ { skip=1; next }
|
||||
END { print "USER=" u; print "DB=" d; print "PASSWORD=" p }
|
||||
'
|
||||
'@
|
||||
$inspectScript = $inspectScript.Replace('__LXC__', $LxcId).Replace('__CT__', $Container)
|
||||
|
||||
$tmpInspect = [IO.Path]::GetTempFileName() + ".sh"
|
||||
[IO.File]::WriteAllText($tmpInspect, $inspectScript, $utf8NoBom)
|
||||
try {
|
||||
$localName = Split-Path -Leaf $tmpInspect
|
||||
$scpArgs = @(
|
||||
"-o", "BatchMode=yes"
|
||||
"-o", "ConnectTimeout=15"
|
||||
"-o", "StrictHostKeyChecking=no"
|
||||
"-i", $SshKey
|
||||
$tmpInspect
|
||||
"root@${ProxmoxHost}:/tmp/$localName"
|
||||
)
|
||||
& scp @scpArgs | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp inspect fallo (exit $LASTEXITCODE)." }
|
||||
$envOut = Invoke-Remote "bash /tmp/$localName"
|
||||
Invoke-Remote "rm -f /tmp/$localName" | Out-Null
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tmpInspect -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$pgUser = ($envOut | Where-Object { $_ -match '^USER=' } | ForEach-Object { $_ -split '=', 2 } | Select-Object -Last 1) | Select-Object -First 1
|
||||
$pgDb = ($envOut | Where-Object { $_ -match '^DB=' } | ForEach-Object { $_ -split '=', 2 } | Select-Object -Last 1) | Select-Object -First 1
|
||||
$pgPass = ($envOut | Where-Object { $_ -match '^PASSWORD=' } | ForEach-Object { $_ -split '=', 2 } | Select-Object -Last 1) | Select-Object -First 1
|
||||
|
||||
if (-not $pgUser) { $pgUser = "postgres" }
|
||||
if (-not $pgDb) { $pgDb = "chatwoot" }
|
||||
|
||||
Write-Host ("[+] PG user={0} db={1} password={2}" -f $pgUser, $pgDb, ($pgPass -replace '.', '*'))
|
||||
|
||||
# --- 2) SQL (3 sentencias identicas al comando original) -------------------
|
||||
$sql = @'
|
||||
UPDATE public.installation_configs
|
||||
SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: enterprise\n"'
|
||||
WHERE name = 'INSTALLATION_PRICING_PLAN';
|
||||
|
||||
UPDATE public.installation_configs
|
||||
SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n"'
|
||||
WHERE name = 'INSTALLATION_PRICING_PLAN_QUANTITY';
|
||||
|
||||
UPDATE public.installation_configs
|
||||
SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: e04t63ee-5gg8-4b94-8914-ed8137a7d938\n"'
|
||||
WHERE name = 'INSTALLATION_IDENTIFIER';
|
||||
'@
|
||||
|
||||
# Script bash que envuelve el original: docker exec -i CT psql -U USER -d DB < sql.
|
||||
$applyScript = @'
|
||||
#!/bin/bash
|
||||
pct exec __LXC__ -- docker exec -i __CT__ env PGPASSWORD=__PASS__ psql -U __USER__ -d __DB__ -v ON_ERROR_STOP=1 < "$1"
|
||||
'@
|
||||
$applyScript = $applyScript.Replace('__LXC__', $LxcId).Replace('__CT__', $Container).Replace('__USER__', $pgUser).Replace('__DB__', $pgDb).Replace('__PASS__', $pgPass)
|
||||
|
||||
$verifyScript = @'
|
||||
#!/bin/bash
|
||||
pct exec __LXC__ -- docker exec -i __CT__ env PGPASSWORD=__PASS__ psql -U __USER__ -d __DB__ -c "SELECT name, serialized_value FROM public.installation_configs WHERE name IN ('INSTALLATION_PRICING_PLAN','INSTALLATION_PRICING_PLAN_QUANTITY','INSTALLATION_IDENTIFIER') ORDER BY name;"
|
||||
'@
|
||||
$verifyScript = $verifyScript.Replace('__LXC__', $LxcId).Replace('__CT__', $Container).Replace('__USER__', $pgUser).Replace('__DB__', $pgDb).Replace('__PASS__', $pgPass)
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[dry-run] SQL que se aplicaria:"
|
||||
Write-Host "------"
|
||||
Write-Host $sql
|
||||
Write-Host "------"
|
||||
Write-Host "[dry-run] apply.sh:"
|
||||
Write-Host "------"
|
||||
Write-Host $applyScript
|
||||
Write-Host "------"
|
||||
return
|
||||
}
|
||||
|
||||
# --- 3) Subir SQL + scripts y ejecutar ------------------------------------
|
||||
$tmpSql = [IO.Path]::GetTempFileName() + ".sql"
|
||||
$tmpApply = [IO.Path]::GetTempFileName() + ".sh"
|
||||
$tmpVerify = [IO.Path]::GetTempFileName() + ".sh"
|
||||
$remoteSql = "/tmp/chatwoot-enterprise-patch.sql"
|
||||
$remoteApply = "/tmp/chatwoot-apply.sh"
|
||||
$remoteVerify = "/tmp/chatwoot-verify.sh"
|
||||
|
||||
try {
|
||||
[IO.File]::WriteAllText($tmpSql, $sql, $utf8NoBom)
|
||||
[IO.File]::WriteAllText($tmpApply, $applyScript, $utf8NoBom)
|
||||
[IO.File]::WriteAllText($tmpVerify, $verifyScript, $utf8NoBom)
|
||||
|
||||
# Subir SQL al HOST Proxmox (no al LXC; lo lee bash local y redirige a pct exec).
|
||||
$sqlLocal = Split-Path -Leaf $tmpSql
|
||||
$scpArgs = @(
|
||||
"-o", "BatchMode=yes"
|
||||
"-o", "ConnectTimeout=15"
|
||||
"-o", "StrictHostKeyChecking=no"
|
||||
"-i", $SshKey
|
||||
$tmpSql
|
||||
"root@${ProxmoxHost}:$remoteSql"
|
||||
)
|
||||
& scp @scpArgs | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp SQL fallo (exit $LASTEXITCODE)." }
|
||||
|
||||
# Subir apply.sh y verify.sh al host.
|
||||
$applyLocal = Split-Path -Leaf $tmpApply
|
||||
$scpArgs = @(
|
||||
"-o", "BatchMode=yes"
|
||||
"-o", "ConnectTimeout=15"
|
||||
"-o", "StrictHostKeyChecking=no"
|
||||
"-i", $SshKey
|
||||
$tmpApply
|
||||
"root@${ProxmoxHost}:$remoteApply"
|
||||
)
|
||||
& scp @scpArgs | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp apply fallo (exit $LASTEXITCODE)." }
|
||||
|
||||
$verifyLocal = Split-Path -Leaf $tmpVerify
|
||||
$scpArgs = @(
|
||||
"-o", "BatchMode=yes"
|
||||
"-o", "ConnectTimeout=15"
|
||||
"-o", "StrictHostKeyChecking=no"
|
||||
"-i", $SshKey
|
||||
$tmpVerify
|
||||
"root@${ProxmoxHost}:$remoteVerify"
|
||||
)
|
||||
& scp @scpArgs | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp verify fallo (exit $LASTEXITCODE)." }
|
||||
|
||||
Write-Host "[*] Aplicando 3 UPDATE en $Container ..."
|
||||
$output = Invoke-Remote "bash $remoteApply $remoteSql"
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
Write-Host "----- psql output -----"
|
||||
$output | ForEach-Object { Write-Host $_ }
|
||||
Write-Host "-----------------------"
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $tmpSql, $tmpApply, $tmpVerify -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# --- 4) Validacion --------------------------------------------------------
|
||||
$updateLines = @($output | Where-Object { $_ -match '^UPDATE\s+1\s*$' })
|
||||
Write-Host ("UPDATE 1 count = {0}" -f $updateLines.Count)
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
Invoke-Remote "rm -f $remoteSql $remoteApply $remoteVerify" | Out-Null
|
||||
throw "psql finalizo con exit code $exitCode."
|
||||
}
|
||||
if ($updateLines.Count -ne 3) {
|
||||
Invoke-Remote "rm -f $remoteSql $remoteApply $remoteVerify" | Out-Null
|
||||
throw "Se esperaban 3 lineas 'UPDATE 1', se obtuvieron $($updateLines.Count)."
|
||||
}
|
||||
|
||||
# --- 5) Verificacion SELECT -----------------------------------------------
|
||||
Write-Host "[*] Verificando valores finales ..."
|
||||
$verify = Invoke-Remote "bash $remoteVerify"
|
||||
$verify | ForEach-Object { Write-Host $_ }
|
||||
|
||||
# Limpieza final.
|
||||
Invoke-Remote "rm -f $remoteSql $remoteApply $remoteVerify" | Out-Null
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[OK] Parche enterprise aplicado correctamente (3/3 UPDATE 1)."
|
||||
Write-Host " NO pulsar 'Refresh' en /super_admin/settings."
|
||||
@@ -0,0 +1,166 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs power-outage auto-start for the Coolify stack on the Proxmox host.
|
||||
|
||||
.DESCRIPTION
|
||||
Makes sure that after a power outage (host reboot) LXC 102 (Coolify) and its
|
||||
Cloudflare tunnel come back up on their own. It does three things:
|
||||
|
||||
1. Sets the LXC to start on boot -> pct set <id> --onboot 1 --startup order=1,up=30
|
||||
2. Installs a self-healing guardian -> /usr/local/bin/coolify-autostart.sh
|
||||
+ systemd unit /etc/systemd/system/coolify-autostart.service
|
||||
3. Enables the guardian so it runs on every boot.
|
||||
|
||||
The guardian is idempotent: on each boot it ensures the LXC is running, waits
|
||||
for Docker inside it, and starts any core Coolify container or tunnel that
|
||||
isn't up. Everything is additive and reversible (see -Uninstall).
|
||||
|
||||
Source files live in scripts/host/ and are pushed over SSH (base64, so no
|
||||
quoting issues and CRLF is normalised to LF on the host).
|
||||
|
||||
.PARAMETER VerifyOnly
|
||||
Only report current state (onboot flag, unit enabled, log tail). No changes.
|
||||
|
||||
.PARAMETER SkipOnboot
|
||||
Install/enable the guardian but do NOT touch the LXC onboot flag.
|
||||
|
||||
.PARAMETER RunNow
|
||||
After installing, trigger the guardian once (systemctl start) to test it and
|
||||
print the resulting log. Safe: the guardian only starts things, never stops.
|
||||
|
||||
.PARAMETER Uninstall
|
||||
Remove the guardian (disable + delete unit + script). Does NOT clear onboot.
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts\Install-CoolifyAutostart.ps1 -RunNow
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts\Install-CoolifyAutostart.ps1 -VerifyOnly
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$VerifyOnly,
|
||||
[switch]$SkipOnboot,
|
||||
[switch]$RunNow,
|
||||
[switch]$Uninstall
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot\ProxmoxAgent.ps1"
|
||||
|
||||
$config = Get-ProxmoxConfig
|
||||
$lxc = $config.CoolifyLxc
|
||||
$svcName = "coolify-autostart.service"
|
||||
$svcPath = "/etc/systemd/system/$svcName"
|
||||
$binPath = "/usr/local/bin/coolify-autostart.sh"
|
||||
$logPath = "/var/log/coolify-autostart.log"
|
||||
|
||||
$localSh = Join-Path $PSScriptRoot "host\coolify-autostart.sh"
|
||||
$localSvc = Join-Path $PSScriptRoot "host\coolify-autostart.service"
|
||||
|
||||
function Write-Section([string]$Text) {
|
||||
Write-Host ""
|
||||
Write-Host "== $Text ==" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Run a remote command, normalising CRLF -> LF first. Multi-line command strings
|
||||
# authored in this (CRLF) file would otherwise carry a trailing '\r' on every
|
||||
# line, which breaks commands whose last token matters (e.g. unit names).
|
||||
function Invoke-Remote([string]$Command) {
|
||||
Invoke-ProxmoxSshCommand -Command ($Command -replace "`r`n", "`n")
|
||||
}
|
||||
|
||||
# Push text to the host as LF-normalised bytes via base64 (avoids every
|
||||
# quoting/CRLF pitfall of heredocs over SSH). Pass -Content to push a
|
||||
# pre-built string instead of a file.
|
||||
function Push-TextFile {
|
||||
param(
|
||||
[string]$LocalPath,
|
||||
[Parameter(Mandatory)][string]$RemotePath,
|
||||
[string]$Content
|
||||
)
|
||||
if ($Content) { $text = $Content }
|
||||
else { $text = Get-Content -LiteralPath $LocalPath -Raw }
|
||||
$text = $text -replace "`r`n", "`n"
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($text)
|
||||
$b64 = [Convert]::ToBase64String($bytes)
|
||||
Invoke-ProxmoxSshCommand -Command "echo '$b64' | base64 -d > '$RemotePath'"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VERIFY-ONLY
|
||||
# ---------------------------------------------------------------------------
|
||||
if ($VerifyOnly) {
|
||||
Write-Section "Verification (read-only) - LXC $lxc"
|
||||
Invoke-Remote @"
|
||||
echo '--- onboot / startup on LXC $lxc ---'
|
||||
grep -Ei 'onboot|startup' /etc/pve/lxc/$lxc.conf 2>/dev/null || echo '(onboot not set -> defaults to 0, will NOT auto-start)'
|
||||
echo '--- guardian unit ---'
|
||||
systemctl is-enabled $svcName 2>/dev/null || echo '($svcName not installed/enabled)'
|
||||
echo '--- guardian script present? ---'
|
||||
test -x $binPath && echo 'present ($binPath)' || echo 'missing ($binPath)'
|
||||
echo '--- last log lines ---'
|
||||
tail -n 15 $logPath 2>/dev/null || echo '(no log yet)'
|
||||
"@
|
||||
return
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UNINSTALL
|
||||
# ---------------------------------------------------------------------------
|
||||
if ($Uninstall) {
|
||||
Write-Section "Uninstalling guardian (onboot flag left untouched)"
|
||||
Invoke-Remote @"
|
||||
systemctl disable --now $svcName 2>/dev/null || true
|
||||
rm -f $svcPath $binPath
|
||||
systemctl daemon-reload
|
||||
echo 'guardian removed. To also stop auto-start of the LXC: pct set $lxc --onboot 0'
|
||||
"@
|
||||
Write-Host "Done. onboot flag NOT changed." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# INSTALL
|
||||
# ---------------------------------------------------------------------------
|
||||
foreach ($f in @($localSh, $localSvc)) {
|
||||
if (-not (Test-Path -LiteralPath $f)) { throw "Missing source file: $f" }
|
||||
}
|
||||
|
||||
# Bake the configured LXC id into the unit so the guardian targets the right one.
|
||||
$svcText = (Get-Content -LiteralPath $localSvc -Raw) -replace "`r`n", "`n"
|
||||
$svcText = $svcText -replace "(?m)^\[Service\]\n", "[Service]`nEnvironment=COOLIFY_LXC=$lxc`n"
|
||||
|
||||
Write-Section "1/4 Push guardian script + unit to host"
|
||||
Push-TextFile -LocalPath $localSh -RemotePath $binPath
|
||||
Push-TextFile -RemotePath $svcPath -Content $svcText
|
||||
Invoke-Remote "chmod 755 '$binPath' && bash -n '$binPath' && echo 'script pushed + syntax OK'"
|
||||
Write-Host " script -> $binPath" -ForegroundColor Green
|
||||
Write-Host " unit -> $svcPath (COOLIFY_LXC=$lxc)" -ForegroundColor Green
|
||||
|
||||
if (-not $SkipOnboot) {
|
||||
Write-Section "2/4 Enable LXC $lxc onboot (native Proxmox auto-start)"
|
||||
Invoke-Remote "pct set $lxc --onboot 1 --startup order=1,up=30 && echo 'onboot=1 set' && grep -Ei 'onboot|startup' /etc/pve/lxc/$lxc.conf"
|
||||
} else {
|
||||
Write-Section "2/4 Skipping onboot flag (-SkipOnboot)"
|
||||
}
|
||||
|
||||
Write-Section "3/4 Enable guardian on every boot"
|
||||
Invoke-Remote "systemctl daemon-reload && systemctl enable $svcName && echo 'guardian enabled'"
|
||||
|
||||
Write-Section "4/4 Verify"
|
||||
Invoke-Remote @"
|
||||
printf 'onboot: '; grep -Ei 'onboot' /etc/pve/lxc/$lxc.conf 2>/dev/null || echo '(not set)'
|
||||
printf 'guardian enabled: '; systemctl is-enabled $svcName 2>/dev/null || echo 'no'
|
||||
printf 'script executable: '; test -x $binPath && echo yes || echo no
|
||||
"@
|
||||
|
||||
if ($RunNow) {
|
||||
Write-Section "RunNow Triggering guardian once (test)"
|
||||
Invoke-Remote "systemctl start $svcName; sleep 2; systemctl is-active $svcName; echo '--- log ---'; tail -n 25 $logPath"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done. After a power outage the host will auto-start LXC $lxc, Docker, the" -ForegroundColor Green
|
||||
Write-Host "Coolify core containers and the Cloudflare tunnel. Re-run with -VerifyOnly" -ForegroundColor Green
|
||||
Write-Host "to check state, or -Uninstall to remove the guardian." -ForegroundColor Green
|
||||
@@ -0,0 +1,77 @@
|
||||
# Thin wrapper for the Cloudflare API (Bearer token).
|
||||
# Mirrors coolify_skill/scripts/Invoke-CoolifyApi.ps1.
|
||||
# Requires CLOUDFLARE_API_TOKEN (load from .env.local.ps1).
|
||||
# Optional: CLOUDFLARE_API_URL (defaults to the public Cloudflare v4 base).
|
||||
# Useful IDs to keep in env: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ZONE_ID, CLOUDFLARE_TUNNEL_ID.
|
||||
#
|
||||
# Examples:
|
||||
# .\scripts\Invoke-CloudflareApi.ps1 -Path "/user/tokens/verify"
|
||||
# .\scripts\Invoke-CloudflareApi.ps1 -Path "/accounts/$($env:CLOUDFLARE_ACCOUNT_ID)/cfd_tunnel/$($env:CLOUDFLARE_TUNNEL_ID)/configurations"
|
||||
# .\scripts\Invoke-CloudflareApi.ps1 -Method PUT -Path "/accounts/.../cfd_tunnel/.../configurations" -BodyJson $body
|
||||
|
||||
param(
|
||||
[ValidateSet("GET", "POST", "PUT", "PATCH", "DELETE")]
|
||||
[string]$Method = "GET",
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[string]$BodyJson,
|
||||
|
||||
[switch]$Raw
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$baseUrl = if ($env:CLOUDFLARE_API_URL) {
|
||||
$env:CLOUDFLARE_API_URL
|
||||
}
|
||||
else {
|
||||
"https://api.cloudflare.com/client/v4"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:CLOUDFLARE_API_TOKEN)) {
|
||||
throw "Set CLOUDFLARE_API_TOKEN before calling the Cloudflare API."
|
||||
}
|
||||
|
||||
$baseUrl = $baseUrl.TrimEnd("/")
|
||||
$cleanPath = if ($Path.StartsWith("/")) { $Path } else { "/$Path" }
|
||||
$uri = "$baseUrl$cleanPath"
|
||||
|
||||
$headers = @{
|
||||
Authorization = "Bearer $($env:CLOUDFLARE_API_TOKEN)"
|
||||
Accept = "application/json"
|
||||
}
|
||||
|
||||
$request = @{
|
||||
Method = $Method
|
||||
Uri = $uri
|
||||
Headers = $headers
|
||||
TimeoutSec = 30
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey("BodyJson")) {
|
||||
try {
|
||||
$null = $BodyJson | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
throw "BodyJson is not valid JSON: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$request.Body = $BodyJson
|
||||
$request.ContentType = "application/json"
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Invoke-RestMethod @request
|
||||
}
|
||||
catch {
|
||||
throw "Cloudflare API request failed: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($Raw) {
|
||||
$response
|
||||
}
|
||||
else {
|
||||
$response | ConvertTo-Json -Depth 50
|
||||
}
|
||||
@@ -12,4 +12,12 @@ $env:PROXMOX_COOLIFY_LXC = "102"
|
||||
$env:COOLIFY_API_URL = "https://coolify.urieljareth.org/api/v1"
|
||||
$env:COOLIFY_TOKEN = "REPLACE_WITH_COOLIFY_TOKEN"
|
||||
|
||||
# Cloudflare API (optional — for full tunnel/DNS control via scripts\Invoke-CloudflareApi.ps1)
|
||||
# Token perms: Account → Cloudflare Tunnel → Edit, and Zone → DNS → Edit for urieljareth.org
|
||||
$env:CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4"
|
||||
$env:CLOUDFLARE_API_TOKEN = "REPLACE_WITH_CLOUDFLARE_TOKEN"
|
||||
$env:CLOUDFLARE_ACCOUNT_ID = "REPLACE_WITH_ACCOUNT_ID"
|
||||
$env:CLOUDFLARE_ZONE_ID = "REPLACE_WITH_ZONE_ID"
|
||||
$env:CLOUDFLARE_TUNNEL_ID = "5f0e5c5b-a180-46f2-a090-44d151006a62"
|
||||
|
||||
Write-Host "Example Proxmox/Coolify environment loaded. Replace token secrets before API tests."
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Auto-start Coolify LXC + Docker stack + Cloudflare tunnel after boot
|
||||
# Managed by the Proxmox & Coolify Manager repo (scripts/host/coolify-autostart.service).
|
||||
# Run after Proxmox has processed onboot guests and the network is up. This is a
|
||||
# self-healing safety net on top of the LXC onboot flag and Docker restart
|
||||
# policies, not a replacement for them.
|
||||
After=pve-guests.service network-online.target
|
||||
Wants=pve-guests.service network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/coolify-autostart.sh
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# coolify-autostart.sh
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ensures the Coolify stack survives a power outage: on every host boot it
|
||||
# starts LXC 102 (if not already up via onboot), waits for Docker inside the
|
||||
# LXC, and makes sure the core Coolify containers + the Cloudflare tunnel are
|
||||
# running. Idempotent and self-healing: safe to run any number of times.
|
||||
#
|
||||
# Installed by scripts/Install-CoolifyAutostart.ps1 to /usr/local/bin/ and
|
||||
# invoked by the systemd unit coolify-autostart.service on multi-user.target.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -uo pipefail
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
LXC_ID="${COOLIFY_LXC:-102}"
|
||||
LOG="/var/log/coolify-autostart.log"
|
||||
MAX_WAIT="${COOLIFY_MAX_WAIT:-180}" # seconds to wait for docker inside the LXC
|
||||
|
||||
# Dependencies first, then the app, proxy and tunnel. These all normally come
|
||||
# up on their own via Docker restart policies; this loop only heals the ones
|
||||
# that didn't (e.g. restart=no, or a wedged start).
|
||||
CORE_CONTAINERS=(coolify-db coolify-redis coolify-realtime coolify coolify-proxy cloudflared)
|
||||
|
||||
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
|
||||
|
||||
log "=== coolify-autostart start (LXC ${LXC_ID}) ==="
|
||||
|
||||
# 1. Ensure the LXC is running -------------------------------------------------
|
||||
status="$(pct status "$LXC_ID" 2>/dev/null | awk '{print $2}')"
|
||||
if [ "$status" != "running" ]; then
|
||||
log "LXC ${LXC_ID} status='${status:-unknown}' -> starting"
|
||||
if pct start "$LXC_ID"; then
|
||||
log "pct start issued"
|
||||
else
|
||||
log "ERROR: pct start ${LXC_ID} failed"
|
||||
fi
|
||||
else
|
||||
log "LXC ${LXC_ID} already running"
|
||||
fi
|
||||
|
||||
# 2. Wait for the Docker daemon inside the LXC to respond ----------------------
|
||||
waited=0
|
||||
until pct exec "$LXC_ID" -- docker info >/dev/null 2>&1; do
|
||||
if [ "$waited" -ge "$MAX_WAIT" ]; then
|
||||
log "ERROR: docker not ready after ${MAX_WAIT}s -> aborting"
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
done
|
||||
log "docker ready after ${waited}s"
|
||||
|
||||
# 3. Ensure the core Coolify containers + tunnel container are running ---------
|
||||
for c in "${CORE_CONTAINERS[@]}"; do
|
||||
st="$(pct exec "$LXC_ID" -- docker inspect -f '{{.State.Running}}' "$c" 2>/dev/null || echo missing)"
|
||||
case "$st" in
|
||||
true) log "container ${c}: running" ;;
|
||||
missing) log "WARN container ${c}: not found (skipping)" ;;
|
||||
*)
|
||||
log "container ${c}: running=${st} -> starting"
|
||||
if pct exec "$LXC_ID" -- docker start "$c" >/dev/null 2>&1; then
|
||||
log "started ${c}"
|
||||
else
|
||||
log "ERROR: could not start ${c}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 4. Ensure the systemd cloudflared tunnel inside the LXC is up ----------------
|
||||
# (second connector to the same tunnel; belt-and-suspenders alongside the
|
||||
# cloudflared Docker container above.)
|
||||
if pct exec "$LXC_ID" -- systemctl is-enabled cloudflared >/dev/null 2>&1; then
|
||||
if pct exec "$LXC_ID" -- systemctl start cloudflared >/dev/null 2>&1; then
|
||||
log "cloudflared.service ensured up"
|
||||
else
|
||||
log "WARN: cloudflared.service start returned non-zero"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "=== coolify-autostart done ==="
|
||||
Reference in New Issue
Block a user