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]>
15 KiB
AGENTS — Building apps that deploy cleanly on this Coolify instance
Audience: LLMs and code agents that are developing an application or service intended to run on this specific self-hosted Coolify instance.
Purpose: This is the single source of truth for compatibility. Follow it while writing the app (Dockerfile,
docker-compose.yaml, env handling, ports) so the first deploy works instead of going through the trial-and-error captured in thedocs/issues. For operating an already-deployed app, see../CLAUDE.mdand the runbooks underrunbooks/.
This guide is portable: copy it into the new app's repo (e.g. as its own AGENTS.md)
so the agent building that app has the constraints on hand.
1. The deployment environment (you cannot change this)
Internet
→ Cloudflare Tunnel (dashboard-managed, *.urieljareth.org)
→ Traefik (container: coolify-proxy, terminates TLS, routes by Host header)
→ your app container (Docker, inside LXC 102)
- Everything runs as Docker containers inside Proxmox LXC
102on host192.168.0.200. There is no direct Docker or LAN access — operators reach it only viassh [email protected] → pct exec 102 -- docker .... - The public edge is a dashboard-managed Cloudflare Tunnel; ingress rules live in the Cloudflare Zero Trust dashboard, not in files on the host.
- TLS is terminated by Traefik (
coolify-proxy), which also obtains Let's Encrypt certificates automatically via DNS challenge. - Coolify manages the lifecycle (build, deploy, env, domains) and writes a generated
compose to
/data/coolify/applications/<UUID>/docker-compose.yaml.
Source of truth for topology: proxmox-inventory.md and
runbooks/cloudflare-tunnel.md.
2. Hard rules (non-negotiable)
Each rule lists what, why, and how to verify.
2.1 Networking — reach siblings by Docker service name, never localhost
- Rule: When your app and its database/cache run as sibling containers on the same
Docker network, the app must connect using the Docker service name, not
localhost/127.0.0.1. - Why:
localhostresolves to the app's own container, not the DB. This is the single most common "cannot connect to database" failure on this host (see../CLAUDE.mdandrunbooks/nextcloud.md, where Nextcloud'sdbhostmust benextcloud-db). - Verify:
It must return the DB container's IP.
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec <app> getent hosts <db-service-name>"
2.2 Networking — join the external coolify network to reach shared services
- Rule: If the app must reach services managed separately by Coolify (a shared
Postgres/Redis, or another app), declare the global
coolifynetwork as external and attach your service to it:networks: coolify: name: coolify external: true - Why: Without this, service-name DNS to those resources fails. This is the fix
documented for Baserow (
runbooks/baserow.md), which resolves its external Postgres/Redis only through thecoolifynetwork. - Verify: same
getent hostscheck against the target service name.
2.3 Ports — do not publish 80/443; let Traefik route
- Rule: Never publish host ports
80or443in your compose (noports: ["80:80"]/["443:443"]). Your app listens on its own internal port; Traefik connects to it and handles the public 80/443. - Why:
80/443on the host belong tocoolify-proxy. Publishing them collides and the app fails to start — exactly the Baserow startup failure inrunbooks/baserow.md. - Also: Coolify defaults a new app's exposed port to
3000. If your app actually listens elsewhere (e.g.80for nginx,8080, etc.), the exposed port must match the real listening port or Traefik routes to a dead port (502). Set it correctly in the Coolify UI/API before the first deploy (ports_exposes).
2.4 Domains & TLS — *.urieljareth.org, DNS challenge, set the FQDN early
- Rule: Public hostname pattern is
https://<name>.urieljareth.org. Set the app's FQDN before the first deploy — do not rely on Coolify's auto-generated UUID subdomain (it has no working DNS route and produces 502s). - Why: TLS is issued by Traefik via DNS challenge (Cloudflare API token), so it
works regardless of Cloudflare's "Always Use HTTPS". Do not assume HTTP-01 challenge
— that path is intentionally not used here
(see
issue-coolify-static-app-deploy.md). - Verify:
curl.exe -k -sSI https://<name>.urieljareth.org/
2.5 Secrets — env vars only, never in the repo or image
- Rule: All credentials (DB passwords, API tokens, keys) come from environment variables injected by Coolify. Never hardcode them in source, Dockerfile, compose, or committed files, and never bake them into the image.
- Why: Repo policy forbids secrets in the tree (
../CLAUDE.md,runbooks/seguridad.md); baked secrets also leak through image layers. - Verify: the app boots with secrets supplied only as env vars; the image contains no credential values.
3. Build / deploy options
Coolify can deploy your app in several ways. Pick the simplest that fits, and design the
repo accordingly. (API endpoints are under /api/v1/...; reference files in
../coolify_skill/references/ops/.)
| Type | When to use | Coolify create endpoint |
|---|---|---|
| Public Git + Nixpacks | Standard app, language auto-detected (Node, Python, Go…) | POST /applications/public |
| Static / SPA | Pure HTML/CSS/JS or built front-end (React/Vue) | POST /applications/public with is_static: true (and is_spa: true for SPAs), static_image for the serving image |
| Dockerfile | Custom build, no separate registry | POST /applications/dockerfile |
| Prebuilt image | Image already in a registry (ghcr.io, Docker Hub) | POST /applications/dockerimage |
| Private Git | Private repo via deploy key / GitHub App | POST /applications/private-deploy-key or /private-github-app |
| Multi-container stack | App + its own DB/cache defined together | POST /services (compose). Note: /applications/dockercompose is deprecated — use /services. |
For static sites, prefer is_static: true with a known static_image (e.g. nginx) and
remember the listening port is then 80 (§2.3).
4. App configuration reference
Fields your app must expose or that you'll set in Coolify. Schemas come from
../coolify_skill/references/ops/ (healthcheck.md,
create-*.md, create-env-by-application-uuid.md).
Environment variables (POST /applications/{uuid}/envs)
| Field | Meaning |
|---|---|
key / value |
The variable |
is_literal |
Do not interpolate $-style references in the value |
is_multiline |
Value spans multiple lines |
is_preview |
Applies to preview deployments only |
is_shown_once |
Hide in UI after first reveal (for secrets) |
Bulk update: PATCH /applications/{uuid}/envs/bulk.
Ports
| Field | Meaning |
|---|---|
ports_exposes |
Port the app actually listens on (Traefik targets this). Must match reality — default is 3000. |
ports_mappings |
Optional host:container mapping. Avoid 80/443 (§2.3). |
Healthcheck (strongly recommended)
| Field | Notes |
|---|---|
health_check_enabled |
Turn it on |
health_check_path |
e.g. /health (expose a cheap, dependency-light endpoint) |
health_check_port |
Defaults to the exposed port |
health_check_method / health_check_return_code |
e.g. GET / 200 |
health_check_scheme |
http between Traefik and the container (TLS is terminated upstream) |
health_check_interval / _timeout / _retries / _start_period |
Tune for slow boots |
Design the app to serve a healthcheck endpoint that does not depend on the DB being reachable at boot, so a slow DB doesn't flap the container.
Resource limits & deployment hooks (optional)
- Limits:
limits_memory,limits_memory_swap,limits_cpus,limits_cpuset,limits_cpu_shares. - Hooks:
pre_deployment_command/post_deployment_command(+ their_container), e.g. run DB migrations post-deploy. Keep these idempotent.
5. Volumes / persistence
- Persistence is modeled in docker-compose with named volumes. Anonymous volumes and container-filesystem writes are lost on redeploy.
- Name every volume that must survive a redeploy, and mount it at the app's data path.
- The compose Coolify actually runs is generated at
/data/coolify/applications/<UUID>/docker-compose.yaml— inspect it after deploy to confirm your volumes and network landed as intended.
6. Reference docker-compose snippet
Minimal compose that satisfies every hard rule: app + sibling DB by service name, joined
to the external coolify network, no published 80/443, a healthcheck, and a named
volume. Secrets arrive as env vars (no literal values here).
services:
app:
image: your-app:latest # or build: .
environment:
# Reach the DB by SERVICE NAME, never localhost (§2.1)
DB_HOST: app-db
DB_PORT: "5432"
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD} # injected by Coolify env, not hardcoded (§2.5)
APP_PUBLIC_URL: https://yourapp.urieljareth.org
# No `ports:` block — Traefik routes to the internal port (§2.3)
expose:
- "8080" # the port the app listens on -> set ports_exposes=8080
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
networks:
- coolify # only if it must reach shared Coolify services (§2.2)
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 # named volume survives redeploys (§5)
volumes:
app-db-data: {}
networks:
coolify:
name: coolify
external: true
7. Pre-deploy checklist
- App reaches its DB/cache by Docker service name, not
localhost(§2.1) - If it needs shared Coolify services, compose declares
coolifynetwork,external: true(§2.2) - Compose does not publish host ports
80/443(§2.3) ports_exposesin Coolify matches the app's real listening port (not the3000default) (§2.3)- FQDN set (
https://<name>.urieljareth.org) before the first deploy — no auto UUID subdomain (§2.4) - TLS assumed via DNS challenge — no HTTP-01 /
.well-knowndependency in the app (§2.4) - All secrets injected as env vars, none committed or baked into the image (§2.5)
- Healthcheck endpoint defined and DB-independent at boot (§4)
- Persistent data uses named volumes (§5)
8. Post-deploy verification
Run from the repo root (PowerShell). These reuse existing scripts — no new tooling.
# 1. Public endpoint responds (TLS issued, Traefik routing correct)
curl.exe -k -sSI https://<name>.urieljareth.org/
# 2. Sibling/shared DNS resolves from inside the app container
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec <app> getent hosts <db-service-name>"
# 3. Container status overview
.\coolify_skill\scripts\Get-CoolifyDockerStatus.ps1 -All
# 4. Certificate / routing errors at the proxy
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs coolify-proxy --tail 50 2>&1 | grep -Ei 'error|certificate|acme|<name>'"
# 5. Inspect the generated compose actually in use
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- cat /data/coolify/applications/<UUID>/docker-compose.yaml"
9. Known pitfalls (symptom → cause → fix)
| Symptom | Root cause | Fix | Source |
|---|---|---|---|
| App's domain returns 502 Bad Gateway | ports_exposes doesn't match the real listening port (often 3000 vs 80) |
Set ports_exposes to the actual port before deploy |
issue-coolify-static-app-deploy.md |
| 502 / no certificate on the app domain | Traefik couldn't get a cert via HTTP challenge | Environment uses DNS challenge; ensure FQDN is set and don't depend on HTTP-01 | issue-coolify-static-app-deploy.md |
| App reachable only at an ugly UUID subdomain | FQDN not set, Coolify auto-generated it | Set fqdn to https://<name>.urieljareth.org before first deploy |
issue-coolify-static-app-deploy.md |
| App won't start, port conflict | Compose publishes 80/443 (owned by coolify-proxy) |
Remove host port publishing; let Traefik route | runbooks/baserow.md |
| App can't reach its DB | Used localhost, or DB is on a different network |
Use the DB service name; for shared services join the coolify network |
runbooks/nextcloud.md, runbooks/baserow.md |
| Coolify UI blank when opening the app page | Cloudflare tunnel route order — /app/* captured /application/... |
Operator fix: /project/* route must precede /app/* in the dashboard |
issue-coolify-static-app-deploy.md |
WebSocket / terminal drops, tls: first record does not look like a TLS handshake |
Tunnel routes for ports 6001/6002 set to https:// |
Operator fix: those routes must be http:// |
ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md |
The last two are operator/infrastructure fixes (Cloudflare dashboard), not things the app developer changes — listed here so an agent recognizes the symptom and points the operator to the right runbook instead of debugging the app.
10. See also
../CLAUDE.md— repo architecture, SSH path, operating rulesproxmox-inventory.md— verified topologyrunbooks/cloudflare-tunnel.md— tunnel routesrunbooks/coolify-docker.md— Docker ops inside LXC 102runbooks/nextcloud.md,runbooks/baserow.md— real app patterns../coolify_skill/references/— Coolify API operation reference (search withrg, open oneops/*.md)