From 0279c3a5913306a302e49fae3db68cad13d654d3 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Mon, 26 Oct 2026 21:36:36 +0800 Subject: [PATCH] Add Coolify deployment and label conventions --- SKILL.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/SKILL.md b/SKILL.md index 8eb6d36..f345099 100644 --- a/SKILL.md +++ b/SKILL.md @@ -477,3 +477,103 @@ or convention: Prefer updating this skill over repeating ad-hoc instructions in a project: if a convention applies to more than one repo, it belongs here. + +## 8. Coolify + +Host: `https://coolify.tzgyn.dev`. API: `$COOLIFY_URL/api/v1`, bearer +`$COOLIFY_TOKEN` (keep the token in an env var or secret manager; never commit +it). The UI and API are the same deployment. + +### Labels are the config + +- Coolify stores an app's labels in `custom_labels` as **base64**. The UI calls + the field **Container Labels**; in v4.5 it is under **General → Container → + Labels** (older versions place it under General/Advanced). +- `is_container_label_readonly_enabled` (UI: **Label management**): + - `true` — “Managed by Coolify (auto-generated)”: Coolify may regenerate + labels from domains/proxy settings and **drop manual additions**. + - `false` — “Managed manually (edit labels yourself)”: `custom_labels` is + kept verbatim; domain/proxy changes no longer update it. + Set it to `false` whenever you add labels Coolify does not generate. +- API-created apps do **not** get the generated HTTP labels merged. Put the + complete set (HTTP routers, middlewares, certresolver, Caddy HTTP labels, and + any TCP routers) in `custom_labels`. +- PATCH only the field, preserving existing labels: + + ```bash + curl -X PATCH -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H 'Content-Type: application/json' \ + -d "{\"custom_labels\":\"$(base64 < labels.txt)\"}" \ + "$COOLIFY_URL/api/v1/applications/$APP" + ``` + + Decode it back and verify. Nested/double base64 can appear after API updates. +- **After every app update/recreation/deploy, check the TCP lines are still + there.** If they vanish, Traefik terminates the SNI with its default + certificate. + +### HTTP vs raw TCP + +- `Domains` only creates HTTP(S) routers (Traefik terminates TLS). Do not add a + wildcard domain when the proxy must pass TLS through — that would make + Traefik ask for a wildcard certificate and decrypt tenant traffic. +- For SNI passthrough (tunnels, non-HTTP services), add the full set: + + ```text + traefik.tcp.routers..entrypoints=https + traefik.tcp.routers..rule=HostSNIRegexp(`^.+\.$`) + traefik.tcp.routers..service= + traefik.tcp.routers..tls.passthrough=true + traefik.tcp.services..loadbalancer.server.port=8443 + ``` + + Traefik dials the container port over the `coolify` Docker network; no host + port publish is needed. The app must listen on that port. The `^.+\.` regex + does not match the apex, so the API domain can stay on the HTTP router. +- Keep the HTTP labels in the same `custom_labels` value when using a manual + label set; otherwise the dashboard/API router disappears. + +### API and deploys + +- Deploy: `POST $COOLIFY_URL/api/v1/deploy?uuid=`. +- App state: `GET /api/v1/applications/` (`status`, `updated_at`). +- App logs: `GET /api/v1/applications//logs` (JSON `logs` string). +- The deployment list JSON can contain invalid escapes/HTML; `jq` sometimes + fails. Parse with Python (`json.load`, then join `logs[].output`). Prefer the + specific deployment endpoint or app status over a long poll of + `/deployments`. +- A deploy can transiently report `running:unhealthy`; wait for the app and hit + `/health` before debugging. +- Env vars: `is_runtime` and `is_buildtime`; preview and production entries + can duplicate the same key. Secrets belong in Coolify envs, not the repo. + +### Gitea webhook + +- Push-to-deploy uses Coolify's manual Gitea webhook: + `X-Hub-Signature-256` = HMAC-SHA256 of the raw body with the app's + `manual_webhook_secret_gitea`; Gitea sends this natively. A curl replay needs + `X-Gitea-Event: push` and the signature. +- **Gitea's hook edit API does not update the stored secret.** Delete and + recreate the hook (branch filter `main`). The app's secret is regenerated + when the app is recreated, so recreate the hook too. +- The audit log is the oracle: `webhook.deployment.queued` vs + `webhook.gitea.signature_failed`. +- Coolify pins the pushed SHA (`git fetch origin `), but this Gitea only + serves the current tip by SHA. If a later push lands before the fetch, the + deploy fails with “not our ref”: re-trigger the deploy. Permanent fix inside + the Gitea container: + + ```bash + git -C /data/git/repositories/TZGyn/.git config \ + uploadpack.allowReachableSHA1InWant true + ``` + +- After any app recreation: recreate the webhook and re-check custom labels. + +### App creation/recreation + +- Use `domains` in the API (not `fqdn`). `private_key_uuid` cannot be changed + on update; recreate the app with the key. Use storage type `persistent` for + data volumes. +- Verify after deploy: app healthy, the real TCP/SNI path (not just HTTP), + custom labels present, and the webhook queued.