docs: add knowledge base for Coolify Docker Compose deployment best practices
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
# Coolify Docker Compose Deployment Knowledge
|
||||
|
||||
Source:
|
||||
- Local archive: `viewpagesourcedockercomposecoolify.html`
|
||||
- Official page: https://coolify.io/docs/applications/build-packs/docker-compose
|
||||
- Cross-check: Coolify docs via Context7, `/coollabsio/coolify-docs`
|
||||
- Last reviewed: 2026-06-11
|
||||
|
||||
This note summarizes how to prepare a project that uses Docker Compose for deployment in Coolify. Use it as a checklist for other projects before deploying.
|
||||
|
||||
## Core Model
|
||||
|
||||
Coolify can deploy a project from a `docker-compose.yml` or `docker-compose.yaml` file. For Docker Compose deployments, the compose file becomes the main source of truth for services, build settings, environment variables, volumes, healthchecks, and service networking.
|
||||
|
||||
Coolify also attaches its reverse proxy to the stack network so public traffic can reach the exposed application service.
|
||||
|
||||
## Recommended Compose Rules
|
||||
|
||||
- Keep service names stable because services communicate by service name, for example `DB_HOST=db`.
|
||||
- Do not use `localhost` or `127.0.0.1` to connect from one container to another container.
|
||||
- Prefer `expose` for internal container ports and let Coolify handle public routing through its proxy.
|
||||
- Use `ports` only when you intentionally need host-level port binding.
|
||||
- Add `restart: unless-stopped` for long-running services.
|
||||
- Add healthchecks for services that must be healthy before the stack is considered ready.
|
||||
- Avoid hardcoded production secrets in the compose file.
|
||||
- Put persistent data in named volumes.
|
||||
- Do not define custom `networks:` unless you have a specific, tested reason.
|
||||
|
||||
## Networking
|
||||
|
||||
Coolify automatically creates an isolated bridge network for each Compose stack. All services in the same stack can reach each other by service name.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
DB_HOST: db
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
```
|
||||
|
||||
The `app` container connects to MySQL using `db:3306`, not `localhost:3306`.
|
||||
|
||||
## Do Not Define Custom Networks
|
||||
|
||||
For normal Coolify Compose deployments, remove custom networks:
|
||||
|
||||
```yaml
|
||||
# Avoid this in Coolify unless you know exactly why it is needed.
|
||||
services:
|
||||
frontend:
|
||||
networks:
|
||||
- my-network
|
||||
backend:
|
||||
networks:
|
||||
- my-network
|
||||
|
||||
networks:
|
||||
my-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
Why: Coolify already creates a managed network and connects Traefik to it. Extra custom networks can cause intermittent routing problems because the proxy may select an unreachable container IP. Symptoms include hanging requests and `504 Gateway Timeout`.
|
||||
|
||||
Use the auto-created network unless you need cross-stack communication. For cross-stack communication, use Coolify's predefined network option and reference the full remote service name when required.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Coolify parses compose environment variables and exposes many of them in its UI.
|
||||
|
||||
Common forms:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
APP_ENV: production
|
||||
APP_DEBUG: "${APP_DEBUG:-false}"
|
||||
APP_KEY: "${APP_KEY:?Set APP_KEY in Coolify}"
|
||||
DB_PASSWORD: "${DB_PASSWORD:?Set DB_PASSWORD in Coolify}"
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- `VALUE`: hardcoded into the container.
|
||||
- `${VAR}`: comes from Coolify/project environment.
|
||||
- `${VAR:-default}`: uses `default` if unset.
|
||||
- `${VAR:?message}`: required; deployment should fail early if unset.
|
||||
|
||||
For production, use required variables for secrets so a deploy cannot silently start with weak defaults.
|
||||
|
||||
## Magic Environment Variables
|
||||
|
||||
Coolify supports magic variables with the `SERVICE_<TYPE>_<IDENTIFIER>` pattern. They can generate URLs, FQDNs, passwords, users, base64 strings, and hex strings.
|
||||
|
||||
Examples:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
APP_URL: "${SERVICE_URL_APP_80}"
|
||||
DB_PASSWORD: "${SERVICE_PASSWORD_64_MYSQL}"
|
||||
```
|
||||
|
||||
Common types:
|
||||
|
||||
- `SERVICE_URL_*`
|
||||
- `SERVICE_FQDN_*`
|
||||
- `SERVICE_PASSWORD_*`
|
||||
- `SERVICE_PASSWORD_64_*`
|
||||
- `SERVICE_PASSWORDWITHSYMBOLS_64_*`
|
||||
- `SERVICE_BASE64_*`
|
||||
- `SERVICE_HEX_*`
|
||||
|
||||
Use these only when you intentionally want Coolify to create and own that variable.
|
||||
|
||||
Important pitfall: if a magic variable is declared in `docker-compose.yaml`, Coolify may block deletion from the UI with a message like:
|
||||
|
||||
```text
|
||||
Cannot delete environment variable 'SERVICE_FQDN_APP'
|
||||
Please remove it from the Docker Compose file first.
|
||||
```
|
||||
|
||||
Fix:
|
||||
|
||||
1. Remove the related `SERVICE_FQDN_*` or `SERVICE_URL_*` entry from `docker-compose.yaml`.
|
||||
2. Redeploy or resync the resource in Coolify.
|
||||
3. Delete the variable again from the Coolify UI.
|
||||
|
||||
## Domains and Public Ports
|
||||
|
||||
For web apps, Coolify needs to know which service and port should receive public traffic.
|
||||
|
||||
Recommended project-side setup:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
expose:
|
||||
- "80"
|
||||
```
|
||||
|
||||
Then configure the domain/proxy in Coolify to target service `app` on port `80`.
|
||||
|
||||
Do not add `SERVICE_URL_APP_80` just to expose the app if you prefer managing domains in the Coolify UI. Declaring `SERVICE_URL_*` makes Coolify manage related magic variables.
|
||||
|
||||
## Volumes and Persistent Data
|
||||
|
||||
Use named volumes for database and persistent application data:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
```
|
||||
|
||||
For application uploads, logs, or generated files, map only the directories that must persist. Do not persist build output unless the app requires runtime writes there.
|
||||
|
||||
## Healthchecks
|
||||
|
||||
Use healthchecks for databases and web services:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/ >/dev/null || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
db:
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
Use `depends_on` with `condition: service_healthy` when the app must wait for the database:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
```
|
||||
|
||||
For one-time jobs or migration-only services, Coolify supports excluding a service from overall healthchecks:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
migrate:
|
||||
exclude_from_hc: true
|
||||
```
|
||||
|
||||
## Laravel Notes
|
||||
|
||||
For Laravel projects:
|
||||
|
||||
- Use a real web server in production, such as Apache or Nginx with PHP-FPM.
|
||||
- Do not use `php artisan serve` as the production command.
|
||||
- Set `APP_ENV=production`.
|
||||
- Set `APP_DEBUG=false`.
|
||||
- Set `APP_KEY` in Coolify.
|
||||
- Set `APP_URL` to the final public URL.
|
||||
- Use `DB_HOST` as the Compose service name, usually `db`.
|
||||
- Run migrations intentionally during entrypoint/deploy, and ensure seeders are idempotent if they run on every deploy.
|
||||
- Keep `.env` out of Git; define production values in Coolify environment variables.
|
||||
|
||||
## MySQL Password Warning
|
||||
|
||||
MySQL initializes credentials only when the database volume is first created. If you change `MYSQL_PASSWORD`, `MYSQL_ROOT_PASSWORD`, or `DB_PASSWORD` after the volume already exists, the existing database users may still use the old password.
|
||||
|
||||
If the app cannot connect after changing passwords:
|
||||
|
||||
- Set `DB_PASSWORD` back to the password used when the volume was initialized, or
|
||||
- Rotate the database password manually inside MySQL, or
|
||||
- Recreate the database volume if data can be deleted.
|
||||
|
||||
## Minimal Production Template
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "80"
|
||||
environment:
|
||||
APP_ENV: "${APP_ENV:-production}"
|
||||
APP_DEBUG: "${APP_DEBUG:-false}"
|
||||
APP_URL: "${APP_URL:?Set APP_URL in Coolify}"
|
||||
APP_KEY: "${APP_KEY:?Set APP_KEY in Coolify}"
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: "${DB_HOST:-db}"
|
||||
DB_PORT: "${DB_PORT:-3306}"
|
||||
DB_DATABASE: "${DB_DATABASE:-app}"
|
||||
DB_USERNAME: "${DB_USERNAME:-app}"
|
||||
DB_PASSWORD: "${DB_PASSWORD:?Set DB_PASSWORD in Coolify}"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/ >/dev/null || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_DATABASE: "${DB_DATABASE:-app}"
|
||||
MYSQL_USER: "${DB_USERNAME:-app}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD:?Set DB_PASSWORD in Coolify}"
|
||||
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:?Set DB_ROOT_PASSWORD in Coolify}"
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
```
|
||||
|
||||
## Pre-Deploy Checklist
|
||||
|
||||
- `docker compose config --quiet` passes locally.
|
||||
- `docker compose build app` passes locally or in CI.
|
||||
- App image exposes the same port configured in Coolify.
|
||||
- No custom `networks:` are defined for normal stacks.
|
||||
- Public app service has a healthcheck.
|
||||
- Database service has a healthcheck.
|
||||
- App uses database service hostname, not `localhost`.
|
||||
- Required secrets are set in Coolify.
|
||||
- No weak fallback secrets remain for production.
|
||||
- Persistent data is stored in named volumes.
|
||||
- Magic `SERVICE_*` variables are only present when intentionally managed by Coolify.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot delete `SERVICE_FQDN_APP`
|
||||
|
||||
Cause: a related magic variable is still declared in compose, commonly `SERVICE_FQDN_APP`, `SERVICE_URL_APP`, or `SERVICE_URL_APP_80`.
|
||||
|
||||
Fix: remove it from compose, redeploy/resync, then delete it in Coolify.
|
||||
|
||||
### 504 Gateway Timeout
|
||||
|
||||
Check:
|
||||
|
||||
- Remove custom networks.
|
||||
- Confirm Coolify routes to the correct service and port.
|
||||
- Confirm the app listens on `0.0.0.0`, not only `127.0.0.1`.
|
||||
- Check app healthcheck and container logs.
|
||||
|
||||
### App cannot connect to database
|
||||
|
||||
Check:
|
||||
|
||||
- `DB_HOST` is the database service name, for example `db`.
|
||||
- `DB_PORT=3306`.
|
||||
- `DB_DATABASE`, `DB_USERNAME`, and `DB_PASSWORD` match the database service.
|
||||
- Existing MySQL volume may still use an old password.
|
||||
|
||||
### Environment value keeps coming back
|
||||
|
||||
Check whether that variable is declared in `docker-compose.yaml`. Coolify treats compose-declared variables as managed by the compose file.
|
||||
Reference in New Issue
Block a user