Self-hosted PaaS ("Heroku/Vercel you run yourself"). Deploys apps, databases and
prebuilt services onto your own servers — with a built-in reverse proxy, automatic TLS
and Git-based deploys.
80/443 and routes by Host header to the right containers.| Application | Service | |
|---|---|---|
| What | your own code / compose | prebuilt stack (WikiJs, Plausible, …) |
| Setting the domain | "Domains for …" field in General tab | per sub-service under Settings or via SERVICE_FQDN_* env var |
| Port | default 80 is assumed automatically |
often must be appended to the domain (:3000) |
| Env vars | free | partly magic SERVICE_* variables from the template |
Rule of thumb: Application → domain in the General tab. Service → domain on the
sub-service or via SERVICE_FQDN.
*.coolify.example.dev A <server-IP>coolify.example.devapp.coolify.example.devA wildcard record covers exactly one level.
*.coolify.example.devmatches
app.coolify.example.dev, nota.b.coolify.example.dev. Keep it flat.
https://wiki.coolify.example.dev:3000
The :3000 is the internal container port, not the public one. Publicly everything
stays on 443. Traefik only uses the port to know where to route.
.dev domains force HTTPSThe entire .dev TLD is on the browsers' HSTS preload list → http:// is rewritten
to https:// automatically. Without a working certificate you see nothing.
Firewall: keep 80 and 443 open (80 is needed for the ACME challenge).
http://Cause: Traefik terminates TLS and forwards internally as HTTP. The framework sees
http → builds http:// URLs → the browser blocks them.
Fix (framework-agnostic, in the nginx config):
map $http_x_forwarded_proto $fastcgi_https {
default '';
https on;
}
# inside the PHP location:
fastcgi_param HTTPS $fastcgi_https;
Fix (Laravel-specific, additionally for the correct client IP):
// bootstrap/app.php
$middleware->trustProxies(at: '*');
Every framework has its knob: Symfony
trusted_proxies, Railsassume_ssl,
Expresstrust proxy, DjangoSECURE_PROXY_SSL_HEADER. Pure SPAs using relative
paths are not affected.
env_file: .env in the compose.env is gitignored → absent in the cloned repo → deploy fails.
Use an explicit environment: block instead and set secrets as ${VAR} in the Coolify UI.
docker-compose.override.yml gets merged unintentionallydocker compose merges the override automatically. In Coolify set the Docker Compose
Location explicitly to /docker-compose.yml, otherwise dev bind-mounts leak into prod.
${}-referenced varsCoolify parses the compose for ${VAR} and only surfaces those in the UI. It doesn't know
Laravel's own .env. Any var that should reach the container must be explicitly
referenced in the environment: block.
env_file, add an environment: block on the app serviceAPP_KEY, APP_URL, DB_PASSWORD, DB_ROOT_PASSWORDAPP_ENV=production, APP_DEBUG=falseAPP_URL == the domain you set (https://…)expose: 80 (no host port → no conflict)/docker-compose.ymlfastcgi_param HTTPS / trustProxies)php artisan migrate --force80 + 443 open in the firewall| Variable | Meaning |
|---|---|
SERVICE_FQDN_<NAME> |
a service's hostname (no protocol) |
SERVICE_URL_<NAME> |
full URL incl. https:// |
SERVICE_FQDN_<NAME>_<PORT> |
port-specific — what Traefik actually uses |
Known bug: when changing a service domain in the UI, sometimes only the
port-specific vars update while the generic ones keep the old value. After changing,
check both in the Env tab.
As of July 2026 · Compiled from the official Coolify docs + hands-on deploy experience.