42 Commits

Author SHA1 Message Date
Eduardo David Paredes Vara
8b1525f2c9 wireguard torrent 2026-04-05 23:54:10 +00:00
root
7eb63a921c feat: add vikunja stack (Coolify-ready)
- pull_policy: always on vikunja service
- hardcoded bind mounts: /opt/vikunja/{postgres,files}
- port hardcoded in label: 3456 (Gotcha 6)
- removed Portainer Traefik router labels
- vikunja_internal: driver bridge (no fixed name)
- proxy: external: true

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:57:15 +00:00
root
8373f1ddfb feat: add memos stack (Coolify-ready)
- pull_policy: always on memos service
- hardcoded bind mounts: /opt/memos/{postgres,data}
- port hardcoded in label: 5230 (Gotcha 6)
- removed Portainer Traefik router labels (Coolify manages routing)
- removed traefik.enable / traefik.docker.network
- memos_internal: driver bridge (not fixed name)
- proxy: external: true

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:52:18 +00:00
root
fd332455c1 fix: hardcode Traefik port labels (Gotcha 6 - vars not expanded in labels)
Coolify does not expand ${VAR} in label values. All loadbalancer port
labels must use hardcoded values:
- trilium:   ${TRILIUM_HTTP_PORT}  → 8080
- adguard:   ${ADGUARD_HTTP_PORT}  → 80
- authentik: ${AUTHENTIK_HTTP_PORT} → 9000
- gitea:     ${GITEA_HTTP_PORT}    → 3000
- wireguard: ${WG_UI_PORT}         → 51821

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:44:58 +00:00
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
root
7896614cfd docs: add gotcha 5 - all host data lives under /opt/<stack>/
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:10:59 +00:00
root
38440fe0f0 fix: adguard and authentik use /opt bind mounts (original Portainer paths)
Data was in /opt/adguard/{work,conf} and /opt/authentik/{postgres,redis}
all along. Previous commit used named volumes which would ignore this data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:10:39 +00:00
root
5617b29fcf fix: replace variable bind mounts with hardcoded paths/named volumes
Coolify converts ${VAR}:/path bind mounts to named Docker volumes when
the variable is not resolved. Fixed per stack:

- adguard: -> named volumes (data already in Coolify-created volumes)
- authentik: -> named volumes (data already in Coolify-created volumes)
- gitea: -> /opt/gitea/{postgres,data,runner}
- mail-relay: -> /opt/mail-relay/{queue,opendkim,secrets/...}
- media-server: COMMON_PATH -> /opt/media (hardcoded)
- trilium: -> /opt/trilium/data
- wireguard: -> /opt/wg-easy + /lib/modules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:09:08 +00:00
root
9f7bcb3ecc docs: add operational gotchas to Coolify template
- Gotcha 1: dynamic config dir is /opt/traefik/dynamic, NOT /data/coolify/proxy/dynamic
- Gotcha 2: entrypoints are http/https (not web/websecure); coolify.yaml fix location
- Gotcha 3: env vars in bind mount paths become named volumes in Coolify
- Gotcha 4: SSH MaxSessions must be >= 10 for parallel deploy sessions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 02:02:16 +00:00
root
93ea33c045 fix: n8n use hardcoded bind mounts to preserve Portainer data
- n8n app data: /opt/n8n/data → /home/node/.n8n
- postgres data: /opt/n8n/postgres → /var/lib/postgresql/data
  (was using ${N8N_DB_DATA_PATH} which Coolify converts to a named volume)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 01:54:07 +00:00
root
9e82928049 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>
2026-03-23 01:51:55 +00:00
root
95f93094da fix: add proxy network to n8n so Coolify Traefik can reach it 2026-03-23 01:42:58 +00:00
root
baa0c6b769 refactor: replace dockerfile_inline with pull_policy: always 2026-03-23 01:38:02 +00:00
root
e6fda25c3d fix: add Traefik port label 5678 to n8n service 2026-03-23 01:36:34 +00:00
root
1dbf673051 fix: n8n use SERVICE_FQDN_N8N for N8N_HOST and WEBHOOK_URL 2026-03-23 01:27:47 +00:00
root
f7a4c1134a fix: adapt n8n compose for Coolify (remove Traefik/proxy/mail_internal, add dockerfile_inline) 2026-03-23 01:16:09 +00:00
root
f36e537336 fix: adapt gitea and karakeep compose for Coolify (remove Traefik, add dockerfile_inline) 2026-03-23 00:58:01 +00:00
Eduardo David Paredes Vara
faffd54ff1 karakeep 2026-03-23 00:55:48 +00:00
Eduardo David Paredes Vara
797fa7aa4a karakeep 2026-03-23 00:49:02 +00:00
Eduardo David Paredes Vara
833b80accf coolify 2026-03-23 00:46:03 +00:00
Eduardo David Paredes Vara
a17b589803 coolify gitea 2026-03-23 00:34:56 +00:00
Eduardo David Paredes Vara
81c7b45069 mail server n8n 2026-03-20 23:39:52 +00:00
Eduardo David Paredes Vara
d484dd5e5f mail server paperless 2026-03-20 23:30:44 +00:00
Eduardo David Paredes Vara
8bd6b3b9f2 nextcloud mail 2026-03-20 23:07:26 +00:00
Eduardo David Paredes Vara
24270af2f0 Mail 2026-03-20 22:35:42 +00:00
Eduardo David Paredes Vara
1f7ed5071d stacks env 2026-03-17 15:30:01 +00:00
Eduardo David Paredes Vara
59cc0c0bab certificados 2026-03-17 00:00:33 +00:00
Eduardo David Paredes Vara
411ebf968d certificados 2026-03-16 23:55:46 +00:00
Eduardo David Paredes Vara
8ff1456a3b certificados 2026-03-16 23:47:33 +00:00
Eduardo David Paredes Vara
1698de3738 delete old ruleta bot 2026-02-17 10:49:01 +00:00
Eduardo David Paredes Vara
f22842052a portainer wrn fix 2026-02-17 09:16:59 +00:00
Eduardo David Paredes Vara
f29208cfa2 middelware fix 2026-02-17 08:57:58 +00:00
Eduardo David Paredes Vara
f8cd4c2df1 ak update 2026-02-17 08:36:30 +00:00
Eduardo David Paredes Vara
db807dcf6f authentik update 2026-02-15 16:40:17 +00:00
Eduardo David Paredes Vara
02b9685804 authentik update 2026-02-15 16:38:12 +00:00
8246bff8a1 Refactor Traefik configuration for Authentik
Updated Traefik router rules and added new callback for www.thehomelesssherlock.com.
2026-02-15 16:27:18 +00:00
Eduardo David Paredes Vara
43c24b4b86 media server 2026-02-15 16:27:18 +00:00
Eduardo David Paredes Vara
43d10ea7cf media server 2026-02-15 16:27:18 +00:00
Eduardo David Paredes Vara
becce96ede media-server 2026-02-15 16:27:18 +00:00
Eduardo David Paredes Vara
a97c88454f Update docker-compose.yml 2025-12-22 16:27:54 +01:00
Eduardo David Paredes Vara
d05d783244 Update Traefik router rule for authentik outpost 2025-12-18 11:27:18 +01:00
Eduardo David Paredes Vara
35bb14028f Refactor Traefik configuration for Authentik
Updated Traefik router rules and added new callback for www.thehomelesssherlock.com.
2025-12-18 10:50:07 +01:00
38 changed files with 2040 additions and 806 deletions

2
.env
View File

@@ -19,7 +19,7 @@
# TRAEFIK_CERTRESOLVER=letsencrypt
# Middleware de autenticación (SSO, etc.)
# TRAEFIK_AUTH_MIDDLEWARE=authentik@docker
# TRAEFIK_AUTH_MIDDLEWARE=ths-authentik@docker
# Dominios de ejemplo (cámbialos por los tuyos)
# PORTAINER_DOMAIN=portainer.example.com

255
COOLIFY-TEMPLATE.md Normal file
View File

@@ -0,0 +1,255 @@
# 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
---
## 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:
```bash
/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
```yaml
# ❌ 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.
```bash
grep MaxSessions /etc/ssh/sshd_config # debe ser >= 10
```
### Gotcha 5 — Todos los datos del host están en /opt/<stack>/
Convención de este servidor: todos los bind mounts van bajo `/opt/<nombre-stack>/`.
Antes de hardcodear rutas en un compose, verificar siempre:
```bash
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:`.
```yaml
# ❌ 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.

View File

@@ -165,7 +165,7 @@ Variables principales a configurar:
- `PORTAINER_DOMAIN`: Tu dominio para Portainer UI (ej: `portainer.tudominio.com`)
- `PORTAINER_API_DOMAIN`: Tu dominio para la API de Portainer (ej: `portainer-api.tudominio.com`)
- `PORTAINER_API_IP_WHITELIST`: IPs permitidas para acceso directo a la API
- `TRAEFIK_AUTH_MIDDLEWARE`: Middleware de autenticación (ej: `authentik@docker`)
- `TRAEFIK_AUTH_MIDDLEWARE`: Middleware de autenticación (ej: `ths-authentik@docker`)
### Paso 10: Actualizar Stack de Portainer (Opcional)
@@ -250,7 +250,7 @@ PORTAINER_API_DOMAIN=portainer-api.example.com
# Seguridad
PORTAINER_API_IP_WHITELIST=10.8.0.0/24,172.18.0.1/32
TRAEFIK_AUTH_MIDDLEWARE=authentik@docker
TRAEFIK_AUTH_MIDDLEWARE=ths-authentik@docker
```
### Configuraciones por Stack

View File

@@ -121,7 +121,7 @@ labels:
traefik.http.routers.dashboard.entrypoints: "websecure"
traefik.http.routers.dashboard.tls.certresolver: "letsencrypt"
traefik.http.routers.dashboard.service: "api@internal"
traefik.http.routers.dashboard.middlewares: "authentik@docker"
traefik.http.routers.dashboard.middlewares: "ths-authentik@docker"
```
**Opción 2: Acceso local (inseguro - solo desarrollo)**
@@ -165,7 +165,7 @@ services:
traefik.http.services.mi-servicio.loadbalancer.server.port: "80"
# Middleware (opcional)
traefik.http.routers.mi-servicio.middlewares: "authentik@docker"
traefik.http.routers.mi-servicio.middlewares: "ths-authentik@docker"
networks:
proxy:
@@ -180,7 +180,7 @@ labels:
traefik.http.routers.app-ui.rule: "Host(`app.tudominio.com`)"
traefik.http.routers.app-ui.entrypoints: "websecure"
traefik.http.routers.app-ui.tls.certresolver: "letsencrypt"
traefik.http.routers.app-ui.middlewares: "authentik@docker"
traefik.http.routers.app-ui.middlewares: "ths-authentik@docker"
traefik.http.routers.app-ui.priority: "10"
# API pública sin protección

View File

@@ -89,7 +89,7 @@ ADGUARD_CERT_KEY_PATH=/opt/adguard/certs/adguard.key
TRAEFIK_DOCKER_NETWORK=proxy
TRAEFIK_ENTRYPOINT_SECURE=websecure
TRAEFIK_CERTRESOLVER=letsencrypt
TRAEFIK_AUTH_MIDDLEWARE=authentik@docker
TRAEFIK_AUTH_MIDDLEWARE=ths-authentik@docker
```
## ⚙️ Configuración Post-Instalación

View File

@@ -2,13 +2,14 @@ services:
adguardhome:
image: ${ADGUARD_IMAGE}
container_name: adguardhome
pull_policy: always
restart: unless-stopped
volumes:
- ${ADGUARD_WORK_PATH}:/opt/adguardhome/work:Z
- ${ADGUARD_CONF_PATH}:/opt/adguardhome/conf:Z
- ${ADGUARD_CERT_CRT_PATH}:/certs/adguard.crt:ro,Z
- ${ADGUARD_CERT_KEY_PATH}:/certs/adguard.key:ro,Z
- /opt/adguard/work:/opt/adguardhome/work:Z
- /opt/adguard/conf:/opt/adguardhome/conf:Z
- adguard-cert-crt-path:/certs/adguard.crt:ro,Z
- adguard-cert-key-path:/certs/adguard.key:ro,Z
# Solo DNS/DoT expuestos en el host
ports:
@@ -22,22 +23,12 @@ services:
ipv4_address: ${ADGUARD_IPV4}
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
traefik.http.services.adguard.loadbalancer.server.port: "80"
# 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}"
volumes:
adguard-cert-crt-path:
adguard-cert-key-path:
networks:
proxy:
external: true

View File

