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.<name>.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>
This commit is contained in:
root
2026-03-23 01:51:55 +00:00
parent 95f93094da
commit 9e82928049
11 changed files with 198 additions and 150 deletions

171
COOLIFY-TEMPLATE.md Normal file
View File

@@ -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.<your-router-name>.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-<uuid>-<servicename>
# 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -2,6 +2,7 @@ services:
mail-relay:
image: ${MAIL_RELAY_IMAGE}
container_name: mail-relay
pull_policy: always
restart: unless-stopped
environment:
TZ: ${TZ}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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