Add Coolify deployment and label conventions
This commit is contained in:
@@ -477,3 +477,103 @@ or convention:
|
|||||||
|
|
||||||
Prefer updating this skill over repeating ad-hoc instructions in a project: if a
|
Prefer updating this skill over repeating ad-hoc instructions in a project: if a
|
||||||
convention applies to more than one repo, it belongs here.
|
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.<name>.entrypoints=https
|
||||||
|
traefik.tcp.routers.<name>.rule=HostSNIRegexp(`^.+\.<domain>$`)
|
||||||
|
traefik.tcp.routers.<name>.service=<name>
|
||||||
|
traefik.tcp.routers.<name>.tls.passthrough=true
|
||||||
|
traefik.tcp.services.<name>.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>`.
|
||||||
|
- App state: `GET /api/v1/applications/<uuid>` (`status`, `updated_at`).
|
||||||
|
- App logs: `GET /api/v1/applications/<uuid>/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 <sha>`), 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/<repo>.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.
|
||||||
|
|||||||
Reference in New Issue
Block a user