@@ -6,7 +6,7 @@ ADGUARD_CERT_CRT_PATH=
ADGUARD_CERT_KEY_PATH=
ADGUARD_DOT_PORT=
ADGUARD_IPV4=
ADGUARD_HTTP_PORT=
ADGUARD_HTTP_PORT=80
##### Traefik / red #####
TRAEFIK_DOCKER_NETWORK=

View File

@@ -203,7 +203,7 @@ Una vez configurado el middleware, añade la label a los servicios que quieras p
```yaml
labels:
traefik.http.routers.portainer.middlewares: "authentik@docker"
traefik.http.routers.portainer.middlewares: "ths-authentik@docker"
```
O si definiste el middleware en archivo:

View File

@@ -1,104 +1,93 @@
services:
authentik-postgres:
ths-authentik-postgres:
image: ${AUTHENTIK_POSTGRES_IMAGE}
container_name: authentik-postgres
container_name: ths-authentik-postgres
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD}
POSTGRES_USER: ${AUTHENTIK_DB_USER}
POSTGRES_DB: ${AUTHENTIK_DB_NAME}
volumes:
- ${AUTHENTIK_POSTGRES_PATH}:/var/lib/postgresql/data:Z
- /opt/authentik/postgres:/var/lib/postgresql/data:Z
networks:
- authentik_internal
- ths_authentik_internal
authentik-redis:
ths-authentik-redis:
image: ${AUTHENTIK_REDIS_IMAGE}
container_name: authentik-redis
container_name: ths-authentik-redis
restart: unless-stopped
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
volumes:
- ${AUTHENTIK_REDIS_PATH}:/data:Z
- /opt/authentik/redis:/data:Z
networks:
- authentik_internal
- ths_authentik_internal
authentik-server:
ths-authentik-server:
image: ${AUTHENTIK_IMAGE}
container_name: authentik-server
container_name: ths-authentik-server
pull_policy: always
restart: unless-stopped
command: ["server"]
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_POSTGRESQL__HOST: ${AUTHENTIK_DB_HOST}
# OJO: forzamos hosts internos para evitar colisiones y depender del .env
AUTHENTIK_POSTGRESQL__HOST: ths-authentik-postgres
AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER}
AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME}
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_REDIS__HOST: ${AUTHENTIK_REDIS_HOST}
AUTHENTIK_REDIS__HOST: ths-authentik-redis
# Bootstrap inicial (primera vez)
AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL}
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}
AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN}
depends_on:
- authentik-postgres
- authentik-redis
- ths-authentik-postgres
- ths-authentik-redis
expose:
- "${AUTHENTIK_HTTP_PORT}"
- "9000"
networks:
- authentik_internal
- ths_authentik_internal
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
# Service Authentik (panel + endpoints)
traefik.http.services.ths-authentik.loadbalancer.server.port: "9000"
# Router del panel de Authentik
traefik.http.routers.authentik.rule: "Host(`${AUTHENTIK_DOMAIN}`)"
traefik.http.routers.authentik.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.authentik.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.services.authentik.loadbalancer.server.port: "${AUTHENTIK_HTTP_PORT}"
# 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"
# Middleware de forwardAuth que usaremos en Portainer, Pi-hole, etc.
traefik.http.middlewares.authentik.forwardauth.address: "http://authentik-server:${AUTHENTIK_HTTP_PORT}/outpost.goauthentik.io/auth/traefik"
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: "true"
traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: "X-Authentik-Username,X-Authentik-Groups,X-Authentik-Email,X-Authentik-Uid,X-Authentik-Jwt"
# Callback del outpost en gitea hacia Authentik
traefik.http.routers.authentik-outpost-gitea.rule: "Host(`${GITEA_DOMAIN}`) && PathPrefix(`/outpost.goauthentik.io/`)"
traefik.http.routers.authentik-outpost-gitea.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.authentik-outpost-gitea.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.authentik-outpost-gitea.service: "authentik"
traefik.http.routers.authentik-outpost-gitea.priority: "50"
authentik-worker:
ths-authentik-worker:
image: ${AUTHENTIK_IMAGE}
container_name: authentik-worker
container_name: ths-authentik-worker
restart: unless-stopped
command: ["worker"]
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_POSTGRESQL__HOST: ${AUTHENTIK_DB_HOST}
# OJO: forzamos hosts internos igual que en server
AUTHENTIK_POSTGRESQL__HOST: ths-authentik-postgres
AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER}
AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME}
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
AUTHENTIK_REDIS__HOST: ${AUTHENTIK_REDIS_HOST}
AUTHENTIK_REDIS__HOST: ths-authentik-redis
depends_on:
- authentik-postgres
- authentik-redis
- ths-authentik-postgres
- ths-authentik-redis
networks:
- authentik_internal
- ths_authentik_internal
networks:
proxy:
external: true
authentik_internal:
ths_authentik_internal:
driver: bridge

View File

@@ -19,7 +19,7 @@ AUTHENTIK_SECRET_KEY=
AUTHENTIK_BOOTSTRAP_EMAIL=
AUTHENTIK_BOOTSTRAP_PASSWORD=
AUTHENTIK_BOOTSTRAP_TOKEN=
AUTHENTIK_HTTP_PORT=
AUTHENTIK_HTTP_PORT=9000
##### Traefik / dominios #####
TRAEFIK_DOCKER_NETWORK=

View File

@@ -1,24 +1,15 @@
services:
portainer:
image: ${PORTAINER_IMAGE:-portainer/portainer-ce:latest}
image: portainer/portainer-ee:2.33.7
container_name: portainer
restart: unless-stopped
env_file:
- .env
volumes:
# Clave de cifrado: misma clave montada en las dos rutas
- ${PORTAINER_SECRET_PATH:-/opt/portainer/secrets/portainer}:/run/secrets/portainer:ro,Z
- ${PORTAINER_SECRET_PATH:-/opt/portainer/secrets/portainer}:/run/portainer/portainer:ro,Z
# Socket de Docker (NO usar :Z aquí)
- /opt/portainer/secrets/portainer:/run/secrets/portainer:ro,Z
- /opt/portainer/secrets/portainer:/run/portainer/portainer:ro,Z
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/portainer/data:/data:Z
# Datos de Portainer (DB cifrada incluida)
- ${PORTAINER_DATA_PATH:-/opt/portainer/data}:/data:Z
# SELinux: evita bloqueos con docker.sock
security_opt:
- label=disable
@@ -26,28 +17,24 @@ services:
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK:-proxy}"
- "traefik.enable=true"
- "traefik.docker.network=proxy"
############################
# 1) UI protegida (ej: SSO)
############################
traefik.http.routers.portainer.rule: "Host(`${PORTAINER_DOMAIN:-portainer.example.com}`)"
traefik.http.routers.portainer.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE:-websecure}"
traefik.http.routers.portainer.tls.certresolver: "${TRAEFIK_CERTRESOLVER:-letsencrypt}"
traefik.http.routers.portainer.middlewares: "${TRAEFIK_AUTH_MIDDLEWARE:-authentik@docker}"
traefik.http.services.portainer.loadbalancer.server.port: "${PORTAINER_HTTP_PORT:-9000}"
# 1) UI protegida Authentik
- "traefik.http.routers.portainer.rule=Host(`portainer.thehomelesssherlock.com`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.routers.portainer.middlewares=ths-authentik@docker"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
#########################################################
# 2) API/App móvil SIN SSO, restringida por IP (ej: VPN)
#########################################################
traefik.http.middlewares.portainer-api-ip.ipwhitelist.sourcerange: "${PORTAINER_API_IP_WHITELIST:-10.8.0.0/24,172.18.0.1/32}"
traefik.http.routers.portainer-direct.rule: "Host(`${PORTAINER_API_DOMAIN:-portainer-api.example.com}`)"
traefik.http.routers.portainer-direct.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE:-websecure}"
traefik.http.routers.portainer-direct.tls.certresolver: "${TRAEFIK_CERTRESOLVER:-letsencrypt}"
traefik.http.routers.portainer-direct.middlewares: "portainer-api-ip"
traefik.http.routers.portainer-direct.service: "portainer"
traefik.http.routers.portainer-direct.priority: "100"
# 2) API/App móvil SIN Authentik, SOLO por VPN (WireGuard)
- "traefik.http.middlewares.portainer-api-ip.ipallowlist.sourcerange=10.8.0.0/24,172.18.0.1/32"
- "traefik.http.routers.portainer-direct.rule=Host(`portainer-api.thehomelesssherlock.com`)"
- "traefik.http.routers.portainer-direct.entrypoints=websecure"
- "traefik.http.routers.portainer-direct.tls.certresolver=letsencrypt"
- "traefik.http.routers.portainer-direct.middlewares=portainer-api-ip"
- "traefik.http.routers.portainer-direct.service=portainer"
- "traefik.http.routers.portainer-direct.priority=100"
networks:
proxy:

View File

@@ -9,12 +9,13 @@ services:
POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD}
TZ: ${TZ}
volumes:
- ${GITEA_POSTGRES_PATH}:/var/lib/postgresql/data:Z
- /opt/gitea/postgres:/var/lib/postgresql/data:Z
networks:
- gitea
gitea:
image: ${GITEA_IMAGE}
pull_policy: always
container_name: gitea
restart: unless-stopped
depends_on:
@@ -61,45 +62,14 @@ services:
GITEA__ui__THEMES: ${GITEA_UI_THEMES}
volumes:
- ${GITEA_DATA_PATH}:/data:Z
- /opt/gitea/data:/data:Z
networks:
- gitea
- proxy
# Exponer SSH (contenedor y host mismo puerto)
ports:
- "${GITEA_SSH_PORT}:${GITEA_SSH_PORT}"
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
traefik.http.services.gitea.loadbalancer.server.port: "${GITEA_HTTP_PORT}"
# Router principal (sin Authentik)
traefik.http.routers.gitea-main.rule: "Host(`${GITEA_DOMAIN}`)"
traefik.http.routers.gitea-main.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.gitea-main.tls: "true"
traefik.http.routers.gitea-main.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.gitea-main.priority: "10"
# Router login + explore + perfil TheHomelessSherlock (con Authentik)
traefik.http.routers.gitea-login.rule: >-
Host(`${GITEA_DOMAIN}`) &&
(Path(`/user/login`) ||
PathPrefix(`/user/sign_up`) ||
PathPrefix(`/user/forgot_password`) ||
PathPrefix(`/user/two_factor`) ||
PathPrefix(`/login/oauth`) ||
PathPrefix(`/explore`) ||
PathPrefix(`/api`) ||
PathPrefix(`/api/swagger`) ||
PathRegexp(`^/TheHomelessSherlock/?$`))
traefik.http.routers.gitea-login.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.gitea-login.tls: "true"
traefik.http.routers.gitea-login.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.gitea-login.middlewares: "${TRAEFIK_AUTH_MIDDLEWARE}"
traefik.http.routers.gitea-login.priority: "20"
traefik.http.services.gitea.loadbalancer.server.port: "3000"
gitea-runner:
image: ${GITEA_RUNNER_IMAGE}
@@ -113,7 +83,7 @@ services:
GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME}
GITEA_RUNNER_LABELS: ${GITEA_RUNNER_LABELS}
volumes:
- ${GITEA_RUNNER_DATA_PATH}:/data:Z
- /opt/gitea/runner:/data:Z
- /var/run/docker.sock:/var/run/docker.sock:Z
networks:
- gitea
@@ -123,4 +93,3 @@ networks:
driver: bridge
proxy:
external: true

View File

