- nextcloud/docker-compose.yml: X-Forwarded-Host=onlyoffice.sherlockhomeless.net
(was ${OO_DOMAIN} which Coolify leaves unexpanded → OnlyOffice loads assets
from https://${oo_domain}/ and editor breaks entirely)
- nextcloud/stack.env: update placeholder domains to real ones
- COOLIFY-TEMPLATE.md: add Gotcha 6 about label variable expansion
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7.3 KiB
Coolify Compose Template — TheHomelessSherlock
Proxy & Networking Rules
Our Setup
- Proxy:
coolify-proxy(Traefik v3) configured with--providers.docker.network=proxy - TLS: certresolver
letsencryptvia HTTP challenge - Domain routing: configured in Coolify UI, NOT in compose labels
Rule 1 — Every service exposed via HTTP/HTTPS must be on the proxy network
services:
myapp:
image: myimage:latest
pull_policy: always
networks:
- internal
- proxy # ← mandatory for Traefik to reach it
labels:
traefik.http.services.myapp.loadbalancer.server.port: "3000" # ← mandatory port label
networks:
internal:
driver: bridge
proxy:
external: true # ← always external, pre-created by Coolify
Rule 2 — Internal-only services do NOT need proxy
services:
mydb:
image: postgres:16
networks:
- internal # ← only internal, no proxy
networks:
internal:
driver: bridge
Rule 3 — Mail-relay access via mail_internal network
Services that need to send mail via the internal mail-relay must be on mail_internal:
services:
myapp:
networks:
- internal
- proxy
- mail_internal # ← only services that need mail
environment:
SMTP_HOST: mail-relay # ← container name as hostname
SMTP_PORT: 587
networks:
mail_internal:
external: true # ← pre-created: docker network create mail_internal
Rule 4 — Labels: only port + optional middleware
Coolify manages routing labels automatically. Only set:
labels:
# MANDATORY: tell Traefik which port your app listens on
traefik.http.services.myapp.loadbalancer.server.port: "3000"
# OPTIONAL: define reusable middleware (e.g. security headers, redirects)
traefik.http.middlewares.myapp-headers.headers.stsSeconds: "31536000"
traefik.http.middlewares.myapp-headers.headers.stsIncludeSubdomains: "true"
NEVER add these (Coolify adds them automatically based on UI config):
traefik.enabletraefik.docker.networktraefik.http.routers.*
Rule 5 — pull_policy: always on main service
Ensures the latest image is pulled on every deploy:
services:
myapp:
image: myimage:latest
pull_policy: always # ← add to main/frontend service only
Rule 6 — Authentik middleware
To protect a service with Authentik SSO, use the middleware defined in the authentik stack:
In Coolify UI: add this label to your application under "Labels":
traefik.http.routers.<your-router-name>.middlewares=ths-authentik@docker
Or in your compose labels (will be merged with Coolify's auto-labels):
labels:
traefik.http.services.myapp.loadbalancer.server.port: "3000"
# The router name Coolify generates follows: https-0-<uuid>-<servicename>
# Use Coolify UI "Labels" field to add the middleware after first deploy
Full Example — Minimal web app with DB and mail
services:
myapp:
image: myimage:latest
pull_policy: always
restart: unless-stopped
depends_on:
- myapp-db
environment:
DB_HOST: myapp-db
DB_PORT: 5432
SMTP_HOST: mail-relay
SMTP_PORT: 587
networks:
- internal
- proxy
- mail_internal
labels:
traefik.http.services.myapp.loadbalancer.server.port: "3000"
myapp-db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- /opt/myapp/db:/var/lib/postgresql/data:Z
networks:
- internal
networks:
internal:
driver: bridge
proxy:
external: true
mail_internal:
external: true
Pre-created networks on the host
These networks must exist before deploying stacks that use them:
# Already created by Coolify:
# docker network create proxy ← created as part of Coolify install
# Create manually once:
docker network create mail_internal
Coolify UI checklist per application
- Ports Exposes: set to the app's HTTP port (must match
loadbalancer.server.portlabel) - Domain: set FQDN (e.g.
myapp.sherlockhomeless.net) - Base Directory: set to the subdirectory (e.g.
/gitea,/n8n) - Environment Variables: fill from
stack.envtemplate
Gotchas del sistema — leer antes de tocar el proxy
Gotcha 1 — Directorio dynamic del proxy NO es el de Coolify
El proxy coolify-proxy monta su directorio de configuración dinámica desde:
/opt/traefik/dynamic → /dynamic (dentro del contenedor)
NO desde /data/coolify/proxy/dynamic/ (ese directorio existe pero Traefik NO lo lee).
Si necesitas añadir rutas manuales (por ejemplo, para el dashboard de Coolify), edita:
/opt/traefik/dynamic/coolify.yaml # ← aquí es donde importa
Gotcha 2 — Entrypoints del proxy: http y https (NO web/websecure)
Coolify genera automáticamente labels de Traefik con:
entryPoints=http(puerto 80)entryPoints=https(puerto 443)
El proxy (/data/coolify/proxy/docker-compose.yml) está configurado con esos mismos nombres.
El contenedor coolify propio tiene labels antiguas con websecure — no importa porque
su routing está definido en /opt/traefik/dynamic/coolify.yaml.
Si el dashboard de Coolify da 404, verificar:
- Que
/opt/traefik/dynamic/coolify.yamlexiste y tiene routershttp/https - Que
coolify-proxypuede resolvercoolify:8080(ambos en redproxy)
Gotcha 3 — Variables en bind mounts → Coolify las convierte en volúmenes nombrados
# ❌ MAL: Coolify NO resuelve la variable → crea volumen Docker nombrado vacío
volumes:
- ${MY_DATA_PATH}:/var/lib/postgresql/data:Z
# ✅ BIEN: rutas hardcodeadas
volumes:
- /opt/myapp/postgres:/var/lib/postgresql/data:Z
Gotcha 4 — SSH MaxSessions
/etc/ssh/sshd_config tiene MaxSessions 20 (cambiado de 2).
Coolify abre múltiples sesiones SSH en paralelo durante el deploy.
Si vuelve a bajar a 2 (upgrade del OS, etc.), todos los deploys fallarán con exit code 255.
grep MaxSessions /etc/ssh/sshd_config # debe ser >= 10
Gotcha 5 — Todos los datos del host están en /opt//
Convención de este servidor: todos los bind mounts van bajo /opt/<nombre-stack>/.
Antes de hardcodear rutas en un compose, verificar siempre:
ls /opt/<stack>/ # ej: /opt/adguard/, /opt/authentik/, /opt/gitea/, etc.
Si existe el directorio con datos → usar bind mount a esa ruta. Si no existe → crear el directorio antes de desplegar, o usar named volume.
Gotcha 6 — Variables en labels de Traefik NO se expanden en Coolify
Coolify expande ${VAR} en la sección environment: pero NO en labels:.
# ❌ MAL: quedará como literal ${OO_DOMAIN} en el label del contenedor
labels:
- traefik.http.middlewares.foo.headers.customRequestHeaders.X-Forwarded-Host=${OO_DOMAIN}
# ✅ BIEN: hardcodear el valor real
labels:
- traefik.http.middlewares.foo.headers.customRequestHeaders.X-Forwarded-Host=onlyoffice.sherlockhomeless.net
Esto afecta especialmente a headers X-Forwarded-Host de OnlyOffice — si queda
como literal, el JS de OnlyOffice intenta cargar assets de https://${oo_domain}/...
y el editor de documentos falla completamente.