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:
urieljareth
2026-07-18 11:31:31 -06:00
parent e7d1c33cd5
commit f587a8baa2
23 changed files with 2312 additions and 2 deletions
+268
View File
@@ -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."
+166
View File
@@ -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
+77
View File
@@ -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
}
+8
View File
@@ -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."
+17
View File
@@ -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
+81
View File
@@ -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 ==="