@@ -1,56 +1,49 @@
##### Postgres Gitea #####
GITEA_POSTGRES_IMAGE=
GITEA_DB_NAME=
GITEA_DB_USER=
GITEA_DB_PASSWORD=
TZ=
GITEA_POSTGRES_PATH=
GITEA_POSTGRES_IMAGE=postgres:16
GITEA_DB_NAME=gitea
GITEA_DB_USER=gitea
GITEA_DB_PASSWORD=pon_una_pass_fuerte_aqui
TZ=Europe/Madrid
GITEA_POSTGRES_PATH=/opt/gitea/postgres
##### Gitea #####
GITEA_IMAGE=
GITEA_USER_UID=
GITEA_USER_GID=
GITEA_IMAGE=gitea/gitea:latest
GITEA_USER_UID=1000
GITEA_USER_GID=1000
GITEA_DB_TYPE=
GITEA_DB_HOST=
GITEA_DB_PORT=
GITEA_DB_TYPE=postgres
GITEA_DB_HOST=postgres
GITEA_DB_PORT=5432
GITEA_DOMAIN=
GITEA_ROOT_URL=
GITEA_SERVER_PROTOCOL=
GITEA_HTTP_PORT=
GITEA_DOMAIN=gitea.thehomelesssherlock.com
GITEA_ROOT_URL=https://gitea.thehomelesssherlock.com/
GITEA_SERVER_PROTOCOL=http
GITEA_HTTP_PORT=3000
GITEA_SSH_DOMAIN=
GITEA_SSH_PORT=
GITEA_START_SSH_SERVER=
GITEA_SSH_DOMAIN=gitea.thehomelesssherlock.com
GITEA_SSH_PORT=1516
GITEA_START_SSH_SERVER=true
GITEA_ACTIONS_ENABLED=
GITEA_DISABLE_REGISTRATION=
GITEA_REQUIRE_SIGNIN_VIEW=
GITEA_ENABLE_OPENID_SIGNUP=
GITEA_ENABLE_OPENID_SIGNIN=
GITEA_DISABLE_LOGIN_FORM=
GITEA_HIDE_EMAIL_ADDRESS=
GITEA_DEFAULT_ALLOW_CREATE_ORGANIZATION=
GITEA_DEFAULT_ORG_VISIBILITY=
GITEA_DEFAULT_VISIBILITY=
GITEA_ACTIONS_ENABLED=true
GITEA_DISABLE_REGISTRATION=true
GITEA_REQUIRE_SIGNIN_VIEW=false
GITEA_ENABLE_OPENID_SIGNUP=false
GITEA_ENABLE_OPENID_SIGNIN=false
GITEA_DISABLE_LOGIN_FORM=false
GITEA_HIDE_EMAIL_ADDRESS=true
GITEA_DEFAULT_ALLOW_CREATE_ORGANIZATION=false
GITEA_DEFAULT_ORG_VISIBILITY=private
GITEA_DEFAULT_VISIBILITY=private
GITEA_DEFAULT_THEME=
GITEA_UI_THEMES=
GITEA_DEFAULT_THEME=gitea-dark
GITEA_UI_THEMES=gitea-dark
GITEA_DATA_PATH=
##### Traefik #####
TRAEFIK_DOCKER_NETWORK=
TRAEFIK_ENTRYPOINT_SECURE=
TRAEFIK_CERTRESOLVER=
TRAEFIK_AUTH_MIDDLEWARE=
GITEA_DATA_PATH=/opt/gitea/data
##### Runner #####
GITEA_RUNNER_IMAGE=
GITEA_INSTANCE_URL=
GITEA_RUNNER_REGISTRATION_TOKEN=
GITEA_RUNNER_NAME=
GITEA_RUNNER_IMAGE=docker.io/gitea/act_runner:latest
GITEA_INSTANCE_URL=http://gitea:3000
GITEA_RUNNER_REGISTRATION_TOKEN=LRex9dPrXdrEeh1tIPiaRXndu4L1o7Co9j9PSjis
GITEA_RUNNER_NAME=almasrv-runner-1
GITEA_RUNNER_LABELS=
GITEA_RUNNER_DATA_PATH=
GITEA_RUNNER_DATA_PATH=/opt/gitea/runner

View File

@@ -0,0 +1,77 @@
services:
karakeep:
image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION}
pull_policy: always
container_name: karakeep
restart: unless-stopped
environment:
DATA_DIR: /data
MEILI_ADDR: http://karakeep-meilisearch:7700
BROWSER_WEB_URL: http://karakeep-chrome:9222
NEXTAUTH_URL: https://${KARAKEEP_DOMAIN}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
DISABLE_SIGNUPS: ${DISABLE_SIGNUPS}
OCR_LANGS: ${OCR_LANGS}
OPENAI_API_KEY: ${OPENAI_API_KEY}
INFERENCE_TEXT_MODEL: ${INFERENCE_TEXT_MODEL}
INFERENCE_IMAGE_MODEL: ${INFERENCE_IMAGE_MODEL}
EMBEDDING_TEXT_MODEL: ${EMBEDDING_TEXT_MODEL}
INFERENCE_LANG: ${INFERENCE_LANG}
INFERENCE_ENABLE_AUTO_TAGGING: ${INFERENCE_ENABLE_AUTO_TAGGING}
INFERENCE_ENABLE_AUTO_SUMMARIZATION: ${INFERENCE_ENABLE_AUTO_SUMMARIZATION}
INFERENCE_CONTEXT_LENGTH: ${INFERENCE_CONTEXT_LENGTH}
INFERENCE_MAX_OUTPUT_TOKENS: ${INFERENCE_MAX_OUTPUT_TOKENS}
INFERENCE_USE_MAX_COMPLETION_TOKENS: ${INFERENCE_USE_MAX_COMPLETION_TOKENS}
INFERENCE_NUM_WORKERS: ${INFERENCE_NUM_WORKERS}
INFERENCE_JOB_TIMEOUT_SEC: ${INFERENCE_JOB_TIMEOUT_SEC}
LOG_LEVEL: ${LOG_LEVEL}
DB_WAL_MODE: ${DB_WAL_MODE}
volumes:
- /opt/karakeep/data:/data:Z
depends_on:
- karakeep-meilisearch
- 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
container_name: karakeep-chrome
restart: unless-stopped
command:
- --no-sandbox
- --disable-gpu
- --disable-dev-shm-usage
- --remote-debugging-address=0.0.0.0
- --remote-debugging-port=9222
- --hide-scrollbars
networks:
- karakeep_internal
karakeep-meilisearch:
image: getmeili/meilisearch:v1.37.0
container_name: karakeep-meilisearch
restart: unless-stopped
environment:
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
MEILI_NO_ANALYTICS: "true"
volumes:
- /opt/karakeep/meili_data:/meili_data:Z
networks:
- karakeep_internal
networks:
karakeep_internal:
driver: bridge
proxy:
external: true

137
mail-relay/README.md Normal file
View File

@@ -0,0 +1,137 @@
# Mail Relay - SMTP de salida para stacks
Stack de relay SMTP interno para centralizar el envio de correos de tus apps sin montar un servidor de correo completo.
## 📋 Descripcion
Este stack despliega un relay SMTP con Postfix usando `boky/postfix` para:
- Recibir correo SMTP desde contenedores internos
- Reenviar todo a un proveedor SMTP externo (smarthost)
- Firmar con DKIM (autogenerado)
- Unificar configuracion de correo para todos los stacks
No incluye recepcion de correo (MX, IMAP, POP3, webmail).
## 🚀 Despliegue
### 1. Preparar rutas y secreto en el host
```bash
sudo mkdir -p /opt/mail-relay/{queue,opendkim,secrets}
sudo chmod 700 /opt/mail-relay/secrets
printf '%s' 'CAMBIA_ESTA_PASSWORD_SMTP' | sudo tee /opt/mail-relay/secrets/relayhost_password > /dev/null
sudo chmod 600 /opt/mail-relay/secrets/relayhost_password
```
### 2. Desplegar desde Portainer
1. Ve a **Stacks** -> **Add stack**
2. Nombre: `mail-relay`
3. Configura el repositorio Git
4. Compose path: `mail-relay/docker-compose.yml`
5. Carga variables desde `mail-relay/stack.env`
6. Ajusta al menos:
- `MAIL_RELAY_HOSTNAME`
- `MAIL_RELAY_ALLOWED_SENDER_DOMAINS`
- `MAIL_RELAY_MASQUERADED_DOMAINS`
- `MAIL_RELAY_SMARTHOST`
- `MAIL_RELAY_SMARTHOST_USERNAME`
7. Deploy del stack
## ⚙️ Variables importantes
```env
MAIL_RELAY_SMARTHOST=[smtp.proveedor.tld]:587
MAIL_RELAY_SMARTHOST_USERNAME=usuario-smtp
MAIL_RELAY_SMTP_TLS_SECURITY_LEVEL=encrypt
MAIL_RELAY_ALLOWED_SENDER_DOMAINS=tudominio.com
MAIL_RELAY_MASQUERADED_DOMAINS=tudominio.com
```
La password SMTP no se pone en `stack.env`; se lee desde el archivo host:
```text
/opt/mail-relay/secrets/relayhost_password
```
## 🔌 Conectar otras apps
En cada stack que deba enviar correo, añade la red externa:
```yaml
networks:
mail_internal:
external: true
```
Y en el servicio de la app:
```yaml
services:
tu-app:
networks:
- default
- mail_internal
```
Config SMTP en la app:
```env
SMTP_HOST=mail-relay
SMTP_PORT=587
SMTP_FROM=noreply@tudominio.com
```
## ✅ DNS minimo recomendado
Para buena entregabilidad en Gmail/Outlook:
- SPF
- DKIM
- DMARC
### SPF (ejemplo)
```text
v=spf1 include:spf.tu-proveedor.tld ~all
```
### DKIM
Tras el primer arranque, extrae el TXT generado:
```bash
find /opt/mail-relay/opendkim -type f -name '*.txt' -exec echo '### {}' \; -exec cat {} \;
```
Copia esos valores a tu DNS.
### DMARC (inicio en monitorizacion)
```text
v=DMARC1; p=none; adkim=s; aspf=s
```
## 🧪 Prueba rapida
```bash
docker exec -i mail-relay sendmail -t <<'EOF'
From: noreply@tudominio.com
To: destino@example.com
Subject: prueba mail relay
Correo de prueba del stack mail-relay.
EOF
```
## 🛠️ Troubleshooting
Ver logs:
```bash
docker logs -f mail-relay
```
Errores tipicos:
- `Relay access denied`: revisa `MAIL_RELAY_ALLOWED_SENDER_DOMAINS`
- Auth fallida con proveedor: revisa usuario/password SMTP
- Rechazo por DNS: valida SPF/DKIM/DMARC

View File

@@ -0,0 +1,53 @@
services:
mail-relay:
image: ${MAIL_RELAY_IMAGE}
container_name: mail-relay
pull_policy: always
restart: unless-stopped
environment:
TZ: ${TZ}
LOG_FORMAT: ${MAIL_RELAY_LOG_FORMAT}
# Hostname del relay
POSTFIX_myhostname: ${MAIL_RELAY_HOSTNAME}
# Solo clientes internos del stack de correo
POSTFIX_mynetworks: ${MAIL_RELAY_MYNETWORKS}
# Dominios permitidos para el sender
ALLOWED_SENDER_DOMAINS: ${MAIL_RELAY_ALLOWED_SENDER_DOMAINS}
# Reescritura de dominio para hosts internos
MASQUERADED_DOMAINS: ${MAIL_RELAY_MASQUERADED_DOMAINS}
# Relay SMTP externo
RELAYHOST: ${MAIL_RELAY_SMARTHOST}
RELAYHOST_USERNAME: ${MAIL_RELAY_SMARTHOST_USERNAME}
RELAYHOST_PASSWORD_FILE: /run/secrets/relayhost_password
POSTFIX_smtp_tls_security_level: ${MAIL_RELAY_SMTP_TLS_SECURITY_LEVEL}
# DKIM
DKIM_AUTOGENERATE: ${MAIL_RELAY_DKIM_AUTOGENERATE}
DKIM_SELECTOR: ${MAIL_RELAY_DKIM_SELECTOR}
volumes:
- /opt/mail-relay/queue:/var/spool/postfix:Z
- /opt/mail-relay/opendkim:/etc/opendkim/keys:Z
- /opt/mail-relay/secrets/relayhost_password:/run/secrets/relayhost_password:ro,Z
networks:
mail_internal:
ipv4_address: ${MAIL_RELAY_IPV4}
# No publicar puertos al exterior para uso interno entre contenedores.
# Descomenta para pruebas desde el host:
# ports:
# - "127.0.0.1:1587:587"
networks:
mail_internal:
name: ${MAIL_RELAY_NETWORK_NAME}
driver: bridge
ipam:
config:
- subnet: ${MAIL_RELAY_SUBNET}

29
mail-relay/stack.env Normal file
View File

@@ -0,0 +1,29 @@
##### General #####
MAIL_RELAY_IMAGE=boky/postfix:v5.1.0
TZ=Europe/Madrid
MAIL_RELAY_LOG_FORMAT=plain
##### Network #####
MAIL_RELAY_NETWORK_NAME=mail_internal
MAIL_RELAY_SUBNET=10.77.0.0/24
MAIL_RELAY_IPV4=10.77.0.10
MAIL_RELAY_MYNETWORKS=127.0.0.0/8,10.77.0.0/24
##### Relay identity #####
MAIL_RELAY_HOSTNAME=mail.example.com
MAIL_RELAY_ALLOWED_SENDER_DOMAINS=example.com
MAIL_RELAY_MASQUERADED_DOMAINS=example.com
##### Upstream SMTP (smarthost) #####
MAIL_RELAY_SMARTHOST=[in-v3.mailjet.com]:587
MAIL_RELAY_SMARTHOST_USERNAME=tu-api-key-mailjet
MAIL_RELAY_SMTP_TLS_SECURITY_LEVEL=encrypt
##### DKIM #####
MAIL_RELAY_DKIM_AUTOGENERATE=true
MAIL_RELAY_DKIM_SELECTOR=mail
##### Host paths #####
MAIL_RELAY_QUEUE_PATH=/opt/mail-relay/queue
MAIL_RELAY_DKIM_KEYS_PATH=/opt/mail-relay/opendkim
MAIL_RELAY_PASSWORD_FILE_PATH=/opt/mail-relay/secrets/relayhost_password

