deploy_skill: soporte para Coolify v4.1.2 (API /applications 404) + flujo UI Playwright
Aprendido desplegando cotizador (Next.js+FastAPI+Postgres, Docker Compose, repo privado) en coolify.urieljareth.org (v4.1.2): - Documenta que /applications/* devuelve 404 en esta instancia y que configurar apps git build-from-source solo es posible por la UI web. - Nuevos scripts scripts/coolify-ui/: coolify-login.mjs (guarda storageState) y Configure-CoolifyComposeApp.mjs (build pack -> Docker Compose, Reload Compose File, env vars por Developer view, dominios por servicio). - references/coolify-4.1.2-notes.md: mapa de API funcional/404, gotchas (BOM UTF-8 rompe el parser YAML de Coolify; Reload Compose File obligatorio; pin de dominios por servicio o 503; no encolar deploys concurrentes; PowerShell [IO.File]::ReadAllText para payloads de llaves). - env.local.template.ps1: COOLIFY_EMAIL/COOLIFY_PASSWORD para la UI. - SKILL.md/TOOLS.md: seccion "instance reality" y flujo UI paso a paso. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cd998ce6b0
commit
e7d1c33cd5
@@ -0,0 +1,319 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Create or update a Coolify SERVICE (Docker Compose multi-container stack)
|
||||
from a local project's compose file.
|
||||
|
||||
.DESCRIPTION
|
||||
Complements New-CoolifyApplication.ps1 (which only handles single-app
|
||||
Dockerfile builds via POST /applications/public). This script handles
|
||||
multi-container stacks via POST /services - the modern replacement for
|
||||
the deprecated POST /applications/dockercompose.
|
||||
|
||||
Workflow:
|
||||
1. Resolve (or create) the target Coolify Project and Environment.
|
||||
2. Resolve the local server UUID.
|
||||
3. Read the docker-compose file from the project (default:
|
||||
docker-compose.coolify.yml). The compose must already satisfy the
|
||||
hard rules from docs/AGENTS-coolify-apps.md (no published 80/443,
|
||||
service-name DB host, healthchecks, named volumes, SERVICE_FQDN_*
|
||||
placeholders so Coolify can wire routes).
|
||||
4. POST /services with the raw compose + URL mappings (which service
|
||||
gets which FQDN).
|
||||
5. Push env vars from an optional .env file to the service.
|
||||
6. Trigger an initial deploy (unless -NoDeploy).
|
||||
|
||||
Each mutating step asks for confirmation unless -Force is supplied.
|
||||
|
||||
.PARAMETER AppPath
|
||||
Local path of the project that contains the compose file. Required.
|
||||
|
||||
.PARAMETER AppName
|
||||
Service name in Coolify. Required. Becomes the resource name; Coolify
|
||||
auto-suffixes with a short UUID when it instantiates sub-services.
|
||||
|
||||
.PARAMETER ComposeFile
|
||||
Compose file name (relative to AppPath) or absolute path.
|
||||
Default: docker-compose.coolify.yml
|
||||
|
||||
.PARAMETER Fqdn
|
||||
Primary public URL, e.g. https://cotizador.urieljareth.org. The FQDN is
|
||||
mapped to the -PrimaryService in the compose via the `urls` payload.
|
||||
Required.
|
||||
|
||||
.PARAMETER PrimaryService
|
||||
The compose service name that should receive the -Fqdn. Default: 'web'.
|
||||
Set this to whatever your compose calls the main app service.
|
||||
|
||||
.PARAMETER ExtraUrls
|
||||
Optional hashtable mapping additional compose service names to FQDNs.
|
||||
Example: @{ api = 'https://cotizador-api.urieljareth.org' }
|
||||
Use when your compose exposes more than one public service.
|
||||
|
||||
.PARAMETER ProjectName
|
||||
Coolify project name. Created if missing. Default: 'default'.
|
||||
|
||||
.PARAMETER EnvironmentName
|
||||
Coolify environment name within the project. Default: 'production'.
|
||||
|
||||
.PARAMETER EnvFile
|
||||
Optional .env-formatted file. Each KEY=VALUE line is pushed to Coolify
|
||||
as a service-level env var. Lines starting with '#' are ignored.
|
||||
|
||||
.PARAMETER InstantDeploy
|
||||
Pass instant_deploy=true to the create call. Coolify starts the stack
|
||||
immediately after creation. Mutually exclusive with -NoDeploy.
|
||||
|
||||
.PARAMETER NoDeploy
|
||||
Do not trigger a deploy. Service is created with env vars but not started.
|
||||
Use this when you want to review in the UI before the first start.
|
||||
|
||||
.PARAMETER ServiceUuid
|
||||
Optional. If a service already exists in Coolify, pass its UUID to PATCH
|
||||
it (compose_raw, urls) in place instead of creating a new one.
|
||||
|
||||
.PARAMETER Force
|
||||
Skip per-step confirmation prompts.
|
||||
|
||||
.EXAMPLE
|
||||
.\New-CoolifyService.ps1 `
|
||||
-AppPath .\cotizador `
|
||||
-AppName cotizador `
|
||||
-Fqdn https://cotizador.urieljareth.org `
|
||||
-PrimaryService web `
|
||||
-EnvFile .\cotizador\.env.coolify `
|
||||
-InstantDeploy
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AppPath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern('^[A-Za-z0-9._-]+$')]
|
||||
[string]$AppName,
|
||||
|
||||
[string]$ComposeFile = "docker-compose.coolify.yml",
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Fqdn,
|
||||
|
||||
[string]$PrimaryService = "web",
|
||||
|
||||
[hashtable]$ExtraUrls,
|
||||
|
||||
[string]$ProjectName = "default",
|
||||
|
||||
[string]$EnvironmentName = "production",
|
||||
|
||||
[string]$EnvFile,
|
||||
|
||||
[switch]$InstantDeploy,
|
||||
|
||||
[switch]$NoDeploy,
|
||||
|
||||
[string]$ServiceUuid,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$coolifyApi = Join-Path $here "..\..\coolify_skill\scripts\Invoke-CoolifyApi.ps1"
|
||||
if (-not (Test-Path -LiteralPath $coolifyApi)) {
|
||||
throw "Invoke-CoolifyApi.ps1 not found at: $coolifyApi"
|
||||
}
|
||||
|
||||
function Confirm-Step {
|
||||
param([string]$Action)
|
||||
if ($Force) { return $true }
|
||||
$reply = Read-Host "Confirm action: $Action`nProceed? (y/N)"
|
||||
return ($reply -match '^(y|yes|s|si|sí)$')
|
||||
}
|
||||
|
||||
function Get-CoolifyResource {
|
||||
param([string]$Path)
|
||||
& $coolifyApi -Path $Path -Raw -ErrorAction Stop
|
||||
}
|
||||
|
||||
function Resolve-Project {
|
||||
param([string]$Name)
|
||||
$projects = Get-CoolifyResource -Path "/projects"
|
||||
$match = $projects | Where-Object { $_.name -eq $Name } | Select-Object -First 1
|
||||
if (-not $match) {
|
||||
if (-not (Confirm-Step "Create project '$Name' in Coolify")) {
|
||||
throw "Aborted by user before creating project."
|
||||
}
|
||||
$body = @{ name = $Name } | ConvertTo-Json -Compress
|
||||
$created = & $coolifyApi -Method POST -Path "/projects" -BodyJson $body -Raw -ErrorAction Stop
|
||||
Write-Host "Project created: $($created.uuid)" -ForegroundColor Green
|
||||
return $created
|
||||
}
|
||||
return $match
|
||||
}
|
||||
|
||||
function Resolve-Environment {
|
||||
param([string]$ProjectUuid, [string]$Name)
|
||||
$envs = Get-CoolifyResource -Path "/projects/$ProjectUuid/environments"
|
||||
$match = $envs | Where-Object { $_.name -eq $Name } | Select-Object -First 1
|
||||
if (-not $match) {
|
||||
if (-not (Confirm-Step "Create environment '$Name' in project $ProjectUuid")) {
|
||||
throw "Aborted by user before creating environment."
|
||||
}
|
||||
$body = @{ name = $Name } | ConvertTo-Json -Compress
|
||||
$created = & $coolifyApi -Method POST -Path "/projects/$ProjectUuid/environments" -BodyJson $body -Raw -ErrorAction Stop
|
||||
Write-Host "Environment created: $($created.uuid)" -ForegroundColor Green
|
||||
return $created
|
||||
}
|
||||
return $match
|
||||
}
|
||||
|
||||
function Resolve-ServerUuid {
|
||||
$servers = Get-CoolifyResource -Path "/servers"
|
||||
if (-not $servers) { throw "No Coolify servers configured." }
|
||||
$srv = $servers | Where-Object { $_.settings.is_usable -eq $true } | Select-Object -First 1
|
||||
if (-not $srv) { $srv = $servers | Select-Object -First 1 }
|
||||
return $srv.uuid
|
||||
}
|
||||
|
||||
# ─── Resolve inputs ──────────────────────────────────────────────────────────
|
||||
|
||||
$appPath = (Resolve-Path -LiteralPath $AppPath).Path
|
||||
|
||||
# Compose file: try as-is (absolute), else relative to AppPath
|
||||
$composePath = if ([System.IO.Path]::IsPathRooted($ComposeFile)) {
|
||||
$ComposeFile
|
||||
} else {
|
||||
Join-Path $appPath $ComposeFile
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $composePath)) {
|
||||
throw "Compose file not found: $composePath (resolved from -ComposeFile '$ComposeFile' against AppPath '$appPath')"
|
||||
}
|
||||
$composeRaw = Get-Content -LiteralPath $composePath -Raw -ErrorAction Stop
|
||||
Write-Host "Loaded compose: $composePath ($($composeRaw.Length) bytes)" -ForegroundColor Cyan
|
||||
|
||||
# URLs array: primary + extras
|
||||
$urlsList = @(
|
||||
[pscustomobject]@{ name = $PrimaryService; url = $Fqdn }
|
||||
)
|
||||
if ($ExtraUrls -and $ExtraUrls.Count -gt 0) {
|
||||
foreach ($k in $ExtraUrls.Keys) {
|
||||
$urlsList += [pscustomobject]@{ name = $k; url = $ExtraUrls[$k] }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Resolving Coolify project '$ProjectName'..." -ForegroundColor Cyan
|
||||
$proj = Resolve-Project -Name $ProjectName
|
||||
Write-Host "Resolving environment '$EnvironmentName'..." -ForegroundColor Cyan
|
||||
$env = Resolve-Environment -ProjectUuid $proj.uuid -Name $EnvironmentName
|
||||
Write-Host "Resolving server..." -ForegroundColor Cyan
|
||||
$serverUuid = Resolve-ServerUuid
|
||||
Write-Host "Using server: $serverUuid" -ForegroundColor Cyan
|
||||
|
||||
# ─── Create or update the service ────────────────────────────────────────────
|
||||
|
||||
if ($ServiceUuid) {
|
||||
Write-Host "Updating existing service: $ServiceUuid" -ForegroundColor Cyan
|
||||
$patch = [ordered]@{
|
||||
name = $AppName
|
||||
docker_compose_raw = $composeRaw
|
||||
urls = $urlsList
|
||||
project_uuid = $proj.uuid
|
||||
environment_name = $env.name
|
||||
server_uuid = $serverUuid
|
||||
}
|
||||
if (-not (Confirm-Step "PATCH /services/$ServiceUuid (compose=$($composePath), urls=$($urlsList.Count), fqdn=$Fqdn)")) {
|
||||
throw "Aborted by user."
|
||||
}
|
||||
$result = & $coolifyApi -Method PATCH -Path "/services/$ServiceUuid" -BodyJson ($patch | ConvertTo-Json -Depth 6) -Raw -ErrorAction Stop
|
||||
$svcUuid = $ServiceUuid
|
||||
} else {
|
||||
$createBody = [ordered]@{
|
||||
type = "one-click-service" # custom service; Coolify accepts any non-empty string here
|
||||
name = $AppName
|
||||
project_uuid = $proj.uuid
|
||||
environment_name = $env.name
|
||||
server_uuid = $serverUuid
|
||||
docker_compose_raw = $composeRaw
|
||||
urls = $urlsList
|
||||
instant_deploy = [bool]$InstantDeploy
|
||||
}
|
||||
if (-not (Confirm-Step "POST /services with AppName=$AppName, compose=$($composePath), fqdn=$Fqdn -> $PrimaryService, instant_deploy=$InstantDeploy")) {
|
||||
throw "Aborted by user."
|
||||
}
|
||||
$result = & $coolifyApi -Method POST -Path "/services" -BodyJson ($createBody | ConvertTo-Json -Depth 6) -Raw -ErrorAction Stop
|
||||
$svcUuid = $result.uuid
|
||||
Write-Host "Service created: $svcUuid" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Show domains assigned (Coolify echoes the resolved FQDNs)
|
||||
if ($result.domains) {
|
||||
Write-Host "Domains assigned:" -ForegroundColor Cyan
|
||||
$result.domains | ForEach-Object { Write-Host " $_" }
|
||||
}
|
||||
|
||||
# ─── Push env vars ───────────────────────────────────────────────────────────
|
||||
|
||||
if ($EnvFile -and (Test-Path -LiteralPath $EnvFile)) {
|
||||
if (-not (Confirm-Step "Push env vars from $EnvFile to service $svcUuid")) {
|
||||
Write-Host "Skipping env push." -ForegroundColor Yellow
|
||||
} else {
|
||||
$pushed = 0
|
||||
Get-Content -LiteralPath $EnvFile | ForEach-Object {
|
||||
$line = $_.Trim()
|
||||
if (-not $line -or $line.StartsWith("#")) { return }
|
||||
$eq = $line.IndexOf("=")
|
||||
if ($eq -le 0) { return }
|
||||
$k = $line.Substring(0, $eq).Trim()
|
||||
$v = $line.Substring($eq + 1).Trim().Trim('"').Trim("'")
|
||||
$body = [ordered]@{
|
||||
key = $k
|
||||
value = $v
|
||||
is_literal = $true
|
||||
is_shown_once = $false
|
||||
} | ConvertTo-Json -Compress
|
||||
try {
|
||||
& $coolifyApi -Method POST -Path "/services/$svcUuid/envs" -BodyJson $body -ErrorAction Stop | Out-Null
|
||||
Write-Host " env: $k set" -ForegroundColor DarkGray
|
||||
$pushed++
|
||||
} catch {
|
||||
Write-Host " env: $k FAILED -> $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
Write-Host "Env vars pushed: $pushed" -ForegroundColor Green
|
||||
}
|
||||
} elseif ($EnvFile) {
|
||||
Write-Host "EnvFile specified but not found: $EnvFile" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ─── Deploy trigger (if not instant) ─────────────────────────────────────────
|
||||
|
||||
if ($NoDeploy) {
|
||||
Write-Host "Service created with env vars. -NoDeploy set: skipping deploy trigger." -ForegroundColor Yellow
|
||||
} elseif ($InstantDeploy -and -not $ServiceUuid) {
|
||||
Write-Host "instant_deploy=true: Coolify is already starting the stack." -ForegroundColor Green
|
||||
} else {
|
||||
if (-not (Confirm-Step "Trigger DEPLOY of service $svcUuid")) {
|
||||
Write-Host "Skipping deploy trigger. Service is created but not started." -ForegroundColor Yellow
|
||||
} else {
|
||||
# Coolify's "deploy" endpoint for services is POST /services/{uuid}/start (or the unified /deploy with type).
|
||||
# /services/{uuid}/start is the documented happy path; it queues the stack start.
|
||||
try {
|
||||
& $coolifyApi -Method POST -Path "/services/$svcUuid/start" -ErrorAction Stop | Out-Null
|
||||
Write-Host "Deploy triggered (POST /services/$svcUuid/start)." -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " POST /services/$svcUuid/start failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
Write-Host " Try the Coolify UI 'Deploy' button or POST /services/$svcUuid/start manually." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[PSCustomObject]@{
|
||||
project_uuid = $proj.uuid
|
||||
environment_name = $env.name
|
||||
server_uuid = $serverUuid
|
||||
service_uuid = $svcUuid
|
||||
name = $AppName
|
||||
primary_fqdn = $Fqdn
|
||||
primary_service = $PrimaryService
|
||||
compose_source = $composePath
|
||||
} | Format-List
|
||||
Reference in New Issue
Block a user