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:
urieljareth
2026-07-08 00:42:12 -06:00
co-authored by Claude Opus 4.8
parent cd998ce6b0
commit e7d1c33cd5
22 changed files with 2935 additions and 0 deletions
@@ -0,0 +1,103 @@
# Coolify v4.1.2 on this instance — hard realities (learned the hard way)
> Written while deploying `cotizador` (Next.js + FastAPI + Postgres, Docker Compose
> build pack, private repo). These are behaviors of the **specific** instance at
> `https://coolify.urieljareth.org`, verified empirically with a root-team token.
> Re-check with `GET /api/v1/version` — if it changed, re-verify the API surface.
## 1. The REST API surface is PARTIAL — `/applications/*` is 404
With a valid **root-team** token (`GET /teams/current` → "Root Team"):
| Endpoint | Result |
|----------|--------|
| `GET /version`, `GET /teams/current` | 200 |
| `GET /projects`, `POST /projects` | 200 / 201 |
| `GET /projects/{uuid}/environments` | 200 |
| `GET /servers` | 200 |
| `POST /security/keys` (create private key) | 201 |
| `GET /resources` | 200 — **the only reliable app/service/db inventory** |
| `GET /services`, `POST /services`, `/services/{uuid}/envs`, `/start` | work |
| `GET /deploy?uuid=<uuid>[&force=true]` | 200 — **triggers a deploy of any existing resource by uuid** |
| `GET /deployments/{deployment_uuid}` | 200 — status + logs (logs = JSON string) |
| **`/applications` (list)** | **404** |
| **`/applications/{uuid}` (get / PATCH)** | **404** |
| **`/applications/{uuid}/envs` (list/create)** | **404** |
| **`/applications/{uuid}/logs`** | **404** |
| **`POST /applications/public`** | **404** |
| **`POST /applications/dockerfile`** | **404** |
| **`POST /applications/private-deploy-key`** | **404** |
| **`POST /applications/private-github-app`** | **404** |
| **`POST /applications/dockercompose`** | **404** (deprecated upstream anyway) |
| `GET /github-apps` | 404 |
**Consequence:** you CANNOT create OR configure a **git-based application that builds
from source** (Dockerfile or Docker Compose build pack) via the API here. The skill's
`New-CoolifyApplication.ps1` (POST /applications/public) will 404 on this instance.
`POST /services` only accepts `docker_compose_raw` with **no git repo** → only good for
**prebuilt-image** stacks, not `build:`-from-source composes.
→ For git+build apps you MUST use the **web UI** (drive it with Playwright). Application
config (build pack, env vars, domains) is UI-only. Only the **deploy trigger** is API-able
(`GET /deploy?uuid=`), and reads via `/resources` + `/deployments/{uuid}`.
## 2. Inventory & status via `/resources`
`GET /resources` returns every application/service/database with `uuid`, `name`,
`status` (e.g. `running:healthy`, `exited:unhealthy`), `fqdn`, `build_pack`,
`git_repository`, `docker_compose_location`, `docker_compose_raw`, `docker_compose`
(parsed), `source_type`/`source_id`, `environment_id`, `destination`. Map an app's
`environment_id` to a project by scanning `/projects/{uuid}/environments`.
Build the UI URL as:
`https://coolify.urieljareth.org/project/{project_uuid}/environment/{env_uuid}/application/{app_uuid}`
(+ `/environment-variables`, `/logs`, `/deployment`, `/source`, `/danger`, …)
## 3. Driving the UI with Playwright (Livewire app)
- Login: `/login`, fill `input[type=email]` + `input[type=password]`, submit; save
`storageState` and reuse it (see `scripts/coolify-ui/coolify-login.mjs`).
- Import Playwright from the manager repo when the script lives elsewhere:
`createRequire('<manager>/package.json')` then `require('playwright')`.
- Dismiss recurring popups: buttons "Accept and Close", "Acknowledge & Disable This Popup",
"Maybe next time".
- **Build Pack** is a `<select wire:model="buildPack">`; robust locator
`select:has(option[value="dockercompose"])`; `selectOption('dockercompose')`.
- **After switching build pack to Docker Compose you MUST click "Reload Compose File"**
so Coolify fetches+parses+persists the compose from the repo. Otherwise the deploy
crashes: `Symfony\Component\Yaml\Yaml::parse(): Argument #1 must be of type string,
null given` (the parsed `docker_compose` field is null). Verify via `/resources`:
`docker_compose` must be non-null after reload.
- **Env vars**: Environment Variables tab → "Developer view" → fill the big textarea with
`KEY=VALUE` lines → click **"Save All Environment Variables"** (NOT a generic "Save").
Re-saving replaces the whole set, so always send the full list.
- **Pin a compose service's public domain** with a magic env var:
`SERVICE_FQDN_<SERVICE>_<PORT>=https://host` (e.g. `SERVICE_FQDN_WEB_3000=https://cotizador.urieljareth.org`).
The compose should also declare a bare `- SERVICE_FQDN_WEB_3000` in that service's env.
- **Deploy**: prefer API `GET /deploy?uuid=&force=true` (works even though app CRUD is 404),
or the UI "Deploy" button.
## 4. Deploy pitfalls seen
- **UTF-8 BOM / mojibake in the compose file breaks Coolify's YAML parser** → `docker_compose`
stays null → `Yaml::parse(null)` crash. Keep compose files clean UTF-8, **no BOM**.
Check: `head -c3 file | xxd` must NOT be `ef bb bf`.
- **Don't queue multiple deploys.** Triggering a new deploy while one is building **cancels**
the in-progress one → the build dies with `exit code 255` + "Gracefully shutting down build
container". Trigger ONE deploy and let it finish. Check `/deployments` (running list) is empty
before triggering.
- `force=true` adds `--no-cache --pull` (slow full rebuild); omit `force` to reuse BuildKit
cache and cut build time / cancellation risk.
- Coolify's `/resources` `status` can lag reality (showed `running:healthy` while Traefik
returned 503). Always confirm with an actual HTTP request to the FQDN + a browser render
(`Test-ServiceOnline.ps1`).
## 5. PowerShell 5.1 gotcha (secret payloads → API)
`Get-Content -Raw` returns a string decorated with ETS NoteProperties; `ConvertTo-Json` then
serializes it as `{"value":"..."}`, so Coolify rejects e.g. `private_key` ("must be a string").
FIX: read with `[System.IO.File]::ReadAllText($path)`.
## 6. Deploy-key path (kept as fallback / for instances where the API is complete)
1. `ssh-keygen -t ed25519`.
2. `POST /security/keys` with `private_key` via `ReadAllText`.
3. Add the public key as a GitHub deploy key: `POST /repos/{owner}/{repo}/keys` `{title,key,read_only:true}`.
4. `POST /applications/private-deploy-key` with `build_pack=dockercompose`,
`docker_compose_location`, `docker_compose_domains`, `ports_exposes`, `private_key_uuid`,
`[email protected]:owner/repo.git`. ← **404 on v4.1.2**; use the UI instead.
+72
View File
@@ -0,0 +1,72 @@
# End-to-End Deploy Flow
This is the canonical sequence the deploy_skill walks a project through.
Every box is a script in `deploy_skill/scripts/`. Diamonds are decisions the
operator (or the agent on behalf of the operator) makes.
```
┌──────────────────────────┐
│ local project dir │
│ (with source code) │
└────────────┬─────────────┘
┌─────────────────▼──────────────────┐
│ Initialize-CoolifyProject.ps1 │
│ -Stack node|python|compose|static │
│ -AppPort <N> │
│ writes Dockerfile, compose, │
│ .dockerignore, .gitignore additions │
└─────────────────┬──────────────────┘
┌─────────────────▼──────────────────┐
│ Test-PreDeployChecklist.ps1 -Strict│
│ enforces AGENTS-coolify-apps.md │
└────────────┬───────────────┬────────┘
FAIL ◄──┤ ├── PASS
│ │
STOP + report │
┌────────────────────────────▼────────┐
│ New-GitHubRepo.ps1 │
│ -Name <app> │
│ uses GITHUB_TOKEN (PAT) │
└────────────────────────────┬─────────┘
┌────────────────────────────▼─────────┐
│ git init / add / commit / push │
│ uses wincred (NOT the PAT) │
└────────────────────────────┬─────────┘
┌────────────────────────────▼─────────┐
│ New-CoolifyApplication.ps1 │
│ - resolve project (+ create) │
│ - resolve environment (+ create) │
│ - resolve server │
│ - POST /applications/public │
│ - POST /applications/<uuid>/envs │
│ - POST /deploy │
└────────────────────────────┬─────────┘
┌────────────────────────────▼─────────┐
│ Test-PostDeploy.ps1 │
│ - curl -I https://<fqdn> │
│ - docker exec getent hosts <svc> │
│ - coolify-proxy logs grep │
│ - /applications/<uuid> (API) │
└────────────────────────────┬─────────┘
LIVE on FQDN ◄
```
`Publish-ProjectToCoolify.ps1` chains all six steps with one confirmation
prompt each (or `-Force` for unattended runs).
## Branching the flow
- **Already-deployed app, just env change** → skip steps 1, 3, 4; call
`New-CoolifyApplication.ps1 -ApplicationUuid <uuid>` (it PATCHes instead of
creating).
- **Rollback to old commit** → `Invoke-CoolifyRollback.ps1`; pushes a
`rollback/<sha>` branch and repoints the app, leaving `main` intact.
- **No code yet, only validate ideas** → just run
`Test-PreDeployChecklist.ps1` against a skeleton.
@@ -0,0 +1,26 @@
# Minimal compliant Dockerfile for a Node app deployed to this Coolify instance.
#
# Hard rules followed (see docs/AGENTS-coolify-apps.md):
# - No published host ports 80/443 (Traefik routes them).
# - Real listening port is set via ports_exposes in Coolify (here 8080).
# - Healthcheck endpoint must NOT depend on the DB being reachable at boot.
#
# Replace the build/run commands with the ones that match your framework.
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
COPY --from=build /app /app
# Cheap endpoint; implement /health in your app to return 200 without DB.
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=20s \
CMD wget -qO- http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["npm", "start"]
@@ -0,0 +1,24 @@
# Minimal compliant Dockerfile for a Python app deployed to this Coolify instance.
#
# Hard rules followed (see docs/AGENTS-coolify-apps.md):
# - No published host ports 80/443 (Traefik routes them).
# - Real listening port is set via ports_exposes in Coolify (here 8080).
# - Healthcheck endpoint must NOT depend on the DB being reachable at boot.
FROM python:3.12-slim AS build
WORKDIR /app
ENV PIP_NO_CACHE_DIR=1
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
FROM python:3.12-slim AS run
WORKDIR /app
ENV PYTHONUNBUFFERED=1
ENV PORT=8080
COPY --from=build /install /usr/local
COPY . .
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=20s \
CMD python -c "import urllib.request,sys; urllib.request.urlopen('http://localhost:8080/health'); sys.exit(0)" || exit 1
EXPOSE 8080
# Adjust to your framework: gunicorn, uvicorn, fastapi, etc.
CMD ["gunicorn", "-b", "0.0.0.0:8080", "app:app"]
@@ -0,0 +1,52 @@
# Reference docker-compose for a multi-container app on this Coolify instance.
#
# Implements EVERY hard rule from docs/AGENTS-coolify-apps.md:
# 2.1 App reaches DB by Docker service name (app-db), never localhost.
# 2.2 Joins the external 'coolify' network so it can resolve shared services.
# 2.3 Does NOT publish 80/443 (owned by coolify-proxy). Traefik routes.
# 2.4 FQDN is set in Coolify (https://<name>.urieljareth.org) before deploy.
# 2.5 Secrets come as env vars from Coolify. None are baked in here.
# 4 Healthcheck on a DB-independent endpoint.
# 5 Named volumes for data that must survive redeploys.
services:
app:
build: .
environment:
DB_HOST: app-db
DB_PORT: "5432"
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
APP_PUBLIC_URL: https://app.urieljareth.org
expose:
- "8080"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
networks:
- coolify
depends_on:
- app-db
app-db:
image: postgres:16
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- app-db-data:/var/lib/postgresql/data
networks:
- coolify
volumes:
app-db-data: {}
networks:
coolify:
name: coolify
external: true
@@ -0,0 +1,31 @@
# Reference .env file. Copy to .env.local.ps1 and fill values.
#
# IMPORTANT: .env.local.ps1 is gitignored (see .gitignore). NEVER commit it.
# The .env.example file in the repo root shows the publicly-safe keys.
# Proxmox (already configured)
$env:PROXMOX_HOST = "192.168.0.200"
$env:PROXMOX_NODE = "thinkcentre"
$env:PROXMOX_USER = "root"
$env:PROXMOX_SSH_KEY = "C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win"
$env:PROXMOX_API_BASE_URL = "https://192.168.0.200:8006/api2/json"
$env:PROXMOX_API_TOKEN_ID = "root@pam!openclaw"
$env:PROXMOX_API_TOKEN_SECRET = "REPLACE_WITH_TOKEN_SECRET"
$env:PROXMOX_COOLIFY_LXC = "102"
# Coolify
$env:COOLIFY_API_URL = "https://coolify.urieljareth.org/api/v1"
$env:COOLIFY_TOKEN = "REPLACE_WITH_COOLIFY_TOKEN"
# Coolify UI login (email/password) — REQUIRED for Playwright-driven UI operations.
# This instance (v4.1.2) does NOT expose the /applications/* REST API (all 404),
# so configuring a git-based / Docker-Compose application (build pack, env vars,
# domains) can ONLY be done through the web UI. See deploy_skill/references/coolify-4.1.2-notes.md.
$env:COOLIFY_EMAIL = "REPLACE_WITH_COOLIFY_UI_EMAIL"
$env:COOLIFY_PASSWORD = "REPLACE_WITH_COOLIFY_UI_PASSWORD"
# GitHub (for deploy_skill: New-GitHubRepo.ps1, Invoke-GitHubApi.ps1)
# Generate at: https://github.com/settings/tokens
# Scopes needed: repo (classic) OR Contents:Read+Write, Metadata:Read (fine-grained).
# The token is used ONLY for API calls (create repo, set deploy key, etc.).
# git push/pull uses Windows Credential Manager (wincred) instead.
$env:GITHUB_TOKEN = "REPLACE_WITH_GITHUB_PAT"