Actualiza toolkit operativo y documentación

This commit is contained in:
urieljareth
2026-09-10 20:53:50 -06:00
parent 3b7209dcc1
commit 714057bfc8
69 changed files with 6023 additions and 384 deletions
+510 -3
View File
@@ -7,6 +7,12 @@
## 1. The REST API surface is PARTIAL — `/applications/*` is 404
> **CORRECCIÓN (2026-08-29, §11):** ese 404 lo imponía **Cloudflare en el
> hostname público, no Coolify**. Contra el origen
> (`http://192.168.0.117:8000/api/v1`) el namespace completo responde 200 y el
> `openapi.yaml` del contenedor declara toda la superficie. Esta sección y la
> tabla se conservan como registro histórico del diagnóstico de 2026-08-07.
With a valid **root-team** token (`GET /teams/current` → "Root Team"):
| Endpoint | Result |
@@ -114,7 +120,7 @@ Realtime, Storage, Kong, Studio) end-to-end. New, reusable facts:
### 7.2 Locally-built images → Coolify's deploy `pull`s and fails
- Coolify's service deploy runs `docker compose pull` → `pull access denied ... repository does not exist` for a local-only image tag. **Don't use Coolify's Deploy button/`/start` for local images.**
- Instead build the image on the server (clone repo + `docker build -t <tag>`), then `docker compose up -d` **manually** in `/data/coolify/services/<uuid>/` (default pull policy skips pull when the image exists locally). Same pattern as `Deploy-SoloLeveling.ps1`.
- Instead build the image on the server (clone repo + `docker build -t <tag>`), then `docker compose up -d` **manually** in `/data/coolify/services/<uuid>/` (default pull policy skips pull when the image exists locally). Same pattern as `scripts/apps/Deploy-SoloLeveling.ps1`.
- Coolify's normalized on-disk compose (from your base64 raw) **preserves** your `image`, inlined `environment`, custom `labels` (incl. Traefik) and networks — but **renames containers to `<service>-<uuid>`**. Cross-container refs must use the *other* service's real name (unchanged), not the renamed one.
- The service dir + its per-service external network `<uuid>` are created only on Coolify's own deploy. For a first manual `up`, run `docker network create --attachable <uuid>` first (else `network <uuid> declared as external, but could not be found`). Put app containers on the external `coolify` network to reach other stacks (Traefik `coolify-proxy` is already on it).
@@ -141,7 +147,7 @@ Every deploy script here sets `$ErrorActionPreference = "Stop"` and shells out t
kills the script on its first line of benign progress (e.g. git `Cloning into 'repo'...`).
Symptom: exit 1 with the error anchored at `& ssh @sshArgs` in `ProxmoxAgent.ps1`, right after the
first `git`/`ssh` progress line — **before any real work fails**.
- **Fix:** invoke the script **plain** — `& .\Deploy-SoloLeveling.ps1` (no `2>&1`/`*>&1`, no
- **Fix:** invoke the script **plain** — `& .\scripts\apps\Deploy-SoloLeveling.ps1` (no `2>&1`/`*>&1`, no
merging pipe). The harness/terminal already captures the process's stderr at the OS level, which
does **not** create ErrorRecords. Same rule for `Publish-ProjectToCoolify.ps1`,
`New-CoolifyService.ps1`, `Invoke-CoolifyRollback.ps1`, `Test-ServiceOnline.ps1`.
@@ -150,7 +156,7 @@ first `git`/`ssh` progress line — **before any real work fails**.
- Note the scripts' *internal* `git push 2>&1 | ForEach-Object {…}` is fine (scoped to one
statement); the trap is the **outer** redirect the caller adds.
### 7.8 build-on-server re-deploy: verification + rollback caveats (Solo Leveling, `Deploy-SoloLeveling.ps1`)
### 7.8 build-on-server re-deploy: verification + rollback caveats (Solo Leveling, `scripts/apps/Deploy-SoloLeveling.ps1`)
- **Confirm the shipped commit, not just health.** The build step echoes `HEAD: <sha> <subject>`
from the fresh clone — gate on it matching your intended commit, and confirm the app container
shows **`Recreated`** (not reused) in `docker compose up -d` output. Health 200 alone only proves
@@ -162,3 +168,504 @@ first `git`/`ssh` progress line — **before any real work fails**.
If instant rollback matters, tag per-commit too (`:<sha>` alongside `:latest`) so you can retag
`:latest` to a prior digest and `docker compose up -d`. (The skill's `Invoke-CoolifyRollback.ps1`
assumes the git-build path, which is `/applications/*` = 404 here — it doesn't apply to build-on-server.)
## 8. Creating git-based apps via direct DB INSERT (when UI credentials are unavailable)
> Verified 2026-07-27 deploying `AgendaMax` (Node 22 + Vite + Express + SQLite,
> Dockerfile build pack, GitHub source). This is the **third** path, alongside
> §3 (Playwright UI) and §6 (API, 404 here). Use it when you have SSH+DB access
> but NOT the Coolify web UI email/password.
### 8.1 The SSH → pct → docker exec → psql chain
```
local machine
→ ssh [email protected] -i keys\proxmox_ed25519
→ pct exec 102 -- docker exec coolify-db psql -U coolify -d coolify -c "SQL_HERE"
```
**Credentials (all local/private, no secrets leave the host):**
- Proxmox host: `[email protected]`
- SSH key: `keys\proxmox_ed25519`
- Coolify LXC: `102`
- Coolify DB container: `coolify-db` (PostgreSQL 15, user `coolify`, db `coolify`, no password — container-internal)
- Coolify app container: `coolify` (image `ghcr.io/coollabsio/coolify:4.1.2`)
- Coolify API token: in `.env.local.ps1` → `$env:COOLIFY_TOKEN`
- GitHub PAT: in `.env.local.ps1` → `$env:GITHUB_TOKEN` (scope `repo`, account `urieljarethbusiness-cpu`)
### 8.2 PowerShell quoting nightmare — the scp+sh workaround
**Problem:** Passing SQL (with single quotes, backslashes in PHP namespaces like
`App\Models\GithubApp`, or `{{pr_id}}` braces) through the chain
PowerShell → ssh → pct → docker → psql is quoting hell. Nested `'…'` inside
`"…"` inside `"…"` breaks at every level.
**Solution:** Write a `.sh` script locally → `scp` to the Proxmox host → `ssh … sh /tmp/script.sh`.
```powershell
# 1. Write the script locally with heredoc-safe content
# 2. scp it to the host
scp -i $SSH_KEY script.sh root@192.168.0.200:/tmp/script.sh
# 3. Execute remotely
ssh -i $SSH_KEY root@192.168.0.200 "sh /tmp/script.sh"
```
**Never** try to pass complex SQL inline through `ssh … "pct exec … psql … -c '…'"` from
PowerShell — the nested quoting will eat hours. Always scp a script.
### 8.3 Step-by-step: create a Dockerfile-based app from a GitHub repo
**Prerequisites:**
- The repo must be on GitHub (`urieljarethbusiness-cpu/<name>`) — Coolify's GitHub App
(source_id=3, installation_id=121211999) is already configured and can access all repos
under that account.
- Push the code first: `git push github main`.
- Get the GitHub repo numeric ID: `GET https://api.github.com/repos/<owner>/<repo>` → `.id`.
**Step 1 — Find the environment_id:**
```sql
SELECT id FROM environments WHERE uuid = '<env_uuid>';
-- e.g. for project "tools" production: returns 9
```
**Step 2 — INSERT into `applications`:** every field with a NOT NULL DEFAULT in the schema
must be set explicitly (the INSERT doesn't fire Laravel's model events, so defaults from
migrations are the DB column defaults, not Eloquent `$casts`/boot logic).
Key fields for a Dockerfile app:
```sql
INSERT INTO applications (
uuid, name, git_repository, git_branch, git_commit_sha,
build_pack, -- 'dockerfile'
dockerfile_location, -- '/Dockerfile'
ports_exposes, -- '3000' (your app's port)
health_check_path, health_check_port, health_check_host,
health_check_method, health_check_return_code, health_check_scheme,
health_check_interval, health_check_timeout, health_check_retries, health_check_start_period,
health_check_enabled, -- false (simpler; enable later via UI if needed)
limits_memory, limits_memory_swap, limits_memory_swappiness, limits_memory_reservation,
limits_cpus, limits_cpu_shares,
status, -- 'exited'
preview_url_template, -- '{{pr_id}}.{{domain}}'
fqdn, -- 'https://<name>.urieljareth.org'
repository_project_id, -- numeric GitHub repo ID (from GitHub API)
source_type, -- 'App\Models\GithubApp'
source_id, -- 3 (the GitHub App, see SELECT id FROM github_apps)
destination_type, -- 'App\Models\StandaloneDocker'
destination_id, -- 0 (the localhost Docker, see SELECT id FROM standalone_dockers)
environment_id, -- from Step 1
base_directory, -- '/'
static_image, -- 'nginx:alpine' (unused for dockerfile pack, but NOT NULL)
created_at, updated_at -- NOW()
) VALUES (...);
```
**Step 3 — INSERT into `application_settings` (CRITICAL — skip this and deploys crash):**
```
production.ERROR: Attempt to read property "disable_build_cache" on null
at ApplicationDeploymentJob.php:214
```
Coolify's deployment job reads `$application->settings->disable_build_cache` in its
constructor. Without a settings row, `settings` is null and the job dies instantly —
the deployment stays "in_progress" forever and blocks the queue (see §8.4).
```sql
INSERT INTO application_settings (
application_id, -- the id from Step 2's RETURNING
is_static, is_git_submodules_enabled, is_git_lfs_enabled,
is_auto_deploy_enabled, is_force_https_enabled, is_debug_enabled,
is_preview_deployments_enabled, is_log_drain_enabled, is_gpu_enabled,
is_swarm_only_worker_nodes, is_raw_compose_deployment_enabled,
is_build_server_enabled, is_consistent_container_name_enabled,
is_gzip_enabled, is_stripprefix_enabled,
is_container_label_escape_enabled, is_container_label_readonly_enabled,
disable_build_cache, is_spa, is_git_shallow_clone_enabled,
is_pr_deployments_public_enabled, use_build_secrets, inject_build_args_to_dockerfile,
docker_images_to_keep,
created_at, updated_at
) VALUES (
<app_id>, false, true, true, true, true, false, false, false, false,
true, false, false, false, true, true, true, true, false, false, true,
false, false, true, 2, NOW(), NOW()
);
```
**Step 4 — Trigger deploy via API (works even though app CRUD is 404):**
```powershell
. .\.env.local.ps1
$h = @{ Authorization = "Bearer $env:COOLIFY_TOKEN" }
Invoke-RestMethod "$env:COOLIFY_API_URL/deploy?uuid=<app_uuid>&force=true" -Headers $h
# Returns: { deployments: [{ deployment_uuid: "..." }] }
```
Monitor: `GET /deployments/<deployment_uuid>` → status goes `queued → in_progress → finished`.
### 8.4 Stuck deployment queue — how to unblock
When a deployment fails (e.g. missing `application_settings`), the queue row stays
`in_progress` forever and blocks ALL subsequent deploys of that app.
**Diagnose:**
```sql
SELECT id, status, created_at FROM application_deployment_queues
WHERE application_id = '<app_id>' ORDER BY id DESC LIMIT 5;
```
**Fix:**
```sql
-- Mark the stuck job as failed
UPDATE application_deployment_queues SET status = 'failed', updated_at = NOW()
WHERE id = <stuck_id>;
-- Delete ALL queue entries for the app and start clean
DELETE FROM application_deployment_queues WHERE application_id = '<app_id>';
```
Then restart the Coolify queue worker (so it picks up new jobs):
```sh
pct exec 102 -- docker exec coolify php artisan queue:restart
```
Wait 5 seconds, then trigger a fresh deploy via the API.
### 8.5 Reference: existing GitHub-source apps on this instance (verified 2026-07-27)
| id | name | repo | build_pack | source_id |
|----|------|------|------------|-----------|
| 43 | baserow | baserow/baserow | dockercompose | 3 |
| 46 | estación-de-documentos | urieljarethbusiness-cpu/Estación-de-Documentos | dockercompose | 3 |
| 47 | cotizador | urieljarethbusiness-cpu/cotizador | dockercompose | 3 |
| 48 | firecrawl | firecrawl/firecrawl | dockercompose | 3 |
| 50 | audio-a-texto | urieljarethbusiness-cpu/Audio-a-Texto | dockerfile | 3 |
| 51 | agendamax | urieljarethbusiness-cpu/agendamax | dockerfile | 3 |
GitHub App: id=3, uuid=`miakw8c0vthrtweroh9kzy1t`, app_id=3266541, installation_id=121211999.
Public GitHub source: id=0, uuid=`yyq0od5j3xkdf8twc28n6coh`.
Standalone Docker (destination): id=0, uuid=`jxlxd62k0d8owl6orgkjj1ob`, network=`coolify`.
Server: id=0, uuid=`l10mdaago0z605pga93gl6cz`, name=`localhost`.
### 8.6 Gitea repos as build source — not directly supported via this path
Coolify's GitHub App source only works with GitHub.com repos. For a Gitea-hosted repo:
- **Option A (recommended):** mirror to GitHub (`git remote add github …; git push github main`),
then create the Coolify app from the GitHub repo. This is what we did for AgendaMax —
the primary repo stays on Gitea, GitHub is just a deploy mirror.
- **Option B:** create a "Private repository (deploy key)" app via the web UI — set
`git_repository` to the Gitea SSH URL and `private_key_id` to an SSH key with Gitea access.
Gitea SSH is on port `22222` (mapped from container 22). This path needs UI credentials
and hasn't been tested on this instance.
Re-verified 2026-07-31 (`prompt-gallery-e3`): Option A still works, and the GitHub App
(source_id=3) reaches a **freshly created** private repo under `urieljarethbusiness-cpu`
with no extra configuration — the installation covers all repos on the account, so there
is nothing to click between `POST /user/repos` and the first deploy.
### 8.7 Env vars are Laravel-`encrypted` — raw SQL INSERT kills the deploy
> Verified 2026-07-31 deploying `prompt-gallery-e3` (Next.js 16 + `node:sqlite`).
> §8.3 creates the app but says nothing about env vars; this is the trap.
`environment_variables.value` carries Laravel's `encrypted` cast. Insert a **plaintext**
value with `psql` and the app row looks perfect, but every deploy dies **after** cloning
and reading the Dockerfile, with no hint about which field is at fault:
```
Deployment failed: The payload is invalid.
Error type: Illuminate\Contracts\Encryption\DecryptException
Location: /var/www/html/vendor/laravel/framework/src/Illuminate/Encryption/Encrypter.php:244
```
A correctly stored value is base64 JSON (`eyJpdiI6…`, `{iv,value,mac,tag}`), 200–300 chars
for short secrets. The `APP_KEY` never leaves the container, so **don't try to encrypt from
the outside** — create the rows through Eloquent instead:
```powershell
# write PHP locally -> scp to host -> pct push -> docker cp -> tinker
pct push 102 /tmp/envs.php /tmp/envs.php
pct exec 102 -- docker cp /tmp/envs.php coolify:/tmp/envs.php
pct exec 102 -- docker exec coolify php artisan tinker --execute='include "/tmp/envs.php";'
```
```php
$fila = new \App\Models\EnvironmentVariable();
$fila->key = 'SESSION_SECRET';
$fila->value = '…'; // se cifra al guardar
$fila->resourceable_type = \App\Models\Application::class;
$fila->resourceable_id = 53;
$fila->save(); // uuid se autogenera
```
Three gotchas found doing this:
- **DELETE then CREATE — never UPDATE a plaintext row.** Saving over an existing bad row
throws the same `DecryptException` first, because a model hook reads `value` before your
assignment is written. Wipe the rows with SQL, then create them via Eloquent.
- **The column is `is_buildtime`, not `is_build_time`** (and `is_runtime`/`is_buildtime`
are set by the model — don't touch them).
- **Two rows per key is correct.** Coolify mirrors every variable into a preview copy
(`is_preview = true`), so ids come in pairs. The pre-existing apps show the same shape;
it is not a duplicate-insert bug.
### 8.8 Persistent volumes by this path
`local_persistent_volumes` uses `resource_type` / `resource_id` — **not** the
`resourceable_*` names that `environment_variables` uses. Plain SQL is fine here (no
encrypted columns):
```sql
INSERT INTO local_persistent_volumes (name, mount_path, resource_type, resource_id, uuid, created_at, updated_at)
VALUES ('<app_uuid>-galeria-datos', '/app/data', 'App\Models\Application', <app_id>, '<uuid24>', NOW(), NOW());
```
Insert the volume **before** the first deploy. `New-CoolifyAppViaDB.ps1` triggers a deploy
as its last step, so for an app that needs env vars or a volume, do the whole set of rows
in one data-modifying-CTE statement and trigger `GET /deploy?uuid=` yourself — otherwise
the first build is guaranteed to fail and you burn ~15 min of Next.js build time.
### 8.9 Script bugs fixed 2026-07-31 (were silently breaking repo creation)
- **`" 20[04-9] "` accepted 200 and 204–209 but rejected 201/202/203** — a character-class
typo for `[0-9]`. Present in **both** `gitea_skill/scripts/Invoke-GiteaApi.ps1` and
`deploy_skill/scripts/Invoke-GitHubApi.ps1`. Every repo/hook/release creation returns
**201**, so it threw *after* successfully creating the resource — leaving a created repo
and an aborted pipeline. Both fixed to `" 20[0-9] "`.
- **`Invoke-GitHubApi.ps1` BOM bug (documented in §7.3) is now actually fixed** — the POST
body is written with `[IO.File]::WriteAllText(..., UTF8Encoding($false))`.
- **`git push` to GitHub: `Authorization: Bearer <PAT>` does NOT work.** git-over-https
wants Basic. For a headless push that leaves no token on disk:
```powershell
$basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("x-access-token:$env:GITHUB_TOKEN"))
git -C $repo -c "http.extraHeader=Authorization: Basic $basic" -c "credential.helper=" push -u github main
```
(`Sync-GiteaRemote.ps1`'s `token <T>` header is right for Gitea; it is not for GitHub.)
- `New-GitHubRepo.ps1` forces `auto_init = $true`, which puts a commit on the remote and
makes pushing existing history a non-fast-forward. For a mirror of a repo that already
has history, POST `/user/repos` yourself with `auto_init = $false`.
## 9. Apps with a sibling database (verified 2026-07-31, `demospa` — Next.js 15 + Prisma + MySQL 8)
Deployed `serenidad-spa` as `demospa.urieljareth.org` (app id 54) with a Coolify-managed
MySQL. New, reusable facts beyond §8:
### 9.1 `POST /databases/mysql` WORKS — the database API is not part of the 404 namespace
Unlike `/applications/*`, the `/databases/*` namespace responds. `POST /databases/mysql`
with `{server_uuid, project_uuid, environment_name, environment_uuid, destination_uuid,
name, image, mysql_root_password, mysql_database, mysql_user, mysql_password}` returns
`{uuid, internal_db_url}`. **The DB's internal hostname IS its uuid** — so
`DATABASE_URL=mysql://user:pass@<db-uuid>:3306/<db>`. Both the DB and dockerfile-pack apps
land on the external `coolify` network, so service-name DNS works with no extra wiring.
Two gotchas:
- **`instant_deploy: true` did NOT start it.** The resource was created with
`status=exited:unhealthy` and no container. `GET /databases/{uuid}/start` then answered
`400 {"message":"Database is already running."}` (the status field lies). What actually
started it: **`GET /deploy?uuid=<db-uuid>`** — the same trigger used for apps.
- **Never call `/start` and `/deploy` back to back.** Doing so recreated the container
mid-initialization and left a partial datadir; MySQL then crash-looped forever on
`--initialize specified but the data directory has files in it`. Recovery =
`docker compose down` in `/data/coolify/databases/<uuid>/`, `docker volume rm
mysql-data-<uuid>`, then one clean `up -d`. Same "don't queue deploys" rule as §4.
### 9.2 MySQL 8 init on this host takes ~7 min, and `start_period` is hardcoded to 5s
Coolify's generated DB compose sets `healthcheck.start_period: 5s`, but a first-time
MySQL 8 init here takes minutes (InnoDB init alone ~53s). Patch
`/data/coolify/databases/<uuid>/docker-compose.yml` to a longer `start_period` before the
first `up -d`.
**Do not trust `mysqladmin ping` as a readiness signal.** The MySQL entrypoint runs a
*temporary* server with `--skip-networking` while it creates the database and user, so the
socket answers (and Coolify reports `healthy`) while TCP still refuses connections. The log
even prints `ready for connections ... port: 0` for that temp server. Gate on TCP:
`mysqladmin -h127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" ping`, and confirm a
`ready for connections ... port: 3306` line.
### 9.3 Prisma + MySQL 8: pin `mysql_native_password`
Add to the DB service's compose `command:`
`--default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci`. 8.0.46 only warns that the flag is deprecated. Set
it **before** the first init so the created user gets native auth (afterwards the user
already exists and the env vars are ignored — you'd need `ALTER USER`).
### 9.4 The rolling-update healthcheck window is ~2 minutes — do slow work in the background
Coolify polls the container's **Dockerfile `HEALTHCHECK`** ~6 times at 30s and aborts with
"New container is not healthy, rolling back" if it hasn't passed. An entrypoint that runs
`prisma db push` + seed *before* starting the server will lose this race on a cold DB — the
build succeeds and the deploy still fails.
Working shape: start the server in the foreground and run DB preparation in a background
subshell, so the health endpoint answers in seconds while data lands moments later.
```sh
preparar_base_de_datos() { ...db push retry loop...; ...seed...; }
preparar_base_de_datos &
exec "$@" # el proceso en segundo plano sobrevive al exec
```
Keep the Dockerfile healthcheck tight (`--start-period=10s --interval=10s`) so it turns
healthy inside Coolify's window, and give the health endpoint **no DB dependency**.
### 9.5 `inject_build_args_to_dockerfile` bakes every env var into the image
`New-CoolifyAppViaDB.ps1`'s settings row sets this `true`, so Coolify rewrites the
Dockerfile with an `ARG`/`ENV` per variable — the build log fills with
`SecretsUsedInArgOrEnv: ... (ARG "AUTH_SECRET")` and the secrets end up in image layers,
violating §2.5 of `AGENTS-coolify-apps.md`. Set it to `false` unless the app genuinely needs
build-time vars (a Next.js app only does if it reads `NEXT_PUBLIC_*`, which are inlined at
build time — grep the source before deciding).
### 9.6 Pushing to the GitHub mirror auto-triggers a deploy
The settings row also sets `is_auto_deploy_enabled = true`, and Coolify's GitHub App gets
push webhooks for the whole account — so `git push github main` queues a deploy on its own.
Expect an extra `in_progress` row in `application_deployment_queues`; clear it (§8.4) before
triggering your own, or just let the automatic one run.
### 9.7 Next.js: pages that query the DB break `docker build`
Any App Router page that hits the database without `export const dynamic = "force-dynamic"`
is prerendered during `next build`, where no DB exists. Pages reading `cookies()` are
already dynamic; public landing/catalog pages usually are not. Validate locally with a
deliberately unreachable `DATABASE_URL` — the build must still finish (Prisma logs errors
but they are non-fatal once every DB page is `ƒ`).
`sharp` needs no special handling: `npm ci` on `node:22-alpine` resolves
`@img/sharp-linuxmusl-x64` as long as the lockfile was generated with all platform variants
(it is, by default), so remote-image optimization works.
---
## 10. Correcciones verificadas 2026-08-27 (despliegue de `escudoverde-site`)
### 10.1 `/deploy` ahora exige POST, no GET
El §8.4 dice `GET /deploy?uuid=&force=true`. **Hoy responde 405 Method Not Allowed.**
Con `-Method POST` funciona y devuelve el `deployment_uuid` normalmente:
```powershell
Invoke-RestMethod "$env:COOLIFY_API_URL/deploy?uuid=<app_uuid>&force=true" -Headers $h -Method POST
```
`GET /deployments/{uuid}` sigue funcionando igual para el seguimiento.
### 10.2 El `GITHUB_TOKEN` de `.env.local.ps1` está caducado
`New-GitHubRepo.ps1` falla con `401 Bad credentials`. El token que **sí** sirve es el que
guarda el Administrador de credenciales de Windows para `github.com` (cuenta
`urieljarethbusiness-cpu`, scopes `gist, repo, workflow`). Se recupera sin exponerlo:
```bash
TOK=$(printf "protocol=https\nhost=github.com\n\n" | git credential fill | sed -n 's/^password=//p')
```
Conviene rotar el de `.env.local.ps1` o hacer que los scripts caigan a `git credential fill`.
### 10.3 Imágenes nginx sin root: el orden dentro del `RUN` importa
El builder de Coolify corre el `docker build` **sin DAC override para root**. Consecuencia
concreta con la receta habitual de nginx no-root:
- Si `nginx -t` va **después** del `chown` de `/tmp` al usuario `nginx`, el build falla con
`open() "/tmp/nginx.pid" failed (13: Permission denied)` — aunque en local funcione.
- `nginx -t` **crea** el fichero pid. Si se queda en la imagen, pertenece a root y el
contenedor no arranca al hacer `USER nginx`.
Orden que funciona en ambos lados:
```dockerfile
RUN set -eux; \
mkdir -p /tmp/nginx/client_body /tmp/nginx/proxy; \
nginx -t -c /etc/nginx/nginx.conf; \
rm -f /tmp/nginx/nginx.pid; \
chown -R nginx:nginx /tmp/nginx /usr/share/nginx/html /var/cache/nginx; \
chmod 0777 /tmp/nginx
USER nginx
```
### 10.4 `immutable` + nombre de fichero fijo = despliegue invisible
Con `Cache-Control: public, max-age=31536000, immutable` sobre `/assets/`, Cloudflare
sirvió el CSS anterior durante todo el despliegue siguiente (`cf-cache-status: HIT`,
`Age: 1008`). El contenedor tenía la versión nueva; el usuario veía la vieja.
**Regla para cualquier app estática en esta instancia:** o los assets llevan hash en el
nombre, o el HTML los referencia con `?v=<hash del contenido>`. Sin eso, `immutable` es
una trampa: el despliegue "funciona" y no cambia nada visible.
---
## 11. Re-verificación completa 2026-08-29 (v4.3.14): el 404 de `/applications` era Cloudflare
Auditoría integral con SSH + APIs validadas. La instancia corre ahora
**`4.3.14`** (`GET /version`), actualizada desde 4.3.10. Hallazgos, en orden de
importancia:
### 11.1 La API está COMPLETA — el 404 de `/applications/*` lo impone Cloudflare, no Coolify
Mismo token de siempre, misma ruta, dos caminos:
| Llamada | Resultado |
|---|---|
| `https://coolify.urieljareth.org/api/v1/applications` (público, vía Cloudflare) | **404** |
| `http://192.168.0.117:8000/api/v1/applications` (origen, LXC 102) | **200** (JSON completo) |
| `/github-apps` | 404 vía CF · **200 vía origen** |
| `/dockerfiles`, `/sources`, `/notifications`, `/private-keys` | 404 en ambos → **no existen como rutas**; los nombres correctos están en el spec (ver 11.2) |
| `/version`, `/resources`, `/servers`, `/projects`, `/teams`, `/services`, `/databases`, `/deployments`, `/security/keys` | 200 por ambas vías |
Todo el diagnóstico de §1 ("API parcial", v4.1.2) era este mismo bloqueo de
Cloudflare, no un recorte de la API. Las secciones §3 (Playwright) y §8 (DB
INSERT) siguen siendo fallbacks válidos, pero **la vía API debería funcionar
llamando al origen** (`$env:COOLIFY_API_URL_ORIGIN`). No se ha ejercitado
end-to-end un `POST /applications/*` contra el origen todavía — verificarlo en el
próximo deploy antes de jubilar el flujo UI. El fix de raíz es corregir la regla
del edge en el dashboard de Cloudflare.
### 11.2 La superficie real (del `openapi.yaml` del propio contenedor)
Dentro del contenedor: `/var/www/html/openapi.yaml`. Rutas declaradas en
4.3.14 (extraídas el 2026-08-29):
- `/applications` + `/applications/{public, private-github-app, private-deploy-key, dockerfile, dockerimage}`
- `/databases` + por motor: `{postgresql, clickhouse, dragonfly, redis, keydb, mariadb, mysql, mongodb}`
- `/deployments`, `/deploy`, `/destinations`, `/servers` (+ `/import`, `/digitalocean`, `/hetzner`, `/vultr`)
- `/github-apps`, `/gitlab-apps`, `/security/keys`, `/s3-storages`, `/tags`
- `/notifications/{email, discord, slack, telegram, pushover, webhook}`
- `/cloud-init-scripts`, `/cloud-tokens`
- `/projects`, `/projects/{uuid}/environments`, `/resources`, `/services`
- `/team`, `/teams`, `/team/envs`, `/team/members`
- `/version`, `/health`, `/enable`, `/disable`, `/mcp/enable`, `/mcp/disable`
`GET /docs` (Scalar UI) redirige a `/login` — requiere sesión web; para el
contrato exacto, leer el YAML del contenedor:
`pct exec 102 -- docker exec coolify cat /var/www/html/openapi.yaml`.
### 11.3 POST-only confirmado como comportamiento de Coolify (no de Cloudflare)
Contra el origen, `GET /deploy?uuid=fake` → **405**
`{"message":"This endpoint has changed to a POST request."}`. Es un cambio real
de Coolify v4.2 (changelog: los endpoints de estado pasaron a POST-only), igual
que §10.1. Otros cambios relevantes de 4.2→4.3: endpoints de logs para
db/servicio/contenedor, settings de application incluidas en las respuestas,
soporte MCP (read-only) y build pack Railpack (beta, 4.3.1). Breaking: se
eliminó el endpoint deprecado de aplicación Docker Compose — usar
`POST /services`. Fuentes: coolify.io/changelog y
github.com/coollabsio/coolify/releases.
### 11.4 Estado de credenciales verificado el 2026-08-29
| Credencial | Estado |
|---|---|
| SSH `root@192.168.0.200` y `root@192.168.0.117` con `keys/proxmox_ed25519` (≡ `~/.ssh/coolify_key`, sin passphrase) | ✅ |
| API Proxmox: token `root@pam!openclaw` (único en `token.cfg`) | ✅ 200 `/version` + `/cluster/resources` — ya en `.env.local.ps1` |
| API Coolify: `COOLIFY_TOKEN` (team root) | ✅ 200 — v4.3.14 |
| Gitea: token de `urieljareth` | ✅ 200 `/api/v1/user` |
| GitHub: PAT `ghp_NFy4…` del `.env` | ❌ caducado (401) — reemplazado en `.env.local.ps1` por el token vivo del Credential Manager de Windows (`gho_…`, cuenta `urieljarethbusiness-cpu`, scopes `gist, repo, workflow`) |
| Cloudflare API | ❌ sin token — gestionar túnel desde el dashboard |
| UI Coolify: `urieljareth@gmail.com` / password habitual | ⚠️ probable (heredado de la instancia vieja `192.168.1.175`, ver `C:\Users\Uriel Jareth\coolify-agent\coolify-agent-skill.md`), sin verificar |
Inventario completo de llaves SSH de la máquina (incluidas las corruptas de
SiteGround y la de la instancia antigua): `ACCESS.md` en la raíz del repo.
@@ -7,7 +7,7 @@
$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_SSH_KEY = "keys\proxmox_ed25519" # relativo a la raíz del repo (≡ ~\.ssh\coolify_key)
$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"
@@ -16,10 +16,10 @@ $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.
# Coolify UI login (email/password) — used by the Playwright UI fallback flow.
# Since v4.3.14 the REST API is complete when called against the origin
# (http://192.168.0.117:8000/api/v1); the /applications/* 404 happens only via
# the public Cloudflare hostname. See deploy_skill/references/coolify-4.1.2-notes.md §11.
$env:COOLIFY_EMAIL = "REPLACE_WITH_COOLIFY_UI_EMAIL"
$env:COOLIFY_PASSWORD = "REPLACE_WITH_COOLIFY_UI_PASSWORD"