Coolify Operational Guide
Concise operational pointers for Coolify v4 self-hosted PaaS troubleshooting and tuning.
Assumes you already know Docker, docker-compose, Traefik basics, and Postgres basics. This skill covers the Coolify-specific layer — the parts models tend to gloss over or hallucinate: the control-plane/destination split, build-pack quirks, env-var precedence, magic SERVICE_* vars, Traefik label generation, deploy-webhook semantics, persistent-storage layout, and the upgrade/rollback path.
When to use
Load when the question is about:
- Control plane vs destination (multi-server, agent install, validated/reachable state)
- Build-pack pick (Nixpacks vs Dockerfile vs Compose vs Static) and per-pack env injection
- Environment variables: build- vs runtime-flag, secrets, magic
SERVICE_*generation, shared-variable scopes - Deploy webhooks and the
/api/v1/deployendpoint (force,pr,tag,uuid) - Preview deployments (GitHub App vs webhook, scoped secrets,
{{pr_id}}/{{random}}) - Traefik integration: auto-labels, read-only mode, www/non-www redirects, proxy switch (Traefik ↔ Caddy)
- Persistent storage layout under
/data/coolify/and Composeis_directory:/content:sugar - Database backups (cron + S3) and Postgres restore (
-Fcvs plain on cross-version upgrade) - Upgrade / downgrade via
install.sh -s <version>, auto-update toggle, instance restore (APP_KEY,APP_PREVIOUS_KEYS) - Rolling-update prerequisites and why redeploys recreate
Do NOT load for: writing Dockerfiles, vanilla docker-compose.yml syntax, generic Traefik routing, generic CI/CD pipeline design — those don't need this skill.
Architecture: control plane vs destinations
- Control plane = the Coolify host. Owns the UI, the encrypted DB, SSH keys, scheduling, and on the same host runs the proxy (
coolify-proxyTraefik container by default). - Destination = any Linux host (incl. the control plane itself) where resources actually run. Reached via SSH from the control plane using a Coolify-managed key whose public half lives in the destination's
root@~/.ssh/authorized_keys. Docker Engine 24+ required on every destination (server prerequisites). - Each destination runs its own proxy. Traffic for an app on a non-control-plane destination goes directly to that host — DNS for that app must resolve to the destination IP, not the control plane (multi-server architecture).
- "Reachable" vs "Validated": reachable = SSH+Docker handshake succeeded once; validated = full host check (sudo, Docker socket, OS info) passed. A reachable-but-not-validated server will refuse new resources.
- Application vs Service: an application is a git source that Coolify builds (Nixpacks/Dockerfile/Compose/Static); a service is a one-click template (e.g. Plausible, MinIO, Appwrite) defined by an upstream
docker-compose.ymlCoolify renders. Different UI tabs, different env-var handling rules. - Sentinel is a separate lightweight container Coolify deploys per server when the metrics toggle is on — collects host CPU/RAM and per-container CPU/RAM. Marked experimental in the docs; metrics collection is not supported for Compose / Service-template resources (Sentinel docs).
Build packs
Coolify ships exactly four build packs (overview):
- Nixpacks (default for git source): auto-detects framework, generates a Dockerfile under the hood. Honors
nixpacks.toml/nixpacks.jsonat repo root for install/build/start command overrides. Build args injected from Coolify env-vars marked "Build Variable." - Dockerfile: uses your
Dockerfilefrom the repo. Build args auto-injected by default — disable in Advanced if you want to manageARGs manually. Network-port field must match the port the container actuallyLISTENs on (0.0.0.0, not127.0.0.1); mismatch yields a Traefik No available server error (Dockerfile pack). - Docker Compose: your
docker-compose.ymlis the source of truth for env, ports, volumes, healthchecks. Remove any user-definednetworks:— Coolify injects its own bridge for Traefik routing; custom networks cause intermittent 502s (compose pack). Rolling updates not supported with Compose (static container names — see "Deployments" below). - Static: takes pre-built artifacts and serves them via embedded Nginx (only option since beta.402). Set the Base directory to the build-output folder (
/dist,/out,/build). Custom Nginx config via the Generate button + edit (static pack).
Environment variables
- Two independent flags per variable: Build Variable (passed as
ARG/ build-arg /--env-fileto BuildKit) and Runtime Variable (written to a.envfile at deploy and loaded bydocker compose --env-file). Both default on. Disable build to keep secrets out of image layers (env vars). - Build secrets use Docker BuildKit (Docker 18.09+). Secrets are never embedded in image layers and are not visible in
docker history— use this in preference to--build-argfor tokens. - Literal flag stops shell interpolation — required for values containing
$, e.g.P@ss$word123. - Predefined:
COOLIFY_FQDN,COOLIFY_URL,COOLIFY_BRANCH,COOLIFY_RESOURCE_UUID,COOLIFY_CONTAINER_NAME, plusSOURCE_COMMIT,PORT,HOST. Available at runtime in every container. - Shared scopes (template-resolved at deploy):
{{team.VAR}},{{project.VAR}},{{environment.VAR}}. Resource-level wins over scoped — set the override on the resource, not by editing the team scope. - Magic generators — Compose only — pattern
SERVICE_<TYPE>_<IDENTIFIER>(compose docs):SERVICE_FQDN_<NAME>— full host (e.g.api-x9fk2.example.com)SERVICE_URL_<NAME>_<PORT>— full URL with optional path-prefix routingSERVICE_PASSWORD_<NAME>/SERVICE_PASSWORD_64_<NAME>— generated, persisted, reused across redeploysSERVICE_USER_<NAME>— random 16-char stringSERVICE_BASE64_<NAME>— random base64- First reference creates and persists the value; subsequent references in the same compose return the same value.
- Pitfall: changing an env var does not auto-redeploy. Click Restart (runtime-only var) or Redeploy (build var or Dockerfile-built app, where Coolify still triggers full rebuild — known behavior, issue #2745).
Deployments
- Git webhook: GitHub (App or webhook), GitLab, Gitea, Bitbucket. With the GitHub App, PR events drive preview deployments; secret is generated and verified server-side. With raw webhook, you set the secret yourself (CI/CD docs).
- Preview deployments: GitHub App only for full PR-comment integration. Each preview gets a URL templated by
{{random}}(random subdomain) or{{pr_id}}(PR number) — wildcard DNS required. Preview env-vars are a separate scope from production — production secrets do not leak to PR builds. Auto-cleaned on PR close/merge (preview-deploy). - Manual / API deploy:
POST /api/v1/deploywith bearer token (Keys & Tokens → API tokens). Query params:uuid(CSV),tag(CSV),force=true|false(skip build cache),pr/pull_request_id(preview),docker_tag(preview only).pris mutually exclusive withtag(deploy endpoint). - API access must be enabled in Settings → Configuration → Advanced before any token will work.
- Restart vs Redeploy: Redeploy always rebuilds the image. Restart should only restart the container, but any config change (incl. env var) promotes Restart to a full rebuild (discussion #2935). Compose deployments hide the Restart button entirely.
- Rolling updates require all four to be true (rolling-updates): valid healthcheck passing, default container naming (no custom name), not Docker Compose, no host port mapping. Miss any → Coolify falls back to stop-then-start, with downtime.
- Health checks: configure path / expected status / interval in UI or Docker
HEALTHCHECKinstruction. Container needscurlorwget— Alpine images often needapk add --no-cache curlfor the UI mode to work. Compose-pack services must use the compose-filehealthcheck:attribute. Setexclude_from_hc: trueon one-shot migration services (health checks). - Failed healthcheck blocks the deploy — Traefik returns 404 / No available server until it passes.
Traefik integration
Coolify ships Traefik as the default proxy on the control plane and on every destination (proxy overview). Caddy is supported but marked experimental (supported proxies).
- Auto-labels are written by Coolify into each container's labels: router rule (
Host()), entry-points, TLS resolver (letsencrypt), service loadbalancer port. Read-only labels mode (default) means the UI controls them — your custom labels merge on top via the Container Labels field. - Switching proxy (Traefik ↔ Caddy) is supported since beta.237. Pre-237 resources need the new label set: hit Reset to Coolify Default Labels (apps) or save the service, then restart.
- FQDN protocol matters: enter
https://app.example.comand Coolify auto-issues a Let's Encrypt cert; enterhttp://...and no TLS is requested — useful when sitting behind another proxy that terminates TLS (domains). Multiple domains: comma-separated. Path-based routing supported (https://example.com/api). - DNS pre-validation: since beta.191 Coolify pre-checks domain DNS via
1.1.1.1(override in Settings → Advanced). A misconfigured DNS will fail the Check DNS step before any cert request. - Let's Encrypt rate limit: 50 certs/registered-domain/week (LE limit, not Coolify's). Repeated failed cert requests during DNS troubleshooting can blow this — Coolify falls back to a self-signed cert on issuance failure (browser warning).
- www / non-www redirect: with read-only labels enabled, the Direction dropdown gives Allow both / Redirect to www / Redirect to non-www. Both URLs must be in the FQDN field. Manual:
redirectregexmiddleware in custom labels (redirects). - Port-80/443 conflict:
coolify-proxywill not start if 80/443 is occupied — even if you've configured the proxy to listen on different ports (issue #6234). To run Coolify behind another reverse proxy, edit/data/coolify/proxy/docker-compose.yml, change host ports to e.g. 5080/5443, restart, and set FQDNs ashttp://...(let the outer proxy do TLS). - Cloudflare Tunnel: tunnels deliver HTTP at the edge, so the FQDN→TLS handshake at Coolify must be
http://— usinghttps://causes loops. For TCP services (SSH, raw TCP databases) the cloudflared connector must run withnetwork_mode: hostor be deployed as a separate TCP-mode application (Cloudflare tunnels).
Persistent storage
- All Coolify state lives under
/data/coolify/on the destination host — applications, databases, ssh keys, proxy config, backups (backup/restore). - Application volumes default to bind mounts under
/data/coolify/applications/<uuid>/. The Storages tab in the UI is the source of truth — adding a volume there modifies the generated compose. - Compose-pack volumes: declare in your
docker-compose.yml. Coolify-specific sugar:is_directory: true— pre-create as a directory, not a file (default behavior is "guess from path")content: |— inline file content, supports${VAR}interpolation. Useful for config files generated at deploy time.
- Named Docker volumes work, but bind mounts are easier to back up. Both survive container recreation.
- Pitfall: deleting a service in the UI used to remove its bind-mount data; beta.474 (release notes) added a guard against accidental data loss when persistent containers are pruned. On older versions, export volumes manually before delete.
Databases (Postgres / MySQL / MariaDB / MongoDB / Redis / KeyDB / Dragonfly / Clickhouse)
- Backups are per-DB, configured via cron expression + S3-compatible destination (AWS S3, R2, B2, Wasabi, MinIO) (backups).
- Tooling per DB: Postgres →
pg_dump -Fc(custom format); MySQL →mysqldump; MariaDB →mariadb-dump; MongoDB →mongodump --gzip. Redis/KeyDB/Dragonfly use their respective dumpers; Clickhouse uses backup tables. - Restore Postgres: drop the dump file in Configuration → Import Backups (file upload or drag-drop). Default import command is
pg_restore— only valid for-Fcfiles. For cross-version upgrades (e.g. PG 14 → PG 16),-Fcis fragile — use plain or tar format on dump and switch the import command topsql(Postgres docs). - Default volume lives at
/data/coolify/databases/<uuid>/on the destination — back this up at the filesystem level if you need consistency-with-WAL recovery rather than logical dump. - Each DB type's UI has different fields — Postgres exposes
POSTGRES_USER/PASSWORD/DB, Mongo exposesMONGO_INITDB_ROOT_USERNAME/PASSWORD. Don't assume parity.
Upgrades and instance recovery
- Three modes (upgrade docs):
- Auto-update: control plane checks
cdn.coollabs.ioperiodically and self-installs new versions - Semi-automatic: notification only, Upgrade button in sidebar
- Manual: SSH +
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash(no version → latest)
- Auto-update: control plane checks
- Pin a version (or downgrade):
... | bash -s 4.0.0-beta.<N>. Disable auto-update first or you'll be re-upgraded next cycle (downgrade). - Downgrade caveats: schema migrations are forward-only — a downgrade across a migration boundary will fail to boot. Always back up the Coolify DB before upgrading so you can restore the matching schema if you need to roll back.
- Instance restore: requires the
APP_KEYfrom/data/coolify/source/.env(encrypts secrets at rest), the SSH keys at/data/coolify/ssh/keys/(or your destinations become unreachable), and the Postgres backup of the Coolify DB. SetAPP_PREVIOUS_KEYS=<old_key>in the new install's.envto decrypt secrets encrypted under the old key. Without all three you'll hit decryption or SSH auth errors (instance backup/restore). - Server Patching is OS-level (
apt/dnf/zypper) — manual click only, never auto-applies. Docker package updates restart Docker, briefly killing every container including Coolify itself (server patching). - Docker Swarm support is deprecated as of beta.474 — slated for removal in v5.
API and CLI
- API: REST under
/api/v1, bearer-token auth (Keys & Tokens → API tokens). Must enable API Access in Settings → Configuration → Advanced first. Resources exposed: applications, services, servers, deployments, projects, teams, environment variables, private keys, databases. coolify-cli: separate Go binary, installable viabrew install coollabsio/coolify-cli/coolify-cliorgo install github.com/coollabsio/coolify-cli/coolify@latest(repo). Multi-context (cloud + multiple self-hosted in one config). Authenticate withcoolify context add(self-hosted) /coolify context set-token cloud <token>. Same surface as the API — apps, services, servers, deployments, env vars, GitHub Apps.
Common pitfalls (cross-cutting)
- Env var saved but not in container: it was saved as build-only or runtime-only and the wrong layer was reloaded. Confirm both flags. After save, manually trigger Restart/Redeploy — there's no implicit deploy.
restart: alwaysin your compose combined with rolling-update conditions: rolling update needs to spawn a sibling, but a host-port mapping in the compose blocks it. Drop the host port and let Traefik do the routing.- Custom networks in compose: remove them. Coolify needs to attach its own bridge for Traefik label discovery — your custom net leaves the container off the proxy network and Traefik returns 502.
- Cert request loops while iterating on DNS: each failed attempt counts toward the LE 50/week limit. Toggle FQDN to
http://while debugging DNS, switch back tohttps://once1.1.1.1resolves correctly. - Container-name customization breaks rolling updates — the rolling logic spawns
<name>-new. If you renamed via composecontainer_name:, the rename collides. - Multi-server: app deployed but DNS still points at control plane → traffic 404s. Move DNS to the destination's IP. Coolify does not front non-control-plane traffic.
force=falseon deploy still rebuilds for some pack/state combinations — known issue #8104. Don't trust the param to cache reliably; treat any deploy as a potential full rebuild.
Authoritative references
Coolify docs (coolify.io/docs):
- Build Packs overview — Nixpacks/Dockerfile/Compose/Static
- Dockerfile build pack
- Static build pack
- Environment Variables — build/runtime, secrets,
COOLIFY_* - Docker Compose magic vars —
SERVICE_FQDN_*,SERVICE_PASSWORD_* - Domains / FQDN
- Traefik proxy overview
- www/non-www redirects
- Health checks
- Rolling updates
- Sentinel
- Supported proxies
- Server Patching
- Database Backups
- PostgreSQL service
- Upgrade / Downgrade
- Backup & Restore Coolify
- Cloudflare Tunnels
- GitHub Preview Deploy
- Deploy API endpoint
Source / release tracking:
- coollabsio/coolify Releases — beta cadence, breaking changes
- coollabsio/coolify-cli — CLI repo and install methods
Guardrails
Before recommending a non-trivial change (proxy switch, upgrade across ≥10 betas, DB engine version bump, restore from backup):
- Quote the specific UI path or API field, not a paraphrase
- Cite the doc page or a release note that confirms current behavior
- Make the recommendation conditional on the user's current Coolify version — features land and rename rapidly
Verify version: Coolify is fast-moving; check the docs/CHANGELOG before recommending any specific UI path or API field. UI labels, API params (e.g. force semantics, issue #8104), and even default packs (Static gained Nginx-only mode at beta.402; Swarm deprecated at beta.474) shift between betas. Anchor every recommendation on a freshly read doc page or release note.