593
media-server/README.md Normal file
View File

@@ -0,0 +1,593 @@
# Media Server - Stack Completo de Gestión de Medios
Stack completo para gestión y streaming de contenido multimedia con automatización de descargas.
## 🎯 Flujo de Trabajo
1. **Usuario solicita contenido** → Jellyseerr
2. **Jellyseerr envía solicitud** → Sonarr (series) o Radarr (películas)
3. **Sonarr/Radarr busca contenido** → Prowlarr/Jackett
4. **Prowlarr consulta trackers** → Indexers configurados
5. **Se descarga el contenido** → Cliente de descargas
6. **Sonarr/Radarr organiza archivos** → Directorios de medios
7. **Jellyfin detecta nuevo contenido** → Disponible para streaming
## 📋 Descripción
Este stack despliega un servidor multimedia completo con:
| Servicio | Descripción | Puerto | Propósito |
|----------|-------------|--------|-----------|
| **Jellyfin** | Servidor de streaming de medios | 8096 | Reproducir contenido multimedia |
| **Jellyseerr** | Sistema de solicitudes | 5055 | Permitir solicitudes de usuarios |
| **Sonarr** | Gestor de series TV | 8989 | Automatizar descargas de series |
| **Radarr** | Gestor de películas | 7878 | Automatizar descargas de películas |
| **Prowlarr** | Gestor de indexers | 9696 | Centralizar trackers |
| **Jackett** | Proxy de trackers | 9117 | Alternativa a Prowlarr |
| **FlareSolverr** | Resolver Cloudflare | 8191 | Bypasear protecciones |
**Características**:
- ✅ Automatización completa de descargas
- ✅ Gestión de calidad y formatos
- ✅ Sistema de solicitudes para usuarios
- ✅ Protección con SSO (Authentik)
- ✅ Certificados SSL automáticos (Let's Encrypt)
- ✅ Red aislada para servicios internos
## 🚀 Despliegue
### Prerequisitos
1. **Red Docker**: Asegúrate de que la red `proxy` existe
2. **Registros DNS**: Configura los registros A para tus dominios de servicios
3. **Almacenamiento**: Prepara los directorios para medios y configuraciones
### Desde Portainer
1. Ve a **Stacks****Add stack**
2. Nombre: `media-server`
3. Selecciona **Repository** o **Git repository**
4. Configura:
- Repository URL: `<tu-repositorio>`
- Repository reference: `main`
- Compose path: `media-server/docker-compose.yml`
5. Carga el archivo de variables de entorno: `media-server/stack.env`
6. Haz clic en **Deploy the stack**
### Variables de Entorno Importantes
Edita el archivo `stack.env`:
```env
# Global
TZ=Europe/Madrid
PUID=0
PGID=0
# Paths - Directorio base para configuraciones
COMMON_PATH=/opt/media-server
# Media mounts - Rutas del servidor donde están los archivos de medios
MEDIA_TV=/mnt/media/tv
MEDIA_MOVIES=/mnt/media/movies
MEDIA_DOWNLOADS=/mnt/media/downloads
# Networks
TRAEFIK_DOCKER_NETWORK=proxy
MEDIA_NETWORK_NAME=media
# Traefik & SSL
TRAEFIK_ENABLE=true
TRAEFIK_ENTRYPOINTS=websecure
TRAEFIK_TLS=true
TRAEFIK_CERTRESOLVER=letsencrypt
AUTH_MIDDLEWARE=ths-authentik@docker
# Dominios - Personaliza según tu dominio
DOMAIN=tudominio.com
PROWLARR_HOST=prowlarr.tudominio.com
JACKETT_HOST=jackett.tudominio.com
SONARR_HOST=sonarr.tudominio.com
RADARR_HOST=radarr.tudominio.com
JELLYSEERR_HOST=requests.tudominio.com
JELLYFIN_HOST=media.tudominio.com
```
## ⚙️ Configuración Post-Instalación
### 1. Preparar Directorios
Antes de desplegar, crea las estructuras de directorios necesarias:
```bash
# Directorio base de configuraciones
sudo mkdir -p /opt/media-server/configs/{prowlarr,jackett,sonarr,radarr,jellyseerr,jellyfin-vps}
sudo mkdir -p /opt/media-server/jellyfin/cache-vps
# Directorios de medios
sudo mkdir -p /mnt/media/{tv,movies,downloads}
# Ajustar permisos
sudo chown -R $USER:$USER /opt/media-server
sudo chown -R $USER:$USER /mnt/media
```
### 2. Configurar Prowlarr (Indexers)
**Prowlarr** es el gestor centralizado de indexers/trackers:
1. Accede a `https://prowlarr.tudominio.com`
2. Completa el asistente de configuración inicial
3. Ve a **Settings****Indexers****Add Indexer**
4. Añade tus trackers preferidos (públicos o privados)
5. Si algún tracker requiere FlareSolverr para Cloudflare:
- Ve a **Settings****Indexers****FlareSolverr**
- URL: `http://flaresolverr:8191`
### 3. Conectar Sonarr y Radarr con Prowlarr
En **Prowlarr**, añade las aplicaciones:
1. Ve a **Settings****Apps****Add Application**
2. Selecciona **Sonarr** o **Radarr**
3. Configura:
- **Prowlarr Server**: `http://prowlarr:9696`
- **Sonarr Server**: `http://sonarr:8989`
- **Radarr Server**: `http://radarr:7878`
- API Key: Cópiala desde Settings → General en cada aplicación
4. Haz clic en **Test** y luego **Save**
5. Sincroniza los indexers automáticamente
### 4. Configurar Clientes de Descarga
En **Sonarr** y **Radarr**, configura tu cliente de descargas (qBittorrent, Transmission, etc.):
1. Ve a **Settings****Download Clients****Add**
2. Selecciona tu cliente
3. Configura la conexión (host, puerto, usuario, contraseña)
4. Establece las categorías (tv para Sonarr, movies para Radarr)
### 5. Configurar Perfiles de Calidad
En **Sonarr** y **Radarr**:
1. Ve a **Settings****Profiles**
2. Configura los perfiles de calidad según tus preferencias
3. Ajusta el idioma preferido
4. Configura el formato preferido (1080p, 4K, etc.)
### 6. Configurar Jellyfin
**Jellyfin** es tu servidor de streaming:
1. Accede a `https://media.tudominio.com`
2. Completa el asistente de configuración:
- Crea el usuario administrador
- Configura las bibliotecas:
- **Series**: `/data/tvshows`
- **Películas**: `/data/movies`
3. Escanea las bibliotecas para detectar contenido
4. Configura metadatos (TMDb, TVDb, etc.)
### 7. Configurar Jellyseerr
**Jellyseerr** permite a los usuarios solicitar contenido:
1. Accede a `https://requests.tudominio.com`
2. Conecta con Jellyfin:
- URL: `http://jellyfin:8096`
- API Key: Desde Jellyfin Dashboard → API Keys
3. Conecta con Sonarr:
- URL: `http://sonarr:8989`
- API Key: Desde Sonarr Settings → General
4. Conecta con Radarr:
- URL: `http://radarr:7878`
- API Key: Desde Radarr Settings → General
5. Configura permisos de usuario
6. Activa notificaciones (opcional)
### 8. Integración con Authentik (SSO)
Todos los servicios están protegidos con Authentik por defecto mediante el middleware `ths-authentik@docker`.
Para personalizar el acceso:
1. En Authentik, crea aplicaciones para cada servicio
2. Configura grupos y permisos según tus necesidades
3. Los usuarios autenticados tendrán acceso automático
Si necesitas acceso público a Jellyfin:
```env
# En stack.env, para Jellyfin
AUTH_MIDDLEWARE= # Deja vacío para acceso público
```
## 🔧 Configuración Avanzada
### Personalizar Puertos de Servicios
Por defecto los servicios usan sus puertos estándar:
```env
PROWLARR_PORT=9696
JACKETT_PORT=9117
SONARR_PORT=8989
RADARR_PORT=7878
JELLYSEERR_PORT=5055
JELLYFIN_PORT=8096
```
### Personalizar Nombres de Contenedores
```env
PROWLARR_CONTAINER_NAME=prowlarr-prod
SONARR_CONTAINER_NAME=sonarr-prod
# etc...
```
### Usar Imágenes Específicas
```env
JELLYFIN_IMAGE=lscr.io/linuxserver/jellyfin:10.8.13
SONARR_IMAGE=lscr.io/linuxserver/sonarr:4.0
RADARR_IMAGE=lscr.io/linuxserver/radarr:5.2
```
### Configurar FlareSolverr para Trackers Protegidos
FlareSolverr ayuda a resolver protecciones de Cloudflare:
```env
# Recursos de memoria
FLARESOLVERR_SHM_SIZE=1gb
# Nivel de logs
LOG_LEVEL=info
LOG_HTML=false
# Solucionador de captchas (none, hcaptcha-solver)
CAPTCHA_SOLVER=none
```
### Configurar PUID/PGID Personalizado
Para evitar problemas de permisos, usa tu usuario del sistema:
```bash
id $USER
# uid=1000(usuario) gid=1000(usuario)
```
```env
PUID=1000
PGID=1000
```
### Notificaciones Personalizadas
Configura notificaciones en cada aplicación:
**Sonarr/Radarr**:
- Ve a **Settings****Connect****Add Notification**
- Opciones: Discord, Telegram, Email, Slack, etc.
**Jellyseerr**:
- Ve a **Settings****Notifications**
- Configura Discord, Email, Telegram, etc.
## 🛠️ Troubleshooting
### Sonarr/Radarr no encuentra series/películas
1. Verifica que Prowlarr está conectado correctamente
2. Comprueba que los indexers están funcionando en Prowlarr
3. Revisa los logs:
```bash
docker logs sonarr
docker logs radarr
```
### FlareSolverr no resuelve captchas
1. Verifica los logs:
```bash
docker logs flaresolverr
```
2. Aumenta la memoria compartida si es necesario:
```env
FLARESOLVERR_SHM_SIZE=2gb
```
3. Prueba la conectividad:
```bash
docker exec prowlarr curl http://flaresolverr:8191/health
```
### Error de permisos en directorios de medios
Si usas SELinux, los volúmenes ya tienen `:Z`:
```bash
# Si persisten problemas
sudo chcon -Rt svirt_sandbox_file_t /opt/media-server
sudo chcon -Rt svirt_sandbox_file_t /mnt/media
```
Si usas PUID/PGID personalizado:
```bash
# Ajustar permisos
sudo chown -R 1000:1000 /opt/media-server
sudo chown -R 1000:1000 /mnt/media
```
### Jellyfin no detecta el contenido
1. Verifica las rutas de las bibliotecas en Jellyfin:
- Series: `/data/tvshows`
- Películas: `/data/movies`
2. Escanea manualmente las bibliotecas:
- Dashboard → Libraries → Scan All Libraries
3. Verifica permisos de lectura:
```bash
docker exec jellyfin-vps ls -la /data/tvshows
docker exec jellyfin-vps ls -la /data/movies
```
### Jellyseerr no se conecta con Sonarr/Radarr
1. Verifica las API Keys en cada servicio
2. Usa las URLs internas del contenedor:
- Sonarr: `http://sonarr:8989`
- Radarr: `http://radarr:7878`
- Jellyfin: `http://jellyfin:8096`
3. Revisa los logs:
```bash
docker logs jellyseerr
```
### Los servicios no son accesibles vía web
1. Verifica que Traefik está corriendo
2. Comprueba las etiquetas de Traefik en los contenedores:
```bash
docker inspect prowlarr | grep traefik
```
3. Verifica los certificados SSL:
```bash
docker logs traefik | grep letsencrypt
```
4. Comprueba los registros DNS:
```bash
nslookup prowlarr.tudominio.com
```
## 📚 Recursos Adicionales
- [Documentación oficial de Jellyfin](https://jellyfin.org/docs/)
- [Wiki de Sonarr](https://wiki.servarr.com/sonarr)
- [Wiki de Radarr](https://wiki.servarr.com/radarr)
- [Documentación de Prowlarr](https://wiki.servarr.com/prowlarr)
- [Documentación de Jellyseerr](https://docs.jellyseerr.dev/)
- [FlareSolverr GitHub](https://github.com/FlareSolverr/FlareSolverr)
## 🔒 Seguridad
- **SSO con Authentik**: Todos los servicios protegidos por defecto
- **API Keys**: Protege las API keys de cada servicio
- **VPN**: Considera usar VPN para clientes de descarga
- **Trackers Privados**: Usa credenciales seguras para trackers privados
- **Certificados SSL**: Let's Encrypt mediante Traefik
- **Backups**: Realiza backups regulares de configuraciones y bases de datos
## 💾 Backups
### Backup de Configuraciones
```bash
# Backup de todas las configuraciones
sudo tar -czf media-server-configs-$(date +%Y%m%d).tar.gz /opt/media-server/configs
# Restaurar
sudo tar -xzf media-server-configs-YYYYMMDD.tar.gz -C /
```
### Backup de Bases de Datos Internas
Cada servicio *arr tiene su propia base de datos SQLite:
```bash
# Sonarr
docker exec sonarr cp /config/sonarr.db /config/sonarr.db.backup
# Radarr
docker exec radarr cp /config/radarr.db /config/radarr.db.backup
# Prowlarr
docker exec prowlarr cp /config/prowlarr.db /config/prowlarr.db.backup
```
### Backup Completo del Stack
```bash
# Script de backup completo
#!/bin/bash
BACKUP_DIR=/backups/media-server
DATE=$(date +%Y%m%d)
mkdir -p $BACKUP_DIR
# Configuraciones
sudo tar -czf $BACKUP_DIR/configs-$DATE.tar.gz /opt/media-server/configs
# Lista de contenido (opcional)
sudo tar -czf $BACKUP_DIR/media-structure-$DATE.tar.gz \
--exclude='*' \
--include='*/' \
/mnt/media
echo "Backup completado: $BACKUP_DIR"
```
## 🔄 Actualizaciones
1. **Backup primero**: Siempre haz backup antes de actualizar
2. Actualiza las versiones de imágenes en `stack.env`:
```env
JELLYFIN_IMAGE=lscr.io/linuxserver/jellyfin:10.9.0
SONARR_IMAGE=lscr.io/linuxserver/sonarr:latest
RADARR_IMAGE=lscr.io/linuxserver/radarr:latest
```
3. Actualiza el stack en Portainer o con Docker Compose:
```bash
docker compose pull
docker compose up -d
```
4. Verifica los logs de cada servicio
5. Comprueba que todo funciona correctamente
### Actualizaciones Recomendadas
- **Jellyfin**: Revisa changelog antes de actualizar versiones mayores
- **Sonarr/Radarr**: Usa `latest` para recibir actualizaciones automáticas
- **Prowlarr**: Mantén sincronizado con Sonarr/Radarr
- **FlareSolverr**: Actualiza cuando haya cambios en Cloudflare
## 📊 Monitoreo
### Ver Estadísticas de Uso
**Jellyfin**:
- Dashboard → Activity → Ver reproducciones activas
- Dashboard → Users → Estadísticas por usuario
**Sonarr/Radarr**:
- System → Status → Ver actividad de descargas
- Queue → Ver cola de descargas
**Prowlarr**:
- System → Status → Estadísticas de indexers
### Logs de Servicios
```bash
# Ver logs en tiempo real
docker logs -f prowlarr
docker logs -f jackett
docker logs -f sonarr
docker logs -f radarr
docker logs -f jellyseerr
docker logs -f jellyfin-vps
docker logs -f flaresolverr
# Ver últimas 100 líneas
docker logs --tail 100 sonarr
```
### Salud del Sistema
Cada servicio *arr tiene una página de salud:
- **Sonarr**: Settings → General → System Status
- **Radarr**: Settings → General → System Status
- **Prowlarr**: System → Status
### Monitoreo de Recursos
Usa `docker stats` para ver uso de recursos:
```bash
docker stats prowlarr sonarr radarr jellyfin-vps jellyseerr
```
### Notificaciones de Estado
Configura notificaciones en Sonarr/Radarr para:
- Descargas completadas
- Fallos en descargas
- Problemas con indexers
- Actualizaciones de aplicaciones
## 📝 Referencia Rápida
### URLs de Servicios (Por Defecto)
```
Prowlarr: https://prowlarr.tudominio.com
Jackett: https://jackett.tudominio.com
Sonarr: https://sonarr.tudominio.com
Radarr: https://radarr.tudominio.com
Jellyseerr: https://requests.tudominio.com
Jellyfin: https://media.tudominio.com
```
### URLs Internas (Entre Contenedores)
```
Prowlarr: http://prowlarr:9696
Jackett: http://jackett:9117
Sonarr: http://sonarr:8989
Radarr: http://radarr:7878
Jellyseerr: http://jellyseerr:5055
Jellyfin: http://jellyfin:8096
FlareSolverr: http://flaresolverr:8191
```
### Rutas de Montaje en Contenedores
```
Configuraciones: /config
Series TV: /tv (Sonarr), /data/tvshows (Jellyfin)
Películas: /movies (Radarr), /data/movies (Jellyfin)
Descargas: /downloads
```
### Comandos Útiles
```bash
# Ver todos los contenedores del stack
docker ps --filter "name=prowlarr|sonarr|radarr|jellyfin|jellyseerr|jackett|flaresolverr"
# Reiniciar un servicio específico
docker restart sonarr
# Ver uso de recursos
docker stats --no-stream prowlarr sonarr radarr jellyfin-vps
# Backup rápido de configuración
docker exec sonarr tar czf /config/backup-$(date +%Y%m%d).tar.gz /config/*.db
# Limpiar logs antiguos
docker exec sonarr find /config/logs -type f -mtime +30 -delete
```
### Orden de Configuración Recomendado
1. ✅ FlareSolverr (si es necesario)
2. ✅ Prowlarr → Añadir indexers
3. ✅ Sonarr/Radarr → Configurar cliente de descargas
4. ✅ Prowlarr → Conectar con Sonarr/Radarr
5. ✅ Jellyfin → Añadir bibliotecas
6. ✅ Jellyseerr → Conectar todo
## 🤝 Soporte y Contribuciones
Para problemas o sugerencias:
1. Revisa la sección de Troubleshooting
2. Consulta la documentación oficial de cada servicio
3. Verifica los logs del contenedor específico
## 📄 Licencia
Este stack utiliza software de código abierto. Consulta las licencias individuales de cada proyecto.
---
**Nota**: Este stack está diseñado para uso personal. Asegúrate de cumplir con las leyes de copyright de tu región al descargar contenido.

View File

@@ -0,0 +1,129 @@
networks:
proxy:
external: true
media:
driver: bridge
services:
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
- LOG_HTML=${LOG_HTML:-false}
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
- TZ=${TZ:-Europe/Madrid}
restart: unless-stopped
networks:
- media
shm_size: "1gb"
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
pull_policy: always
environment:
- PUID=0
- PGID=0
- TZ=${TZ:-Europe/Madrid}
volumes:
- /opt/media/configs/prowlarr:/config:Z
restart: unless-stopped
networks:
- media
- proxy
labels:
- 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
- TZ=${TZ:-Europe/Madrid}
volumes:
- /opt/media/configs/jackett:/config:Z
restart: unless-stopped
networks:
- media
- proxy
labels:
- 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
- TZ=${TZ:-Europe/Madrid}
volumes:
- /opt/media/configs/sonarr:/config:Z
- /mnt/media/tv:/tv
- /mnt/media/downloads:/downloads
restart: unless-stopped
networks:
- media
- proxy
labels:
- 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
- TZ=${TZ:-Europe/Madrid}
volumes:
- /opt/media/configs/radarr:/config:Z
- /mnt/media/movies:/movies
- /mnt/media/downloads:/downloads
restart: unless-stopped
networks:
- media
- proxy
labels:
- 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}
volumes:
- /opt/media/configs/jellyseerr:/app/config:Z
restart: unless-stopped
networks:
- media
- proxy
labels:
- 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
- TZ=${TZ:-Europe/Madrid}
volumes:
- /opt/media/configs/jellyfin-vps:/config:Z
- /opt/media/jellyfin/cache-vps:/cache:Z
- /mnt/media/tv:/data/tvshows
- /mnt/media/movies:/data/movies
- /mnt/media/downloads:/data/media_downloads
restart: unless-stopped
networks:
- media
- proxy
labels:
- traefik.http.services.jellyfin.loadbalancer.server.port=8096

96
media-server/stack.env Normal file
View File

@@ -0,0 +1,96 @@
# =========================
# Global
# =========================
TZ=
LOG_LEVEL=
LOG_HTML=
CAPTCHA_SOLVER=
# =========================
# Paths (VPS)
# =========================
COMMON_PATH=
# =========================
# Media mounts (HOST VPS)
# =========================
MEDIA_MOUNT_BASE=
MEDIA_TV=
MEDIA_MOVIES=
MEDIA_DOWNLOADS=
# =========================
# PUID/PGID
# =========================
PUID=
PGID=
# =========================
# Networks
# =========================
TRAEFIK_DOCKER_NETWORK=
MEDIA_NETWORK_NAME=
# =========================
# Traefik common
# =========================
TRAEFIK_ENABLE=
TRAEFIK_ENTRYPOINTS=
TRAEFIK_TLS=
TRAEFIK_CERTRESOLVER=
AUTH_MIDDLEWARE=
# =========================
# Images (optional override)
# =========================
FLARESOLVERR_IMAGE=
PROWLARR_IMAGE=
JACKETT_IMAGE=
SONARR_IMAGE=
RADARR_IMAGE=
JELLYSEERR_IMAGE=
JELLYFIN_IMAGE=
# =========================
# Container names (optional override)
# =========================
FLARESOLVERR_CONTAINER_NAME=
PROWLARR_CONTAINER_NAME=
JACKETT_CONTAINER_NAME=
SONARR_CONTAINER_NAME=
RADARR_CONTAINER_NAME=
JELLYSEERR_CONTAINER_NAME=
JELLYFIN_CONTAINER_NAME=
# =========================
# Service ports (optional override)
# =========================
PROWLARR_PORT=
JACKETT_PORT=
SONARR_PORT=
RADARR_PORT=
JELLYSEERR_PORT=
JELLYFIN_PORT=
# =========================
# Jellyseerr extra
# =========================
JELLYSEERR_LOG_LEVEL=
# =========================
# FlareSolverr resources
# =========================
FLARESOLVERR_SHM_SIZE=
# =========================
# Domains
# =========================
DOMAIN=
PROWLARR_HOST=
JACKETT_HOST=
SONARR_HOST=
RADARR_HOST=
JELLYSEERR_HOST=
JELLYFIN_HOST=

48
memos/docker-compose.yml Normal file
View File

@@ -0,0 +1,48 @@
services:
memos-db:
image: postgres:17-alpine
container_name: memos-db
restart: unless-stopped
environment:
TZ: ${TZ}
POSTGRES_DB: ${MEMOS_DB_NAME}
POSTGRES_USER: ${MEMOS_DB_USER}
POSTGRES_PASSWORD: ${MEMOS_DB_PASSWORD}
volumes:
- /opt/memos/postgres:/var/lib/postgresql/data:Z
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${MEMOS_DB_USER} -d ${MEMOS_DB_NAME}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- memos_internal
memos:
image: neosmemo/memos:stable
pull_policy: always
container_name: memos
restart: unless-stopped
depends_on:
memos-db:
condition: service_healthy
environment:
MEMOS_PORT: 5230
MEMOS_DATA: /var/opt/memos
MEMOS_DRIVER: postgres
MEMOS_DSN: postgresql://${MEMOS_DB_USER}:${MEMOS_DB_PASSWORD}@memos-db:5432/${MEMOS_DB_NAME}?sslmode=disable
MEMOS_INSTANCE_URL: https://${MEMOS_DOMAIN}
volumes:
- /opt/memos/data:/var/opt/memos:Z
networks:
- memos_internal
- proxy
labels:
traefik.http.services.memos.loadbalancer.server.port: "5230"
networks:
memos_internal:
driver: bridge
proxy:
external: true

5
memos/stack.env Normal file
View File

@@ -0,0 +1,5 @@
TZ=Europe/Madrid
MEMOS_DOMAIN=memos.sherlockhomeless.net
MEMOS_DB_NAME=memos
MEMOS_DB_USER=memos
MEMOS_DB_PASSWORD=CAMBIAME_db_super_largo_y_unico

View File

@@ -1,71 +1,62 @@
services:
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
pull_policy: always
container_name: n8n
restart: unless-stopped
environment:
- DB_TYPE=${N8N_DB_TYPE}
- DB_POSTGRESDB_HOST=${N8N_DB_HOST}
- DB_POSTGRESDB_PORT=${N8N_DB_PORT}
- DB_POSTGRESDB_DATABASE=${N8N_DB_NAME}
- DB_POSTGRESDB_USER=${N8N_DB_USER}
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
DB_TYPE: ${N8N_DB_TYPE}
DB_POSTGRESDB_HOST: ${N8N_DB_HOST}
DB_POSTGRESDB_PORT: ${N8N_DB_PORT}
DB_POSTGRESDB_DATABASE: ${N8N_DB_NAME}
DB_POSTGRESDB_USER: ${N8N_DB_USER}
DB_POSTGRESDB_PASSWORD: ${N8N_DB_PASSWORD}
- N8N_HOST=${N8N_HOST}
- N8N_PORT=${N8N_PORT}
- N8N_PROTOCOL=${N8N_PROTOCOL}
- WEBHOOK_URL=${N8N_WEBHOOK_URL}
N8N_HOST: ${SERVICE_FQDN_N8N}
N8N_PORT: ${N8N_PORT}
N8N_PROTOCOL: ${N8N_PROTOCOL}
WEBHOOK_URL: https://${SERVICE_FQDN_N8N}/
- GENERIC_TIMEZONE=${N8N_TIMEZONE}
GENERIC_TIMEZONE: ${N8N_TIMEZONE}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
# Clave para cifrar credenciales
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
NODE_ENV: ${N8N_NODE_ENV}
N8N_DIAGNOSTICS_ENABLED: ${N8N_DIAGNOSTICS_ENABLED}
- NODE_ENV=${N8N_NODE_ENV}
- N8N_DIAGNOSTICS_ENABLED=${N8N_DIAGNOSTICS_ENABLED}
N8N_EMAIL_MODE: ${N8N_EMAIL_MODE}
N8N_SMTP_HOST: ${N8N_SMTP_HOST}
N8N_SMTP_PORT: ${N8N_SMTP_PORT}
N8N_SMTP_USER: ${N8N_SMTP_USER}
N8N_SMTP_PASS: ${N8N_SMTP_PASS}
N8N_SMTP_SENDER: ${N8N_SMTP_SENDER}
N8N_SMTP_SSL: ${N8N_SMTP_SSL}
N8N_SMTP_STARTTLS: ${N8N_SMTP_STARTTLS}
volumes:
- /opt/n8n/data:/home/node/.n8n:Z
networks:
- proxy
- n8n
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "proxy"
traefik.http.routers.n8n-ui.rule: "Host(`${N8N_DOMAIN}`)"
traefik.http.routers.n8n-ui.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.n8n-ui.tls: "true"
traefik.http.routers.n8n-ui.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.n8n-ui.service: "n8n"
traefik.http.routers.n8n-ui.priority: "10"
traefik.http.routers.n8n-ui.middlewares: "${TRAEFIK_AUTH_MIDDLEWARE}"
traefik.http.routers.n8n-webhook.rule: "Host(`${N8N_DOMAIN}`) && (PathPrefix(`/webhook`) || PathPrefix(`/webhook-test`))"
traefik.http.routers.n8n-webhook.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.n8n-webhook.tls: "true"
traefik.http.routers.n8n-webhook.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.n8n-webhook.service: "n8n"
traefik.http.routers.n8n-webhook.priority: "20"
traefik.http.services.n8n.loadbalancer.server.port: "${N8N_PORT}"
traefik.http.services.n8n.loadbalancer.server.port: "5678"
n8n-db:
image: postgres:16
pull_policy: always
container_name: n8n-pg
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ${N8N_DB_DATA_PATH}:/var/lib/postgresql/data:Z
- /opt/n8n/postgres:/var/lib/postgresql/data:Z
networks:
- n8n
networks:
proxy:
external: true
n8n:
driver: bridge
proxy:
external: true

View File

@@ -1,36 +1,39 @@
##### n8n - Base de datos #####
N8N_DB_TYPE=
N8N_DB_HOST=
N8N_DB_PORT=
N8N_DB_NAME=
N8N_DB_USER=
N8N_DB_TYPE=postgresdb
N8N_DB_HOST=n8n-db
N8N_DB_PORT=5432
N8N_DB_NAME=n8n
N8N_DB_USER=n8n
N8N_DB_PASSWORD=
##### n8n - Servidor / dominio #####
N8N_HOST=
N8N_PORT=
N8N_PROTOCOL=
N8N_PORT=5678
N8N_PROTOCOL=https
N8N_WEBHOOK_URL=
N8N_TIMEZONE=
N8N_TIMEZONE=Europe/Madrid
# Genera una nueva con: openssl rand -hex 32
N8N_ENCRYPTION_KEY=
N8N_NODE_ENV=
N8N_DIAGNOSTICS_ENABLED=
N8N_NODE_ENV=production
N8N_DIAGNOSTICS_ENABLED=false
##### n8n - Correo (SMTP) #####
N8N_EMAIL_MODE=smtp
N8N_SMTP_HOST=
N8N_SMTP_PORT=587
N8N_SMTP_USER=
N8N_SMTP_PASS=
N8N_SMTP_SENDER=n8n@thehomelesssherlock.com
N8N_SMTP_SSL=false
N8N_SMTP_STARTTLS=false
##### PostgreSQL interno #####
POSTGRES_USER=
POSTGRES_USER=n8n
POSTGRES_PASSWORD=
POSTGRES_DB=
# Ruta en el host para los datos de Postgres (relativa o absoluta)
N8N_DB_DATA_PATH=
##### Traefik #####
N8N_DOMAIN=
TRAEFIK_ENTRYPOINT_SECURE=
TRAEFIK_CERTRESOLVER=
TRAEFIK_AUTH_MIDDLEWARE=
POSTGRES_DB=n8n
# Ruta en el host para los datos de Postgres
N8N_DB_DATA_PATH=/opt/n8n/postgres

View File

@@ -0,0 +1,171 @@
services:
nextcloud-db:
image: mariadb:lts
container_name: nextcloud-db
restart: unless-stopped
command: >
--transaction-isolation=READ-COMMITTED
--binlog-format=ROW
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
environment:
TZ: ${TZ}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- /opt/nextcloud/db:/var/lib/mysql:Z
networks:
- nextcloud_internal
nextcloud-redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
command: redis-server --save 60 1 --loglevel warning
environment:
TZ: ${TZ}
volumes:
- /opt/nextcloud/redis:/data:Z
networks:
- nextcloud_internal
nextcloud:
image: nextcloud:33-apache
container_name: nextcloud
pull_policy: always
restart: unless-stopped
depends_on:
- nextcloud-db
- nextcloud-redis
environment:
TZ: ${TZ}
MYSQL_HOST: nextcloud-db
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
REDIS_HOST: nextcloud-redis
NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: ${NC_DOMAIN} nextcloud localhost
TRUSTED_PROXIES: ${TRUSTED_PROXIES}
OVERWRITEHOST: ${NC_DOMAIN}
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://${NC_DOMAIN}
PHP_MEMORY_LIMIT: 2048M
PHP_UPLOAD_LIMIT: 16G
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_SECURE: ${SMTP_SECURE}
SMTP_AUTHTYPE: ${SMTP_AUTHTYPE}
SMTP_NAME: ${SMTP_NAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
MAIL_DOMAIN: ${MAIL_DOMAIN}
volumes:
- /opt/nextcloud/html:/var/www/html:Z
- /opt/nextcloud/config:/var/www/html/config:Z
- /opt/nextcloud/data:/var/www/html/data:Z
- /opt/nextcloud/custom_apps:/var/www/html/custom_apps:Z
- /opt/nextcloud/themes:/var/www/html/themes:Z
# Opcional: exponer archivo final de Paperless en Nextcloud como solo lectura
- /opt/paperless/media:/mnt/paperless-media:ro,Z
networks:
- nextcloud_internal
- proxy
- mail_internal
labels:
- 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
- traefik.http.middlewares.nc-secure-headers.headers.stsSeconds=31536000
- traefik.http.middlewares.nc-secure-headers.headers.stsIncludeSubdomains=true
- traefik.http.middlewares.nc-secure-headers.headers.stsPreload=true
- traefik.http.middlewares.nc-secure-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.nc-secure-headers.headers.browserXssFilter=true
- traefik.http.services.nextcloud.loadbalancer.server.port=80
nextcloud-cron:
image: nextcloud:33-apache
container_name: nextcloud-cron
restart: unless-stopped
depends_on:
- nextcloud
entrypoint: /cron.sh
environment:
TZ: ${TZ}
MYSQL_HOST: nextcloud-db
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
REDIS_HOST: nextcloud-redis
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_SECURE: ${SMTP_SECURE}
SMTP_AUTHTYPE: ${SMTP_AUTHTYPE}
SMTP_NAME: ${SMTP_NAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
MAIL_DOMAIN: ${MAIL_DOMAIN}
volumes:
- /opt/nextcloud/html:/var/www/html:Z
- /opt/nextcloud/config:/var/www/html/config:Z
- /opt/nextcloud/data:/var/www/html/data:Z
- /opt/nextcloud/custom_apps:/var/www/html/custom_apps:Z
- /opt/nextcloud/themes:/var/www/html/themes:Z
# Opcional: exponer archivo final de Paperless en Nextcloud como solo lectura
- /opt/paperless/media:/mnt/paperless-media:ro,Z
networks:
- nextcloud_internal
- mail_internal
onlyoffice-documentserver:
image: onlyoffice/documentserver:9.3.1
container_name: onlyoffice-documentserver
restart: unless-stopped
environment:
TZ: ${TZ}
JWT_ENABLED: "true"
JWT_SECRET: ${OO_JWT_SECRET}
JWT_HEADER: Authorization
SECURE_LINK_SECRET: ${OO_SECURE_LINK_SECRET}
ALLOW_PRIVATE_IP_ADDRESS: "true"
volumes:
- /opt/onlyoffice/logs:/var/log/onlyoffice:Z
- /opt/onlyoffice/data:/var/www/onlyoffice/Data:Z
- /opt/onlyoffice/lib:/var/lib/onlyoffice:Z
- /opt/onlyoffice/postgresql:/var/lib/postgresql:Z
#- /opt/onlyoffice/plugins:/var/www/onlyoffice/documentserver/sdkjs-plugins:Z
networks:
- nextcloud_internal
- proxy
labels:
- 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
- traefik.http.middlewares.oo-secure-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.oo-forwarded.headers.customRequestHeaders.X-Forwarded-Proto=https
- traefik.http.middlewares.oo-forwarded.headers.customRequestHeaders.X-Forwarded-Host=onlyoffice.sherlockhomeless.net
- traefik.http.middlewares.oo-forwarded.headers.customRequestHeaders.X-Forwarded-Port=443
- traefik.http.middlewares.oo-forwarded.headers.customRequestHeaders.X-Forwarded-Ssl=on
- traefik.http.services.onlyoffice.loadbalancer.server.port=80
networks:
nextcloud_internal:
driver: bridge
proxy:
external: true
mail_internal:
external: true

21
nextcloud/stack.env Normal file
View File

@@ -0,0 +1,21 @@
TZ=Europe/Madrid
NC_DOMAIN=nextcloud.sherlockhomeless.net
OO_DOMAIN=onlyoffice.sherlockhomeless.net
TRAEFIK_CERTRESOLVER=letsencrypt
TRUSTED_PROXIES=10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
MYSQL_ROOT_PASSWORD=change_me_mysql_root_password_long_and_secure
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=change_me_nextcloud_db_password_long_and_secure
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change_me_nextcloud_admin_password_long_and_secure
OO_JWT_SECRET=change_me_onlyoffice_jwt_secret_long_and_random
OO_SECURE_LINK_SECRET=change_me_onlyoffice_secure_link_secret_long_and_random
SMTP_HOST=mail-relay
SMTP_PORT=587
SMTP_SECURE=tls
SMTP_AUTHTYPE=
SMTP_NAME=
SMTP_PASSWORD=
MAIL_FROM_ADDRESS=nextcloud@thehomelesssherlock.com
MAIL_DOMAIN=thehomelesssherlock.com

View File

@@ -0,0 +1,155 @@
services:
paperless-db:
image: postgres:18
container_name: paperless-db
restart: unless-stopped
environment:
TZ: ${TZ}
POSTGRES_DB: ${PAPERLESS_DBNAME}
POSTGRES_USER: ${PAPERLESS_DBUSER}
POSTGRES_PASSWORD: ${PAPERLESS_DBPASS}
volumes:
- /opt/paperless/pgdata:/var/lib/postgresql:Z
networks:
- paperless_internal
paperless-redis:
image: redis:8
container_name: paperless-redis
restart: unless-stopped
volumes:
- /opt/paperless/redis:/data:Z
networks:
- paperless_internal
paperless-gotenberg:
image: gotenberg/gotenberg:8.27
container_name: paperless-gotenberg
restart: unless-stopped
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
networks:
- paperless_internal
paperless-tika:
image: apache/tika:latest
container_name: paperless-tika
restart: unless-stopped
networks:
- paperless_internal
paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
container_name: paperless
pull_policy: always
restart: unless-stopped
depends_on:
- paperless-db
- paperless-redis
- paperless-gotenberg
- paperless-tika
environment:
TZ: ${TZ}
PAPERLESS_REDIS: redis://paperless-redis:6379
PAPERLESS_DBHOST: paperless-db
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBNAME: ${PAPERLESS_DBNAME}
PAPERLESS_DBUSER: ${PAPERLESS_DBUSER}
PAPERLESS_DBPASS: ${PAPERLESS_DBPASS}
PAPERLESS_URL: https://${PAPERLESS_DOMAIN}
PAPERLESS_SECRET_KEY: ${PAPERLESS_SECRET_KEY}
PAPERLESS_ALLOWED_HOSTS: ${PAPERLESS_ALLOWED_HOSTS}
PAPERLESS_CSRF_TRUSTED_ORIGINS: https://${PAPERLESS_DOMAIN}
PAPERLESS_TRUSTED_PROXIES: ${TRUSTED_PROXIES}
PAPERLESS_ADMIN_USER: ${PAPERLESS_ADMIN_USER}
PAPERLESS_ADMIN_PASSWORD: ${PAPERLESS_ADMIN_PASSWORD}
PAPERLESS_ADMIN_MAIL: ${PAPERLESS_ADMIN_MAIL}
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_ENDPOINT: http://paperless-tika:9998
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://paperless-gotenberg:3000
# Más robusto cuando los ficheros llegan por sync/mount y no por inotify puro
PAPERLESS_CONSUMER_POLLING: ${PAPERLESS_CONSUMER_POLLING}
volumes:
- /opt/paperless/data:/usr/src/paperless/data:Z
- /opt/paperless/media:/usr/src/paperless/media:Z
- /opt/paperless/export:/usr/src/paperless/export:Z
- /opt/paperless/consume:/usr/src/paperless/consume:Z
networks:
- paperless_internal
- proxy
- mail_internal
labels:
- 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
- traefik.http.middlewares.paperless-secure-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.paperless-secure-headers.headers.browserXssFilter=true
- traefik.http.services.paperless.loadbalancer.server.port=8000
paperless-ai:
image: clusterzx/paperless-ai:latest
container_name: paperless-ai
pull_policy: always
restart: unless-stopped
depends_on:
- paperless
environment:
TZ: ${TZ}
volumes:
- /opt/paperless-ai/data:/app/data:Z
networks:
- paperless_internal
- proxy
labels:
- 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
- traefik.http.middlewares.paperless-ai-secure-headers.headers.contentTypeNosniff=true
- traefik.http.services.paperless-ai.loadbalancer.server.port=3000
# Sync unidireccional: Nextcloud/Paperless-Inbox -> paperless/consume
paperless-inbox-sync:
image: rclone/rclone:latest
container_name: paperless-inbox-sync
restart: unless-stopped
depends_on:
- paperless
entrypoint:
- /bin/sh
- /rclone-sync.sh
environment:
TZ: ${TZ}
RCLONE_CONFIG_NC_TYPE: webdav
RCLONE_CONFIG_NC_URL: https://${NC_DOMAIN}/remote.php/dav/files/${NC_WEBDAV_USER}
RCLONE_CONFIG_NC_VENDOR: nextcloud
RCLONE_CONFIG_NC_USER: ${NC_WEBDAV_USER}
RCLONE_CONFIG_NC_PASS: ${NC_WEBDAV_PASS}
RCLONE_SYNC_INTERVAL: ${RCLONE_SYNC_INTERVAL}
PAPERLESS_INBOX_DIR: ${PAPERLESS_INBOX_DIR}
volumes:
- /opt/paperless/consume:/consume:Z
- /opt/rclone:/config/rclone:Z
- /opt/paperless/rclone-sync.sh:/rclone-sync.sh:ro,Z
networks:
- paperless_internal
networks:
paperless_internal:
driver: bridge
proxy:
external: true
mail_internal:
external: true

19
paperless/stack.env Normal file
View File

@@ -0,0 +1,19 @@
TZ=Europe/Madrid
NC_DOMAIN=nextcloud.example.com
PAPERLESS_DOMAIN=paperless.example.com
PAPERLESS_AI_DOMAIN=paperless-ai.example.com
TRAEFIK_CERTRESOLVER=letsencrypt
TRUSTED_PROXIES=10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
PAPERLESS_DBNAME=paperless
PAPERLESS_DBUSER=paperless
PAPERLESS_DBPASS=change_me_paperless_db_password_long_and_secure
PAPERLESS_SECRET_KEY=change_me_paperless_secret_key_long_and_random_string
PAPERLESS_ADMIN_USER=admin
PAPERLESS_ADMIN_PASSWORD=change_me_paperless_admin_password_long_and_secure
PAPERLESS_ADMIN_MAIL=admin@example.com
PAPERLESS_CONSUMER_POLLING=60
PAPERLESS_ALLOWED_HOSTS=paperless.example.com,paperless,localhost
NC_WEBDAV_USER=paperless
NC_WEBDAV_PASS=change_me_nextcloud_webdav_password_long_and_random
PAPERLESS_INBOX_DIR=Paperless-Inbox
RCLONE_SYNC_INTERVAL=60

View File

@@ -1,438 +0,0 @@
# Ruleta - Aplicación Next.js
Aplicación web personalizada construida con Next.js.
## 📋 Descripción
Este stack despliega una aplicación Next.js con:
- Dos rutas de acceso: subdominio y path
- Integración con Traefik para HTTPS
- Soporte para múltiples dominios
- Configuración opcional de Authentik (SSO)
## 🚀 Despliegue
### Prerequisitos
1. **Red Docker**: Asegúrate de que la red `proxy` existe
2. **Registros DNS**: Configura los registros A para tus dominios
3. **Imagen Docker**: La aplicación debe estar construida como imagen Docker
### Desde Portainer
1. Ve a **Stacks****Add stack**
2. Nombre: `ruleta`
3. Selecciona **Repository** o **Git repository**
4. Configura:
- Repository URL: `<tu-repositorio>`
- Repository reference: `main`
- Compose path: `ruleta/docker-compose.yml`
5. Carga el archivo de variables de entorno: `ruleta/stack.env`
6. Haz clic en **Deploy the stack**
### Variables de Entorno
Edita el archivo `stack.env`:
```env
# Imagen de la aplicación (construida previamente)
RULETA_IMAGE=tu-usuario/ruleta:latest
# Entorno de Node.js
RULETA_NODE_ENV=production
RULETA_NEXT_TELEMETRY_DISABLED=1
# Puerto interno de Next.js
RULETA_APP_PORT=3000
# Dominios
# Opción 1: Subdominio dedicado
RULETA_SUBDOMAIN=ruleta.tudominio.com
# Opción 2: Path en dominio principal
RULETA_MAIN_DOMAIN=tudominio.com
# Traefik
TRAEFIK_DOCKER_NETWORK=proxy
TRAEFIK_ENTRYPOINT_SECURE=websecure
TRAEFIK_CERTRESOLVER=letsencrypt
# Si usas Supabase, descomenta y configura:
# NEXT_PUBLIC_SUPABASE_URL=https://tu-proyecto.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=tu-clave-publica
# SUPABASE_SERVICE_ROLE_KEY=tu-clave-privada
```
## ⚙️ Rutas de Acceso
Esta aplicación tiene dos formas de acceso configuradas:
### 1. Subdominio Dedicado
Accede mediante: `https://ruleta.tudominio.com`
- URL completa del subdominio
- Next.js recibe las peticiones en el path raíz `/`
- No requiere configuración especial en Next.js
### 2. Path en Dominio Principal
Accede mediante: `https://tudominio.com/ruleta`
- Path dentro del dominio principal
- Traefik elimina el prefijo `/ruleta` antes de enviar a Next.js
- Next.js recibe las peticiones como si estuvieran en `/`
> **Nota**: Puedes usar ambas rutas simultáneamente o deshabilitar una editando el `docker-compose.yml`.
## 🔧 Configuración de Next.js
### Configurar basePath (si usas path)
Si quieres que Next.js sea consciente del path `/ruleta`, edita `next.config.js`:
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: '/ruleta',
assetPrefix: '/ruleta',
// ... otras opciones
}
module.exports = nextConfig
```
> **Nota**: Con la configuración actual (middleware de stripprefix en Traefik), esto NO es necesario. Traefik se encarga de eliminar el prefijo.
### Variables de Entorno en Next.js
Para exponer variables al cliente (navegador):
```javascript
// next.config.js
module.exports = {
env: {
CUSTOM_VAR: process.env.CUSTOM_VAR,
},
// o usa NEXT_PUBLIC_ prefix
}
```
En `stack.env`:
```env
NEXT_PUBLIC_API_URL=https://api.tudominio.com
CUSTOM_VAR=valor-personalizado
```
## 🔒 Proteger con Authentik (Opcional)
Por defecto, la aplicación NO está protegida con SSO. Para protegerla:
### Opción 1: Proteger Todo
Edita el `docker-compose.yml` y descomenta:
```yaml
labels:
# Para subdominio
traefik.http.routers.ruleta-sub.middlewares: "authentik@docker"
# Para path (requiere cadena de middlewares)
traefik.http.routers.ruleta-path.middlewares: "authentik@docker,ruleta-strip@docker"
```
### Opción 2: Proteger Solo Ciertas Rutas
Crea routers adicionales en Traefik:
```yaml
# Router para rutas públicas
traefik.http.routers.ruleta-public.rule: "Host(`ruleta.tudominio.com`) && PathPrefix(`/api`, `/public`)"
traefik.http.routers.ruleta-public.priority: "20"
# Router para rutas protegidas
traefik.http.routers.ruleta-private.rule: "Host(`ruleta.tudominio.com`) && PathPrefix(`/admin`)"
traefik.http.routers.ruleta-private.middlewares: "authentik@docker"
traefik.http.routers.ruleta-private.priority: "30"
```
## 🏗️ Construir la Imagen Docker
### Dockerfile de Ejemplo
```dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copiar archivos necesarios
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
```
### next.config.js para Standalone
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
// ... otras opciones
}
module.exports = nextConfig
```
### Construir y Publicar
```bash
# Construir
docker build -t tu-usuario/ruleta:latest .
# Probar localmente
docker run -p 3000:3000 tu-usuario/ruleta:latest
# Publicar en Docker Hub
docker push tu-usuario/ruleta:latest
# O publicar en GitHub Container Registry
docker tag tu-usuario/ruleta:latest ghcr.io/tu-usuario/ruleta:latest
docker push ghcr.io/tu-usuario/ruleta:latest
```
## 🔧 Configuración Avanzada
### Integración con Supabase
Si tu aplicación usa Supabase:
1. Crea un proyecto en [supabase.com](https://supabase.com)
2. Obtén las credenciales:
- **URL del proyecto**: Settings → API → Project URL
- **Anon key**: Settings → API → anon public
- **Service role key**: Settings → API → service_role (secreto)
3. Añade a `stack.env`:
```env
NEXT_PUBLIC_SUPABASE_URL=https://tu-proyecto.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbG...
SUPABASE_SERVICE_ROLE_KEY=eyJhbG...
```
4. En tu código Next.js:
```javascript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
```
### Variables de Entorno Sensibles
Para secrets (API keys, tokens):
1. No las incluyas en `stack.env` si el repositorio es público
2. Configúralas manualmente en Portainer:
- Edita el stack
- Ve a la pestaña **Environment variables**
- Añade variables adicionales
3. O usa Docker secrets:
```yaml
services:
app:
secrets:
- api_key
secrets:
api_key:
external: true
```
### Volúmenes para Datos Persistentes
Si necesitas persistir datos (uploads, cache):
```yaml
services:
app:
volumes:
- ${RULETA_DATA_PATH}:/app/data:Z
- ${RULETA_UPLOADS_PATH}:/app/public/uploads:Z
```
Añade a `stack.env`:
```env
RULETA_DATA_PATH=/opt/ruleta/data
RULETA_UPLOADS_PATH=/opt/ruleta/uploads
```
## 🛠️ Troubleshooting
### La aplicación no inicia
1. Verifica los logs:
```bash
docker logs ruleta-app
```
2. Verifica que la imagen existe:
```bash
docker images | grep ruleta
```
3. Verifica variables de entorno:
```bash
docker inspect ruleta-app | grep -A 20 Env
```
### No puedo acceder por dominio
1. Verifica DNS:
```bash
nslookup ruleta.tudominio.com
```
2. Verifica Traefik:
```bash
docker logs traefik | grep ruleta
```
3. Verifica labels de Traefik:
```bash
docker inspect ruleta-app | grep -A 30 Labels
```
### Error 502 Bad Gateway
1. Verifica que el puerto interno es correcto:
- Next.js por defecto usa puerto 3000
- Verifica `RULETA_APP_PORT=3000`
2. Verifica que la aplicación está escuchando:
```bash
docker exec ruleta-app netstat -tulpn
```
3. Verifica conectividad desde Traefik:
```bash
docker exec traefik ping ruleta-app
```
### Las rutas no funcionan con basePath
Si configuraste `basePath` en Next.js:
1. **Opción A**: Elimina el middleware `stripprefix` de Traefik
2. **Opción B**: Elimina `basePath` de Next.js (Traefik se encarga)
### Recursos estáticos (CSS/JS) no cargan
Verifica:
1. El `assetPrefix` en `next.config.js` (debe coincidir con basePath)
2. Los logs del navegador (F12 → Console)
3. Las rutas en el inspector de red (F12 → Network)
### Variables de entorno no disponibles
Recuerda:
- Solo variables con prefijo `NEXT_PUBLIC_` están disponibles en el navegador
- Variables sin prefijo solo están disponibles en el servidor (API routes, getServerSideProps)
## 📚 Recursos Adicionales
- [Documentación de Next.js](https://nextjs.org/docs)
- [Deploying Next.js](https://nextjs.org/docs/deployment)
- [Next.js con Docker](https://github.com/vercel/next.js/tree/canary/examples/with-docker)
- [Traefik con Next.js](https://doc.traefik.io/traefik/routing/routers/)
## 🔄 Actualizaciones
### Actualizar la Aplicación
1. Construye una nueva versión de la imagen:
```bash
docker build -t tu-usuario/ruleta:v2.0 .
docker push tu-usuario/ruleta:v2.0
```
2. Actualiza en `stack.env`:
```env
RULETA_IMAGE=tu-usuario/ruleta:v2.0
```
3. Actualiza el stack en Portainer
### Rolling Updates
Para actualizaciones sin downtime, usa múltiples réplicas (requiere Docker Swarm o Kubernetes).
### Rollback
Si algo sale mal:
1. Vuelve a la versión anterior en `stack.env`
2. Actualiza el stack en Portainer
## 📊 Monitoreo
### Logs de la Aplicación
```bash
# Logs en tiempo real
docker logs -f ruleta-app
# Últimas 100 líneas
docker logs --tail 100 ruleta-app
# Logs con timestamps
docker logs -t ruleta-app
```
### Logs de Next.js
Next.js registra en stdout/stderr, accesibles con `docker logs`.
Para logs estructurados, considera usar:
- [Pino](https://github.com/pinojs/pino)
- [Winston](https://github.com/winstonjs/winston)
### Métricas
Para monitorear la aplicación:
1. Implementa un endpoint `/api/health`
2. Usa herramientas como Prometheus + Grafana
3. O servicios de APM como New Relic, Datadog
## 🎯 Casos de Uso
Este stack es ideal para:
- Aplicaciones Next.js personalizadas
- Landing pages
- Dashboards administrativos
- Aplicaciones full-stack con API routes
- JAMstack con SSR/SSG
## 💡 Tips
- Usa `output: 'standalone'` en Next.js para imágenes Docker más pequeñas
- Implementa caching con Redis para mejor rendimiento
- Usa ISR (Incremental Static Regeneration) para contenido dinámico
- Implementa rate limiting en API routes
- Usa CDN para assets estáticos (imágenes, CSS, JS)

View File

@@ -1,57 +0,0 @@
services:
# Aplicación Next.js
app:
image: ${RULETA_IMAGE}
container_name: ruleta-app
restart: unless-stopped
environment:
NODE_ENV: ${RULETA_NODE_ENV}
NEXT_TELEMETRY_DISABLED: ${RULETA_NEXT_TELEMETRY_DISABLED}
# Si usas Supabase, descomenta y configura:
# NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
# NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
# SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
networks:
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
# ---------------------------
# 1) Router EXISTENTE (subdominio)
# https://ruleta.example.com
# ---------------------------
traefik.http.routers.ruleta-sub.rule: "Host(`${RULETA_SUBDOMAIN}`)"
traefik.http.routers.ruleta-sub.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.ruleta-sub.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.ruleta-sub.service: "ruleta"
# ---------------------------
# 2) Router NUEVO (.net + path)
# https://sherlockhomeless.net/ruleta
# ---------------------------
traefik.http.routers.ruleta-path.rule: "Host(`${RULETA_MAIN_DOMAIN}`) && PathPrefix(`/ruleta`)"
traefik.http.routers.ruleta-path.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.ruleta-path.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
traefik.http.routers.ruleta-path.service: "ruleta"
traefik.http.routers.ruleta-path.middlewares: "ruleta-strip@docker"
# Quita /ruleta antes de llegar a Next.js
traefik.http.middlewares.ruleta-strip.stripprefix.prefixes: "/ruleta"
traefik.http.middlewares.ruleta-strip.stripprefix.forceSlash: "true"
# Servicio interno (Next.js escucha en 3000)
traefik.http.services.ruleta.loadbalancer.server.port: "${RULETA_APP_PORT}"
# Proteger con Authentik (si quieres habilitarlo)
# OJO: si lo activas, ponlo en ambos routers o usa una cadena.
# traefik.http.routers.ruleta-sub.middlewares: "authentik@docker"
# traefik.http.routers.ruleta-path.middlewares: "authentik@docker,ruleta-strip@docker"
networks:
proxy:
external: true

View File

@@ -1,18 +0,0 @@
##### Ruleta - App #####
RULETA_IMAGE=
RULETA_NODE_ENV=
RULETA_NEXT_TELEMETRY_DISABLED=
RULETA_APP_PORT=
# Supabase (opcional)
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
##### Traefik / dominios #####
TRAEFIK_DOCKER_NETWORK=
RULETA_SUBDOMAIN=
RULETA_MAIN_DOMAIN=
TRAEFIK_ENTRYPOINT_SECURE=
TRAEFIK_CERTRESOLVER=

View File

@@ -2,6 +2,7 @@ services:
trilium:
image: ${TRILIUM_IMAGE}
container_name: trilium
pull_policy: always
restart: unless-stopped
hostname: ${TRILIUM_HOSTNAME}
@@ -9,30 +10,17 @@ services:
TZ: ${TZ}
volumes:
- ${TRILIUM_DATA_PATH}:/home/node/trilium-data:Z
- /opt/trilium/data:/home/node/trilium-data:Z
expose:
- "${TRILIUM_HTTP_PORT}"
- "8080"
networks:
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_DOCKER_NETWORK}"
# Router HTTPS (dos dominios válidos, usando OR)
traefik.http.routers.trilium.rule: "Host(`${TRILIUM_DOMAIN_1}`) || Host(`${TRILIUM_DOMAIN_2}`)"
traefik.http.routers.trilium.entrypoints: "${TRAEFIK_ENTRYPOINT_SECURE}"
traefik.http.routers.trilium.tls: "true"
traefik.http.routers.trilium.tls.certresolver: "${TRAEFIK_CERTRESOLVER}"
# 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.services.trilium.loadbalancer.server.port: "8080"
traefik.http.middlewares.trilium-sec.headers.stsSeconds: "31536000"
traefik.http.middlewares.trilium-sec.headers.stsIncludeSubdomains: "true"
traefik.http.middlewares.trilium-sec.headers.stsPreload: "true"
@@ -42,4 +30,3 @@ services:
networks:
proxy:
external: true

View File

@@ -3,7 +3,7 @@ TRILIUM_IMAGE=
TRILIUM_HOSTNAME=
TZ=
TRILIUM_DATA_PATH=
TRILIUM_HTTP_PORT=
TRILIUM_HTTP_PORT=8080
##### Traefik / dominios #####
TRAEFIK_DOCKER_NETWORK=

View File

@@ -0,0 +1,57 @@
services:
vikunja-db:
image: postgres:17-alpine
container_name: vikunja-db
restart: unless-stopped
environment:
TZ: ${TZ}
POSTGRES_DB: ${VIKUNJA_DB_NAME}
POSTGRES_USER: ${VIKUNJA_DB_USER}
POSTGRES_PASSWORD: ${VIKUNJA_DB_PASSWORD}
volumes:
- /opt/vikunja/postgres:/var/lib/postgresql/data:Z
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${VIKUNJA_DB_USER} -d ${VIKUNJA_DB_NAME}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- vikunja_internal
vikunja:
image: vikunja/vikunja:latest
pull_policy: always
container_name: vikunja
restart: unless-stopped
user: "1000:1000"
depends_on:
vikunja-db:
condition: service_healthy
environment:
TZ: ${TZ}
VIKUNJA_SERVICE_PUBLICURL: https://${VIKUNJA_DOMAIN}/
VIKUNJA_SERVICE_JWTSECRET: ${VIKUNJA_JWT_SECRET}
VIKUNJA_DATABASE_TYPE: postgres
VIKUNJA_DATABASE_HOST: vikunja-db
VIKUNJA_DATABASE_PORT: 5432
VIKUNJA_DATABASE_DATABASE: ${VIKUNJA_DB_NAME}
VIKUNJA_DATABASE_USER: ${VIKUNJA_DB_USER}
VIKUNJA_DATABASE_PASSWORD: ${VIKUNJA_DB_PASSWORD}
VIKUNJA_SERVICE_ENABLECALDAV: "true"
VIKUNJA_MAILER_ENABLED: "false"
VIKUNJA_SERVICE_ENABLEEMAILREMINDERS: "false"
VIKUNJA_SERVICE_ENABLEREGISTRATION: "false"
volumes:
- /opt/vikunja/files:/app/vikunja/files:Z
networks:
- vikunja_internal
- proxy
labels:
traefik.http.services.vikunja.loadbalancer.server.port: "3456"
networks:
vikunja_internal:
driver: bridge
proxy:
external: true

6
vikunja/stack.env Normal file
View File

@@ -0,0 +1,6 @@
TZ=Europe/Madrid
VIKUNJA_DOMAIN=vikunja.sherlockhomeless.net
VIKUNJA_DB_NAME=vikunja
VIKUNJA_DB_USER=vikunja
VIKUNJA_DB_PASSWORD=CAMBIAME_db_super_largo_y_unico
VIKUNJA_JWT_SECRET=CAMBIAME_jwt_super_largo_y_unico_y_muy_largo

View File

@@ -83,7 +83,7 @@ WG_DOMAIN=vpn-admin.tudominio.com
TRAEFIK_DOCKER_NETWORK=proxy
TRAEFIK_ENTRYPOINT_SECURE=websecure
TRAEFIK_CERTRESOLVER=letsencrypt
TRAEFIK_AUTH_MIDDLEWARE=authentik@docker
TRAEFIK_AUTH_MIDDLEWARE=ths-authentik@docker
```
> **⚠️ Importante**:

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:
@@ -16,40 +17,25 @@ services:
WG_HOST: ${WG_HOST}
WG_PORT: ${WG_PORT}
PORT: ${WG_UI_PORT}
# Arranque desatendido (solo si el volumen está vacío)
INIT_ENABLED: ${INIT_ENABLED}
INIT_USERNAME: ${INIT_USERNAME}
INIT_PASSWORD: ${INIT_PASSWORD}
# Evita reglas ip6tables (tabla nat inexistente en el host)
DISABLE_IPV6: ${DISABLE_IPV6}
volumes:
- ${WG_DATA_PATH}:/etc/wireguard:Z
- ${WG_MODULES_PATH}:/lib/modules:ro,Z
- /opt/wg-easy:/etc/wireguard:Z
- /lib/modules:/lib/modules:ro,Z
# Puerto UDP de WireGuard expuesto al mundo
ports:
- "${WG_UDP_PORT}:${WG_PORT}/udp"
- "${TORRENT_PORT}:${TORRENT_PORT}/tcp"
- "${TORRENT_PORT}:${TORRENT_PORT}/udp"
networks:
- 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}"
traefik.http.services.wg.loadbalancer.server.port: "51821"
networks:
proxy:

View File

@@ -2,7 +2,7 @@
WG_EASY_IMAGE=
WG_HOST=
WG_PORT=
WG_UI_PORT=
WG_UI_PORT=51821
INIT_ENABLED=
INIT_USERNAME=