From 9e82928049a062dc48c09d0ead82739d04af360e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 23 Mar 2026 01:51:55 +0000 Subject: [PATCH] feat: migrate all stacks to Coolify (proxy network, clean labels, pull_policy) - Remove traefik.enable, traefik.docker.network, traefik.http.routers.* from all services - Keep traefik.http.services..loadbalancer.server.port labels - Keep all middleware definitions (forwardauth, headers, redirects) - Add pull_policy: always to main/frontend services - Add proxy network + label to gitea and karakeep (previously missing) - Add COOLIFY-TEMPLATE.md reference guide Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- COOLIFY-TEMPLATE.md | 171 ++++++++++++++++++++++++++++++++ adguard/docker-compose.yml | 15 +-- authentik/docker-compose.yml | 23 +---- gitea/docker-compose.yml | 5 + karakeep/docker-compose.yml | 5 + mail-relay/docker-compose.yml | 3 +- media-server/docker-compose.yml | 49 ++------- nextcloud/docker-compose.yml | 19 +--- paperless/docker-compose.yml | 20 +--- trilium/docker-compose.yml | 24 +---- wireguard/docker-compose.yml | 14 +-- 11 files changed, 198 insertions(+), 150 deletions(-) create mode 100644 COOLIFY-TEMPLATE.md diff --git a/COOLIFY-TEMPLATE.md b/COOLIFY-TEMPLATE.md new file mode 100644 index 0000000..8a0fc9c --- /dev/null +++ b/COOLIFY-TEMPLATE.md @@ -0,0 +1,171 @@ +# 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 + +```yaml +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 + +```yaml +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`: + +```yaml +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: + +```yaml +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: + +```yaml +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..middlewares=ths-authentik@docker +``` + +Or in your compose labels (will be merged with Coolify's auto-labels): +```yaml +labels: + traefik.http.services.myapp.loadbalancer.server.port: "3000" + # The router name Coolify generates follows: https-0-- + # Use Coolify UI "Labels" field to add the middleware after first deploy +``` + +## Full Example — Minimal web app with DB and mail + +```yaml +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: + +```bash +# 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 diff --git a/adguard/docker-compose.yml b/adguard/docker-compose.yml index 466b793..ec0b471 100644 --- a/adguard/docker-compose.yml +++ b/adguard/docker-compose.yml @@ -2,6 +2,7 @@ services: adguardhome: image: ${ADGUARD_IMAGE} container_name: adguardhome + pull_policy: always restart: unless-stopped volumes: @@ -22,22 +23,8 @@ services: ipv4_address: ${ADGUARD_IPV4} labels: - traefik.enable: "true" - traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}" - - # Router HTTPS para el panel web - traefik.http.routers.adguard.rule: "Host(`${ADGUARD_DOMAIN}`)" - traefik.http.routers.adguard.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}" - traefik.http.routers.adguard.tls.certresolver: "${TRAEFIK_CERTRESOLVER}" - - # Panel interno de AdGuard (HTTP en el contenedor) - # OJO: si es la primera vez y el panel escucha en 3000, cambia a 3000 traefik.http.services.adguard.loadbalancer.server.port: "${ADGUARD_HTTP_PORT}" - # Proteger el panel con Authentik (middleware definido en authentik-server) - traefik.http.routers.adguard.middlewares: "${TRAEFIK_AUTH_MIDDLEWARE}" - networks: proxy: external: true - diff --git a/authentik/docker-compose.yml b/authentik/docker-compose.yml index 2278071..a97fba4 100644 --- a/authentik/docker-compose.yml +++ b/authentik/docker-compose.yml @@ -25,6 +25,7 @@ services: ths-authentik-server: image: ${AUTHENTIK_IMAGE} container_name: ths-authentik-server + pull_policy: always restart: unless-stopped command: ["server"] environment: @@ -54,33 +55,14 @@ services: - proxy labels: - traefik.enable: "true" - traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}" - # Service Authentik (panel + endpoints) traefik.http.services.ths-authentik.loadbalancer.server.port: "${AUTHENTIK_HTTP_PORT}" - # Panel Authentik (auth.thehomelesssherlock.com) - traefik.http.routers.ths-authentik.rule: "Host(`${AUTHENTIK_DOMAIN}`)" - traefik.http.routers.ths-authentik.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}" - traefik.http.routers.ths-authentik.tls: "true" - traefik.http.routers.ths-authentik.tls.certresolver: "${TRAEFIK_CERTRESOLVER}" - traefik.http.routers.ths-authentik.service: "ths-authentik" - - # Middleware forwardAuth (para proteger otros servicios) -> usar ths-ths-authentik@docker en tus stacks THS + # Middleware forwardAuth (para proteger otros servicios) -> usar ths-authentik@docker en tus stacks THS traefik.http.middlewares.ths-authentik.forwardauth.address: "http://ths-authentik-server:${AUTHENTIK_HTTP_PORT}/outpost.goauthentik.io/auth/traefik" traefik.http.middlewares.ths-authentik.forwardauth.trustForwardHeader: "true" traefik.http.middlewares.ths-authentik.forwardauth.authResponseHeaders: "X-Authentik-Username,X-Authentik-Groups,X-Authentik-Email,X-Authentik-Uid,X-Authentik-Jwt" - # OUTPOST genérico para TODO el dominio THS (subdominios + apex + www) - # ✅ Sin comas dentro de Host() - traefik.http.routers.ths-authentik-outpost.rule: "(HostRegexp(`{subdomain:[a-z0-9-]+}.thehomelesssherlock.com`) || Host(`thehomelesssherlock.com`) || Host(`www.thehomelesssherlock.com`)) && PathPrefix(`/outpost.goauthentik.io/`)" - traefik.http.routers.ths-authentik-outpost.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}" - traefik.http.routers.ths-authentik-outpost.tls: "true" - traefik.http.routers.ths-authentik-outpost.tls.certresolver: "${TRAEFIK_CERTRESOLVER}" - traefik.http.routers.ths-authentik-outpost.service: "ths-authentik" - traefik.http.routers.ths-authentik-outpost.priority: "1000" - ths-authentik-worker: image: ${AUTHENTIK_IMAGE} container_name: ths-authentik-worker @@ -109,4 +91,3 @@ networks: external: true ths_authentik_internal: driver: bridge - diff --git a/gitea/docker-compose.yml b/gitea/docker-compose.yml index 9be6c12..5ef3a36 100644 --- a/gitea/docker-compose.yml +++ b/gitea/docker-compose.yml @@ -65,8 +65,11 @@ services: - ${GITEA_DATA_PATH}:/data:Z networks: - gitea + - proxy ports: - "${GITEA_SSH_PORT}:${GITEA_SSH_PORT}" + labels: + traefik.http.services.gitea.loadbalancer.server.port: "${GITEA_HTTP_PORT}" gitea-runner: image: ${GITEA_RUNNER_IMAGE} @@ -88,3 +91,5 @@ services: networks: gitea: driver: bridge + proxy: + external: true diff --git a/karakeep/docker-compose.yml b/karakeep/docker-compose.yml index c14932d..715683e 100644 --- a/karakeep/docker-compose.yml +++ b/karakeep/docker-compose.yml @@ -40,6 +40,9 @@ services: - karakeep-chrome networks: - karakeep_internal + - proxy + labels: + traefik.http.services.karakeep.loadbalancer.server.port: "3000" karakeep-chrome: image: gcr.io/zenika-hub/alpine-chrome:124 @@ -70,3 +73,5 @@ services: networks: karakeep_internal: driver: bridge + proxy: + external: true diff --git a/mail-relay/docker-compose.yml b/mail-relay/docker-compose.yml index 8f078b9..54a3334 100644 --- a/mail-relay/docker-compose.yml +++ b/mail-relay/docker-compose.yml @@ -2,6 +2,7 @@ services: mail-relay: image: ${MAIL_RELAY_IMAGE} container_name: mail-relay + pull_policy: always restart: unless-stopped environment: TZ: ${TZ} @@ -49,4 +50,4 @@ networks: driver: bridge ipam: config: - - subnet: ${MAIL_RELAY_SUBNET} \ No newline at end of file + - subnet: ${MAIL_RELAY_SUBNET} diff --git a/media-server/docker-compose.yml b/media-server/docker-compose.yml index 7789cd7..525d9a8 100644 --- a/media-server/docker-compose.yml +++ b/media-server/docker-compose.yml @@ -21,6 +21,7 @@ services: prowlarr: image: lscr.io/linuxserver/prowlarr:latest container_name: prowlarr + pull_policy: always environment: - PUID=0 - PGID=0 @@ -32,18 +33,12 @@ services: - media - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - traefik.http.routers.prowlarr.rule=Host(`${PROWLARR_HOST}`) - - traefik.http.routers.prowlarr.entrypoints=websecure - - traefik.http.routers.prowlarr.tls=true - - traefik.http.routers.prowlarr.tls.certresolver=letsencrypt - - traefik.http.routers.prowlarr.middlewares=ths-authentik@docker - traefik.http.services.prowlarr.loadbalancer.server.port=9696 jackett: image: lscr.io/linuxserver/jackett:latest container_name: jackett + pull_policy: always environment: - PUID=0 - PGID=0 @@ -55,18 +50,12 @@ services: - media - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - traefik.http.routers.jackett.rule=Host(`${JACKETT_HOST}`) - - traefik.http.routers.jackett.entrypoints=websecure - - traefik.http.routers.jackett.tls=true - - traefik.http.routers.jackett.tls.certresolver=letsencrypt - - traefik.http.routers.jackett.middlewares=ths-authentik@docker - traefik.http.services.jackett.loadbalancer.server.port=9117 sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr + pull_policy: always environment: - PUID=0 - PGID=0 @@ -80,18 +69,12 @@ services: - media - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - traefik.http.routers.sonarr.rule=Host(`${SONARR_HOST}`) - - traefik.http.routers.sonarr.entrypoints=websecure - - traefik.http.routers.sonarr.tls=true - - traefik.http.routers.sonarr.tls.certresolver=letsencrypt - - traefik.http.routers.sonarr.middlewares=ths-authentik@docker - traefik.http.services.sonarr.loadbalancer.server.port=8989 radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr + pull_policy: always environment: - PUID=0 - PGID=0 @@ -105,18 +88,12 @@ services: - media - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - traefik.http.routers.radarr.rule=Host(`${RADARR_HOST}`) - - traefik.http.routers.radarr.entrypoints=websecure - - traefik.http.routers.radarr.tls=true - - traefik.http.routers.radarr.tls.certresolver=letsencrypt - - traefik.http.routers.radarr.middlewares=ths-authentik@docker - traefik.http.services.radarr.loadbalancer.server.port=7878 jellyseerr: image: fallenbagel/jellyseerr:latest container_name: jellyseerr + pull_policy: always environment: - LOG_LEVEL=debug - TZ=${TZ:-Europe/Madrid} @@ -127,19 +104,13 @@ services: - media - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - traefik.http.routers.jellyseerr.rule=Host(`${JELLYSEERR_HOST}`) - - traefik.http.routers.jellyseerr.entrypoints=websecure - - traefik.http.routers.jellyseerr.tls=true - - traefik.http.routers.jellyseerr.tls.certresolver=letsencrypt - - traefik.http.routers.jellyseerr.middlewares=ths-authentik@docker - traefik.http.services.jellyseerr.loadbalancer.server.port=5055 # Opcional: Jellyfin en VPS (sin GPU) jellyfin: image: lscr.io/linuxserver/jellyfin:latest container_name: jellyfin-vps + pull_policy: always environment: - PUID=0 - PGID=0 @@ -155,12 +126,4 @@ services: - media - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - traefik.http.routers.jellyfin.rule=Host(`${JELLYFIN_HOST}`) - - traefik.http.routers.jellyfin.entrypoints=websecure - - traefik.http.routers.jellyfin.tls=true - - traefik.http.routers.jellyfin.tls.certresolver=letsencrypt - - traefik.http.routers.jellyfin.middlewares=ths-authentik@docker - traefik.http.services.jellyfin.loadbalancer.server.port=8096 - diff --git a/nextcloud/docker-compose.yml b/nextcloud/docker-compose.yml index 4fdc5dc..5ca8725 100644 --- a/nextcloud/docker-compose.yml +++ b/nextcloud/docker-compose.yml @@ -34,6 +34,7 @@ services: nextcloud: image: nextcloud:33-apache container_name: nextcloud + pull_policy: always restart: unless-stopped depends_on: - nextcloud-db @@ -79,15 +80,6 @@ services: - proxy - mail_internal labels: - - traefik.enable=true - - traefik.docker.network=proxy - - - traefik.http.routers.nextcloud.rule=Host(`${NC_DOMAIN}`) - - traefik.http.routers.nextcloud.entrypoints=websecure - - traefik.http.routers.nextcloud.tls=true - - traefik.http.routers.nextcloud.tls.certresolver=${TRAEFIK_CERTRESOLVER} - - traefik.http.routers.nextcloud.middlewares=nc-dav,nc-secure-headers - - traefik.http.middlewares.nc-dav.redirectregex.permanent=true - traefik.http.middlewares.nc-dav.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav - traefik.http.middlewares.nc-dav.redirectregex.replacement=https://$${1}/remote.php/dav @@ -156,15 +148,6 @@ services: - nextcloud_internal - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - - traefik.http.routers.onlyoffice.rule=Host(`${OO_DOMAIN}`) - - traefik.http.routers.onlyoffice.entrypoints=websecure - - traefik.http.routers.onlyoffice.tls=true - - traefik.http.routers.onlyoffice.tls.certresolver=${TRAEFIK_CERTRESOLVER} - - traefik.http.routers.onlyoffice.middlewares=oo-secure-headers,oo-forwarded - - traefik.http.middlewares.oo-secure-headers.headers.stsSeconds=31536000 - traefik.http.middlewares.oo-secure-headers.headers.stsIncludeSubdomains=true - traefik.http.middlewares.oo-secure-headers.headers.stsPreload=true diff --git a/paperless/docker-compose.yml b/paperless/docker-compose.yml index 7b79bd9..e131b0e 100644 --- a/paperless/docker-compose.yml +++ b/paperless/docker-compose.yml @@ -43,6 +43,7 @@ services: paperless: image: ghcr.io/paperless-ngx/paperless-ngx:latest container_name: paperless + pull_policy: always restart: unless-stopped depends_on: - paperless-db @@ -85,15 +86,6 @@ services: - proxy - mail_internal labels: - - traefik.enable=true - - traefik.docker.network=proxy - - - traefik.http.routers.paperless.rule=Host(`${PAPERLESS_DOMAIN}`) - - traefik.http.routers.paperless.entrypoints=websecure - - traefik.http.routers.paperless.tls=true - - traefik.http.routers.paperless.tls.certresolver=${TRAEFIK_CERTRESOLVER} - - traefik.http.routers.paperless.middlewares=paperless-secure-headers - - traefik.http.middlewares.paperless-secure-headers.headers.stsSeconds=31536000 - traefik.http.middlewares.paperless-secure-headers.headers.stsIncludeSubdomains=true - traefik.http.middlewares.paperless-secure-headers.headers.stsPreload=true @@ -105,6 +97,7 @@ services: paperless-ai: image: clusterzx/paperless-ai:latest container_name: paperless-ai + pull_policy: always restart: unless-stopped depends_on: - paperless @@ -116,15 +109,6 @@ services: - paperless_internal - proxy labels: - - traefik.enable=true - - traefik.docker.network=proxy - - - traefik.http.routers.paperless-ai.rule=Host(`${PAPERLESS_AI_DOMAIN}`) - - traefik.http.routers.paperless-ai.entrypoints=websecure - - traefik.http.routers.paperless-ai.tls=true - - traefik.http.routers.paperless-ai.tls.certresolver=${TRAEFIK_CERTRESOLVER} - - traefik.http.routers.paperless-ai.middlewares=paperless-ai-secure-headers - - traefik.http.middlewares.paperless-ai-secure-headers.headers.stsSeconds=31536000 - traefik.http.middlewares.paperless-ai-secure-headers.headers.stsIncludeSubdomains=true - traefik.http.middlewares.paperless-ai-secure-headers.headers.stsPreload=true diff --git a/trilium/docker-compose.yml b/trilium/docker-compose.yml index daabea3..bf157c2 100644 --- a/trilium/docker-compose.yml +++ b/trilium/docker-compose.yml @@ -2,6 +2,7 @@ services: trilium: image: ${TRILIUM_IMAGE} container_name: trilium + pull_policy: always restart: unless-stopped hostname: ${TRILIUM_HOSTNAME} @@ -18,28 +19,8 @@ services: - proxy labels: - traefik.enable: "true" - traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}" - - # Router HTTPS - dominio principal - traefik.http.routers.trilium.rule: "Host(`${TRILIUM_DOMAIN_1}`)" - traefik.http.routers.trilium.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}" - traefik.http.routers.trilium.tls: "true" - traefik.http.routers.trilium.tls.certresolver: "${TRAEFIK_CERTRESOLVER}" - - # Router HTTPS - dominio secundario (sin redirección) - traefik.http.routers.trilium-alt.rule: "Host(`${TRILIUM_DOMAIN_2}`)" - traefik.http.routers.trilium-alt.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}" - traefik.http.routers.trilium-alt.tls: "true" - traefik.http.routers.trilium-alt.tls.certresolver: "${TRAEFIK_CERTRESOLVER}" - traefik.http.routers.trilium-alt.service: "trilium@docker" - - # Servicio interno traefik.http.services.trilium.loadbalancer.server.port: "${TRILIUM_HTTP_PORT}" - - # Middleware solo de headers (sin Authentik) - traefik.http.routers.trilium.middlewares: "trilium-sec@docker" - + traefik.http.middlewares.trilium-sec.headers.stsSeconds: "31536000" traefik.http.middlewares.trilium-sec.headers.stsIncludeSubdomains: "true" traefik.http.middlewares.trilium-sec.headers.stsPreload: "true" @@ -49,4 +30,3 @@ services: networks: proxy: external: true - diff --git a/wireguard/docker-compose.yml b/wireguard/docker-compose.yml index 921c143..775192e 100644 --- a/wireguard/docker-compose.yml +++ b/wireguard/docker-compose.yml @@ -2,6 +2,7 @@ services: wg-easy: image: ${WG_EASY_IMAGE} container_name: wg-easy + pull_policy: always restart: unless-stopped cap_add: @@ -37,21 +38,8 @@ services: - proxy labels: - traefik.enable: "true" - traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}" - - # Router HTTPS para la UI de wg-easy - traefik.http.routers.wg.rule: "Host(`${WG_DOMAIN}`)" - traefik.http.routers.wg.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}" - traefik.http.routers.wg.tls.certresolver: "${TRAEFIK_CERTRESOLVER}" - - # Servicio apuntando al puerto HTTP interno de la UI traefik.http.services.wg.loadbalancer.server.port: "${WG_UI_PORT}" - # Proteger la UI con Authentik (middleware definido en authentik-server) - traefik.http.routers.wg.middlewares: "${TRAEFIK_AUTH_MIDDLEWARE}" - networks: proxy: external: true -