Files
Portainer/COOLIFY-TEMPLATE.md
root ca4fd7be30 fix: hardcode OO domain in Traefik label; Coolify does not expand vars in labels
- 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>
2026-03-23 02:27:38 +00:00

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 letsencrypt via 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.enable
  • traefik.docker.network
  • traefik.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

  1. Ports Exposes: set to the app's HTTP port (must match loadbalancer.server.port label)
  2. Domain: set FQDN (e.g. myapp.sherlockhomeless.net)
  3. Base Directory: set to the subdirectory (e.g. /gitea, /n8n)
  4. Environment Variables: fill from stack.env template

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:

  1. Que /opt/traefik/dynamic/coolify.yaml existe y tiene routers http/https
  2. Que coolify-proxy puede resolver coolify:8080 (ambos en red proxy)

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.