From 33c368936ecc2152e9250116226af50808ad19cd Mon Sep 17 00:00:00 2001 From: merios Date: Thu, 15 Oct 2026 11:34:39 -0300 Subject: [PATCH] feat: infraestructura base de Traefik + generador de proyectos - core/: reverse proxy Traefik v3 (HTTP 80 / HTTPS 443) - traefik.yml con provider Docker (exposedByDefault: false) y provider file - dynamic_tls.yml sanitizado con certificados .local comentados - certs/README.md con procedimiento de certificados auto-firmados - init_project.sh: generador de proyectos base (SRV_DIR configurable) - plantilla compose con redireccion HTTP->HTTPS + Let's Encrypt - bloque alternativo comentado para dominios .local - README.md: documentacion integral (puesta en marcha, HTTP/HTTPS, troubleshooting y seguridad) - docs/arquitectura.md: guia de instalacion y gestion de URLs - docs/http-https.md: referencia de etiquetas Traefik, TLS y certresolver - .gitignore: excluye .env, acme.json, certificados, datos y respaldos --- .env.global | 13 ++ .gitignore | 46 +++++ README.md | 404 ++++++++++++++++++++++++++++++++++++++ core/data/certs/README.md | 110 +++++++++++ core/data/dynamic_tls.yml | 19 ++ core/data/traefik.yml | 18 ++ core/docker-compose.yml | 23 +++ docs/arquitectura.md | 245 +++++++++++++++++++++++ docs/http-https.md | 247 +++++++++++++++++++++++ init_project.sh | 187 ++++++++++++++++++ 10 files changed, 1312 insertions(+) create mode 100644 .env.global create mode 100644 .gitignore create mode 100644 README.md create mode 100644 core/data/certs/README.md create mode 100644 core/data/dynamic_tls.yml create mode 100644 core/data/traefik.yml create mode 100644 core/docker-compose.yml create mode 100644 docs/arquitectura.md create mode 100644 docs/http-https.md create mode 100755 init_project.sh diff --git a/.env.global b/.env.global new file mode 100644 index 0000000..a3caef2 --- /dev/null +++ b/.env.global @@ -0,0 +1,13 @@ +# ============================================================================== +# Configuración global del servidor (opcional) +# ============================================================================== +# Este archivo NO contiene secretos reales y puede versionarse. +# Los secretos por proyecto viven en ~/srv/projects//.env (chmod 600) +# y NUNCA deben copiarse acá ni subirse al repositorio. +# ------------------------------------------------------------------------------ + +# Ruta base donde se alojan los proyectos administrados por init_project.sh +SRV_DIR=/home/merios/srv + +# Dominio/contacto del administrador del servidor (opcional) +# SERVER_ADMIN=marcos@ejemplo.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acc7ea0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# ============================================================================== +# Archivos que NUNCA deben subirse al repositorio +# ============================================================================== + +# --- Secretos y variables de entorno ----------------------------------------- +.env +.env.* +!.env.example +!.env.global +*.pem +*.p12 +*.pfx + +# --- Certificados (públicos y privados) -------------------------------------- +*.crt +*.key +*.csr +core/data/certs/* +!core/data/certs/README.md + +# --- Cuenta ACME de Let's Encrypt (contiene la clave privada) ---------------- +acme.json +**/acme.json + +# --- Datos persistentes, bases de datos y respaldos -------------------------- +/data/ +/projects/ +/backups/ +*.sql +*.sql.gz +*.dump +*.zip +*.tar.gz +*.tgz + +# --- Dependencias y artefactos de terceros ----------------------------------- +vendor/ +node_modules/ +.DS_Store +Thumbs.db + +# --- Editores / IDE ---------------------------------------------------------- +.idea/ +.vscode/ +*.swp +*~ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c87cf1b --- /dev/null +++ b/README.md @@ -0,0 +1,404 @@ +# TrafficServerLocal + +Infraestructura base para alojar múltiples proyectos Docker detrás de un único +proxy inverso **Traefik v3**, con soporte para **HTTP y HTTPS**: + +- **Dominios públicos** → certificados de **Let's Encrypt** (automático). +- **Dominios locales `.local`** → certificados **auto-firmados** montados desde + el filesystem. + +Este repositorio contiene **solo la infraestructura reutilizable**. El código, +las bases de datos y los datos persistentes de cada proyecto viven en +`~/srv/projects//` y se publican en sus propios repositorios. + +--- + +## 1. Estructura + +```text +~/srv/ +├── core/ # Reverse proxy Traefik +│ ├── docker-compose.yml +│ └── data/ +│ ├── traefik.yml # Configuración estática +│ ├── dynamic_tls.yml # Certificados estáticos (.local) +│ ├── acme.json # Clave ACME (NO versionado) +│ └── certs/ # .crt / .key locales (NO versionados) +├── projects/ # Un directorio por proyecto +│ └── / +│ ├── app/ # Código fuente / Dockerfile +│ ├── data/{db,storage}/ # Persistencia +│ ├── .env # Secretos (chmod 600) +│ └── docker-compose.yml # Etiquetas de Traefik +└── backups/ # Respaldos (fuera de Git) +``` + +En este repositorio (lo publicable): + +```text +TrafficServerLocal/ +├── README.md +├── .gitignore +├── .env.global +├── init_project.sh # Generador de proyectos base +├── docs/ +│ ├── arquitectura.md # Guía de instalación y gestión de URLs +│ └── http-https.md # Referencia de etiquetas HTTP/HTTPS y TLS +└── core/ + ├── docker-compose.yml + └── data/ + ├── traefik.yml + ├── dynamic_tls.yml + └── certs/README.md # Cómo generar certificados .local +``` + +--- + +## 2. Requisitos + +- **Docker Engine** y **Docker Compose** v2 (`docker compose`) +- El usuario debe pertenecer al grupo `docker`: + + ```bash + sudo usermod -aG docker "$USER" + newgrp docker # o cerrá y volvé a iniciar sesión + ``` + +- Puertos **80/443** libres en el host +- Para HTTPS público: el dominio debe apuntar por DNS a la IP del servidor + +--- + +## 3. Puesta en marcha + +### 3.1 Estructura y red compartida + +```bash +mkdir -p ~/srv/core ~/srv/projects +docker network create proxy_network # red externa compartida +``` + +`proxy_network` es **externa**: la crea una vez y la reutilizan el proxy y +todos los proyectos. + +### 3.2 Almacenamiento de certificados ACME + +```bash +mkdir -p ~/srv/core/data +touch ~/srv/core/data/acme.json +chmod 600 ~/srv/core/data/acme.json +``` + +> `acme.json` contiene la clave privada de la cuenta de Let's Encrypt. +> **Nunca se versiona** (ya está en `.gitignore`). + +### 3.3 Levantar el proxy + +```bash +cd ~/srv/core +docker compose up -d +docker ps | grep core-proxy +``` + +Traefik queda escuchando en `:80` (HTTP) y `:443` (HTTPS). + +--- + +## 4. Cómo funciona el proxy + +| Concepto | Detalle | +|---|---| +| **Provider Docker** | Traefik lee `/var/run/docker.sock` (solo lectura) y detecta contenedores con `traefik.enable=true` | +| `exposedByDefault: false` | Solo se publican los contenedores etiquetados | +| **Provider File** | Carga certificados estáticos desde `dynamic_tls.yml` con `watch: true` | +| `proxy_network` | Red externa por la que Traefik alcanza a los servicios | +| Red `default` | Red bridge **aislada** del proxy para bases de datos, colas, etc. | +| `no-new-privileges` | Endurecimiento básico del contenedor del proxy | +| Dashboard | Habilitado en `api.dashboard` (ver [Seguridad](#10-seguridad)) | + +### Puertos expuestos por el proxy + +```yaml +ports: + - "80:80" # entrypoint: web → HTTP + - "443:443" # entrypoint: websecure → HTTPS +``` + +--- + +## 5. Crear un proyecto base + +```bash +cd ~/srv +./init_project.sh api-ventas api.midominio.com 80 +``` + +Genera: + +```text +~/srv/projects/api-ventas/ +├── app/ # Código fuente, repo o Dockerfile +├── data/ +│ ├── db/ # Persistencia de base de datos +│ └── storage/ # Archivos del usuario +├── .env # Variables (chmod 600) +└── docker-compose.yml # Conectado a proxy_network +``` + +El directorio del proyecto queda con permisos `700`. + +### Pasos siguientes + +```bash +# 1. Colocar el código +git clone https://github.com/tu-usuario/api-ventas.git \ + ~/srv/projects/api-ventas/app/ +# o bien: +cp -r /ruta/a/tus/archivos/* ~/srv/projects/api-ventas/app/ + +# 2. Ajustar docker-compose.yml +# - Opción A: descomentar build: { context: ./app, dockerfile: Dockerfile } +# - Opción B: cambiar image: api-ventas:latest por tu imagen +# - Servicios extra (Postgres, Redis) → red `default` (aislados del proxy) + +# 3. Levantar +cd ~/srv/projects/api-ventas +docker compose up -d +``` + +### Variables generadas en `.env` + +```ini +APP_NAME=api-ventas +APP_DOMAIN=api.midominio.com +PORT=80 +``` + +Agregá ahí tus credenciales. **Nunca** subas ese archivo a Git. + +--- + +## 6. HTTP / HTTPS + +Detalle completo en **[`docs/http-https.md`](docs/http-https.md)**. + +### A) Dominio público + Let's Encrypt + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + + # Middleware de redirección HTTP → HTTPS + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" + + # Router HTTP (redirige) + - "traefik.http.routers.miapp-http.rule=Host(`api.midominio.com`)" + - "traefik.http.routers.miapp-http.entrypoints=web" + - "traefik.http.routers.miapp-http.middlewares=redirect-to-https" + + # Router HTTPS (emite el certificado) + - "traefik.http.routers.miapp-https.rule=Host(`api.midominio.com`)" + - "traefik.http.routers.miapp-https.entrypoints=websecure" + - "traefik.http.routers.miapp-https.tls=true" + - "traefik.http.routers.miapp-https.tls.certresolver=letsencrypt" + + # Servicio interno + - "traefik.http.services.miapp.loadbalancer.server.port=80" +``` + +### B) Dominio local `.local` + certificado auto-firmado + +1. Generar el certificado: + + ```bash + cd ~/srv/core/data/certs + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout miapp.local.key -out miapp.local.crt \ + -subj "/CN=miapp.local" -addext "subjectAltName=DNS:miapp.local" + ``` + +2. Declararlo en `core/data/dynamic_tls.yml`: + + ```yaml + tls: + certificates: + - certFile: /etc/traefik/certs/miapp.local.crt + keyFile: /etc/traefik/certs/miapp.local.key + ``` + +3. Etiquetar el proyecto **sin** certresolver: + + ```yaml + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.miapp.rule=Host(`miapp.local`)" + - "traefik.http.routers.miapp.entrypoints=websecure" + - "traefik.http.routers.miapp.tls=true" + - "traefik.http.services.miapp.loadbalancer.server.port=80" + ``` + +4. Reiniciar el proxy y resolver el nombre en el cliente: + + ```bash + cd ~/srv/core && docker compose restart traefik + echo "192.168.1.50 miapp.local" | sudo tee -a /etc/hosts + ``` + +Instrucciones completas (SAN múltiple, instalación del CA en cada SO) en +**[`core/data/certs/README.md`](core/data/certs/README.md)**. + +### C) Sin redirección (solo HTTP) + +Para entornos internos donde no interese HTTPS: + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.miapp.rule=Host(`miapp.local`)" + - "traefik.http.routers.miapp.entrypoints=web" + - "traefik.http.services.miapp.loadbalancer.server.port=80" +``` + +--- + +## 7. Comandos útiles + +```bash +# Estado y logs del proxy +docker ps | grep core-proxy +docker logs -f core-proxy + +# Recargar tras editar dynamic_tls.yml +cd ~/srv/core && docker compose restart traefik + +# Proyecto: ciclo de vida +cd ~/srv/projects/miapp +docker compose up -d # levantar +docker compose up -d --build # reconstruir imagen +docker compose restart # reiniciar +docker compose down # detener (conserva datos) +docker compose logs -f web + +# Entrar al contenedor +docker exec -it miapp-web bash + +# Ver redes de un contenedor +docker network inspect proxy_network --format '{{.Containers}}' +``` + +--- + +## 8. Ejemplo real: proyecto Laravel + PostgreSQL + +```yaml +services: + web: + build: + context: ./app + dockerfile: Dockerfile + container_name: optica-web + restart: unless-stopped + env_file: [ .env ] + volumes: + - ./app/src:/var/www/optica + - ./data/storage:/var/www/optica/storage + networks: [ default, proxy_network ] + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.routers.optica-http.rule=Host(`optica.local`)" + - "traefik.http.routers.optica-http.entrypoints=web" + - "traefik.http.routers.optica-http.middlewares=redirect-to-https" + - "traefik.http.routers.optica-https.rule=Host(`optica.local`)" + - "traefik.http.routers.optica-https.entrypoints=websecure" + - "traefik.http.routers.optica-https.tls=true" + - "traefik.http.routers.optica-https.service=optica-svc" + - "traefik.http.services.optica-svc.loadbalancer.server.port=80" + + pg: + image: postgres:14 + container_name: optica-pg + networks: [ default ] # ← aislada: no accesible desde el proxy + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + volumes: [ ./data/db:/var/lib/postgresql ] + +networks: + default: + driver: bridge + proxy_network: + external: true +``` + +> ⚠️ Mové las credenciales de `environment:` al `.env` del proyecto. +> Las contraseñas escritas directamente en el `docker-compose.yml` terminan en +> el historial de Git. + +--- + +## 9. Solución de problemas + +| Síntoma | Causa probable | Solución | +|---|---|---| +| `404 Not Found` | Contenedor sin `traefik.enable=true` o fuera de `proxy_network` | `docker network inspect proxy_network` | +| El router no aparece en el dashboard | Servicio sin `traefik.enable` o red incorrecta | Verificar etiquetas y redes del compose | +| `502 Bad Gateway` | El servicio interno no escucha en el puerto declarado | `docker exec -it sh -c 'netstat -tlnp'` | +| Certificado no se emite (Let's Encrypt) | DNS no apunta al server, o 80/443 bloqueados | `dig +short dominio`, revisar firewall | +| `too many certificates` / `rate limit` | Se emitted de más en staging/producción | Revisar `acme.json` y esperar la ventana de rate limit | +| `port is already allocated` | Otro servicio ocupa 80/443 | `docker ps --filter publish=80` | +| `permission denied` en Docker socket | Usuario fuera del grupo `docker` | `sudo usermod -aG docker $USER` y re-loguear | +| El `.crt` local no se aplica | Ruta incorrecta en `dynamic_tls.yml` | Usar la ruta **dentro del contenedor** (`/etc/traefik/certs/...`) y reiniciar | +| `no such file or directory: /acme.json` | Falta el archivo o permisos | `touch acme.json && chmod 600 acme.json` | +| Cambios en `traefik.yml` sin efecto | El archivo está montado `:ro` | `docker compose restart traefik` | + +Diagnóstico rápido: + +```bash +docker logs -f core-proxy | grep -iE "error|warn" +curl -sI http://miapp.local # debería devolver 301 → https +curl -sk https://miapp.local -I # -k ignora el certificado auto-firmado +``` + +--- + +## 10. Seguridad + +- **Nunca subas**: `.env`, `acme.json`, `*.key`, `*.crt`, contraseñas de bases de + datos, dumps SQL ni respaldos. El `.gitignore` de este repo lo cubre. +- Los `.env` por proyecto se crean con `chmod 600`. +- `docker.sock` se monta **solo lectura** (`ro`) en Traefik. +- El dashboard está habilitado. En producción: + - agregá `basicAuth` / `forwardAuth` a las etiquetas del dashboard, o + - deshabilitalo con `api.dashboard: false`. +- Traefik corre con `no-new-privileges:true`. +- `acme.json` debe quedar en `600` o el proxy no managea los certificados. +- Restringí los puertos publicados de las bases de datos (`ports:`) a `127.0.0.1` + si solo los necesitás desde el host: + ```yaml + ports: ["127.0.0.1:5432:5432"] + ``` + +--- + +## 11. Publicación + +Este repositorio es la **capa de infraestructura**. Cada proyecto se mantiene en +su propio repositorio y se despliega en `~/srv/projects//` siguiendo +los pasos de este README. + +```bash +cd ~/srv/core && docker compose pull && docker compose up -d # actualizar proxy +``` + +--- + +## Licencia + +MIT — ver el repositorio para detalles. diff --git a/core/data/certs/README.md b/core/data/certs/README.md new file mode 100644 index 0000000..c7e0495 --- /dev/null +++ b/core/data/certs/README.md @@ -0,0 +1,110 @@ +# Certificados locales (`.local`) + +Este directorio aloja los certificados **auto-firmados** que usan los dominios +internos (`*.local`). Está montado en el contenedor de Traefik como: + +```yaml +- ./data/certs:/etc/traefik/certs:ro +``` + +⚠️ **El contenido de esta carpeta NO se versiona** (ver `.gitignore`): +las claves privadas no deben salir del servidor. + +--- + +## 1. Generar un certificado auto-firmado + +Desde la raíz del repositorio: + +```bash +cd core/data/certs + +openssl req -x509 -nodes -days 3650 \ + -newkey rsa:2048 \ + -keyout miapp.local.key \ + -out miapp.local.crt \ + -subj "/CN=miapp.local" \ + -addext "subjectAltName=DNS:miapp.local" +``` + +Archivos resultantes: + +| Archivo | Descripción | +|---|---| +| `miapp.local.crt` | Certificado público (se puede distribuir) | +| `miapp.local.key` | **Clave privada** — permisos `600`, nunca se comparte | + +Si el certificado debe cubrir **varios** nombres en un solo `.crt`: + +```bash +openssl req -x509 -nodes -days 3650 \ + -newkey rsa:2048 \ + -keyout intranet.local.key \ + -out intranet.local.crt \ + -subj "/CN=intranet.local" \ + -addext "subjectAltName=DNS:intranet.local,DNS:app.intranet.local,DNS:api.intranet.local" +``` + +## 2. Declararlo en Traefik + +Editar `../dynamic_tls.yml` y descomentar el bloque **con la ruta interna**: + +```yaml +tls: + certificates: + - certFile: /etc/traefik/certs/miapp.local.crt + keyFile: /etc/traefik/certs/miapp.local.key +``` + +## 3. Recargar el proxy + +```bash +cd ../../ # desde core/ +docker compose restart traefik +docker logs -f core-proxy +``` + +`dynamic_tls.yml` tiene `watch: true`, pero reiniciar es la forma más segura de +confirmar que el certificado se cargó. + +## 4. Etiquetar el proyecto + +El router debe declarar `tls=true` **sin** certresolver (el certificado ya lo +aporta el provider `file`): + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.miapp.rule=Host(`miapp.local`)" + - "traefik.http.routers.miapp.entrypoints=websecure" + - "traefik.http.routers.miapp.tls=true" + - "traefik.http.services.miapp.loadbalancer.server.port=80" +``` + +## 5. Resolver el nombre en el cliente + +En cada máquina que deba acceder al sitio: + +```bash +echo "192.168.1.50 miapp.local" | sudo tee -a /etc/hosts +``` + +## 6. Evitar el aviso del navegador + +El navegador mostrará "certificado no confiable" porque es auto-firmado. +Opciones: + +- **Windows**: `certutil -addstore -f "ROOT" miapp.local.crt` (PowerShell admin). +- **Linux (systemd)**: copiar a `/usr/local/share/ca-certificates/` y ejecutar + `sudo update-ca-certificates`. +- **macOS**: doble clic sobre el `.crt` → *Añadir al llavero* → *Confiar*. +- **Firefox** no usa el almacén del sistema:Settings → Certificates → Import. + +--- + +## Certificados reales que usa este servidor + +Los certificados de producción de los dominios públicos **no** viven acá: los +emite Let's Encrypt y Traefik los guarda en `../acme.json` (también excluido +del repositorio). diff --git a/core/data/dynamic_tls.yml b/core/data/dynamic_tls.yml new file mode 100644 index 0000000..3865b2f --- /dev/null +++ b/core/data/dynamic_tls.yml @@ -0,0 +1,19 @@ +# ============================================================================== +# Certificados TLS estáticos (dominios .local / sin Let's Encrypt) +# ============================================================================== +# Traefik monta este archivo en /etc/traefik/dynamic_tls.yml (solo lectura) y lo +# recarga automáticamente (watch: true en traefik.yml). +# +# Para habilitar un certificado local: +# 1. Generá el par .crt / .key en core/data/certs/ (ver core/data/certs/README.md) +# 2. Descomentá el bloque correspondiente con la RUTA ABSOLUTA dentro del +# contenedor (/etc/traefik/certs/...), no la ruta del host. +# 3. Reiniciá el proxy: docker compose restart traefik (desde core/) +# +# Los archivos .crt y .key están excluidos por .gitignore: nunca se versionan. +# ------------------------------------------------------------------------------ + +tls: + certificates: [] + # - certFile: /etc/traefik/certs/miapp.local.crt + # keyFile: /etc/traefik/certs/miapp.local.key diff --git a/core/data/traefik.yml b/core/data/traefik.yml new file mode 100644 index 0000000..5c141ff --- /dev/null +++ b/core/data/traefik.yml @@ -0,0 +1,18 @@ +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + network: "proxy_network" + exposedByDefault: false + file: + filename: "/etc/traefik/dynamic_tls.yml" + watch: true diff --git a/core/docker-compose.yml b/core/docker-compose.yml new file mode 100644 index 0000000..ae58d99 --- /dev/null +++ b/core/docker-compose.yml @@ -0,0 +1,23 @@ +services: + traefik: + image: traefik:v3.0 + container_name: core-proxy + restart: unless-stopped + security_opt: + - no-new-privileges:true + ports: + - "80:80" + - "443:443" + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./data/traefik.yml:/traefik.yml:ro + - ./data/dynamic_tls.yml:/etc/traefik/dynamic_tls.yml:ro + - ./data/certs:/etc/traefik/certs:ro + - ./data/acme.json:/acme.json + networks: + - proxy_network + +networks: + proxy_network: + external: true diff --git a/docs/arquitectura.md b/docs/arquitectura.md new file mode 100644 index 0000000..51d345a --- /dev/null +++ b/docs/arquitectura.md @@ -0,0 +1,245 @@ +# Arquitectura del servidor y gestión de URLs + +Guía de instalación del entorno base en el host (`~/srv`), despliegue de la +infraestructura común y alta de proyectos Docker enlazados al proxy inverso. + +> Documento de referencia. Para el detalle fino de etiquetas TLS ver +> [`http-https.md`](http-https.md). + +--- + +## 1. Configuración inicial del host + +### 1.1 Estructura base y red Docker + +```bash +mkdir -p ~/srv/core ~/srv/projects +docker network create proxy_network +``` + +Permisos para usar Docker sin `sudo`: + +```bash +sudo usermod -aG docker "$USER" +newgrp docker +``` + +### 1.2 Habilitar el script generador + +```bash +chmod +x ~/srv/init_project.sh +``` + +--- + +## 2. Puesta en marcha del reverse proxy (`~/srv/core`) + +### Opción A — Traefik (detección automática por etiquetas) + +`~/srv/core/docker-compose.yml`: + +```yaml +services: + traefik: + image: traefik:v3.0 + container_name: core-proxy + restart: unless-stopped + security_opt: + - no-new-privileges:true + ports: + - "80:80" + - "443:443" + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./data/traefik.yml:/traefik.yml:ro + - ./data/acme.json:/acme.json + networks: + - proxy_network + +networks: + proxy_network: + external: true +``` + +Configuración estática `~/srv/core/data/traefik.yml`: + +```yaml +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + network: "proxy_network" + exposedByDefault: false + file: + filename: "/etc/traefik/dynamic_tls.yml" + watch: true +``` + +Inicialización del almacenamiento de certificados: + +```bash +mkdir -p ~/srv/core/data +touch ~/srv/core/data/acme.json +chmod 600 ~/srv/core/data/acme.json +``` + +Arranque: + +```bash +cd ~/srv/core && docker compose up -d +``` + +### Opción B — Nginx Proxy Manager (panel web) + +Misma idea, pero la configuración se hace desde la interfaz: + +1. Entrar a `http://:81`. +2. **Hosts → Proxy Hosts → Add Proxy Host**. + - **Domain Names:** `api.midominio.com` + - **Scheme:** `http` + - **Forward Hostname / IP:** nombre del contenedor en `proxy_network` + - **Forward Port:** puerto interno del contenedor + - **Block Common Exploits:** activado +3. Pestaña **SSL** → *Request a new SSL Certificate* → *Force SSL*. +4. **Save**. + +--- + +## 3. Alta de proyectos + +### Paso 1 — Crear la estructura limpia + +```bash +~/srv/init_project.sh api-ventas api.midominio.com 80 +``` + +Genera: + +```text +~/srv/projects/api-ventas/ +├── app/ # Código, repo o Dockerfile +├── data/ +│ ├── db/ +│ └── storage/ +├── .env # Variables de entorno (chmod 600) +└── docker-compose.yml # Plantilla conectada a proxy_network +``` + +### Paso 2 — Colocar el proyecto + +```bash +# Clonando un repositorio +git clone https://github.com/tu-usuario/api-ventas.git ~/srv/projects/api-ventas/app/ + +# O copiando archivos locales +cp -r /ruta/a/tus/archivos/* ~/srv/projects/api-ventas/app/ +``` + +### Paso 3 — Ajustar `docker-compose.yml` + +- **Build local:** descomentar + ```yaml + build: + context: ./app + dockerfile: Dockerfile + ``` +- **Imagen ya publicada:** reemplazar `image: :latest` +- **Servicios adicionales** (Postgres, Redis): agregarlos en el mismo archivo + usando la red `default`, así quedan aislados del proxy. + +### Paso 4 — Levantar + +```bash +cd ~/srv/projects/api-ventas +docker compose up -d +``` + +### Paso 5 — Post-instalación (opcional, apps Laravel) + +```bash +docker exec -it -web composer install --no-interaction --prefer-dist +docker exec -it -web php artisan key:generate --force +docker exec -it -web php artisan migrate --force +docker exec -it -web php artisan optimize:clear +``` + +Verificar permisos si el usuario `www-data` no puede escribir: + +```bash +docker exec -it -web chown -R www-data:www-data /var/www//storage +docker exec -it -web chmod -R 775 /var/www//storage /var/www//bootstrap/cache +``` + +--- + +## 4. Dónde y cómo se registran las URLs + +### Traefik (automático) + +El registro se hace con **etiquetas** en el `docker-compose.yml` del proyecto: + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.api-ventas.rule=Host(`api.midominio.com`)" + - "traefik.http.routers.api-ventas.entrypoints=websecure" + - "traefik.http.routers.api-ventas.tls=true" + - "traefik.http.routers.api-ventas.tls.certresolver=letsencrypt" + - "traefik.http.services.api-ventas.loadbalancer.server.port=80" +``` + +Traefik lee el socket de Docker, detecta el contenedor, genera el enrutamiento y +solicita el certificado SSL automáticamente. + +### Nginx Proxy Manager (panel web) + +Ver [Opción B](#opción-b--nginx-proxy-manager-panel-web). + +--- + +## 5. Copias de seguridad + +Los respaldos se guardan **fuera** de los repositorios Git: + +```bash +# PostgreSQL +docker exec pg_dump -U > ~/srv/backups/-$(date +%F).sql + +# MySQL +docker exec mysqldump -u -p > ~/srv/backups/-$(date +%F).sql + +# Archivos +tar czf ~/srv/backups/-files-$(date +%F).tar.gz ~/srv/projects//data +``` + +Sugerencia de rotación (crontab diario a las 3:00): + +```cron +0 3 * * * /usr/local/bin/backup-proyectos.sh >> ~/srv/backups/backup.log 2>&1 +``` + +--- + +## 6. Resumen de convenciones + +| Elemento | Convención | +|---|---| +| Carpeta de proyecto | `~/srv/projects//` | +| Nombre del contenedor | `-web` | +| Red del proxy | `proxy_network` (externa) | +| Red interna | `default` (bridge, aislada) | +| Secretos | `~/srv/projects//.env` con `chmod 600` | +| Dominios locales | `.local` | +| Certificados `.local` | `~/srv/core/data/certs/` + `dynamic_tls.yml` | +| Certificados públicos | Let's Encrypt vía `acme.json` | diff --git a/docs/http-https.md b/docs/http-https.md new file mode 100644 index 0000000..dcc4aac --- /dev/null +++ b/docs/http-https.md @@ -0,0 +1,247 @@ +# HTTP / HTTPS en Traefik — referencia de etiquetas + +Todos los ejemplos son etiquetas de Docker Compose aplicables al servicio `web` +del proyecto. Traefik las lee por el socket de Docker y construye la cadena +**Router → Middleware → Service**. + +--- + +## 1. Anatomía de una etiqueta + +```text +traefik.http...= + │ │ │ + │ │ └─ propiedad (rule, entrypoints, tls, port…) + │ └─────────── tipo (routers, services, middlewares) + └──────────────────── ámbito/grupo (ej. "miapp-https") +``` + +Componentes usados: + +| Componente | Función | +|---|---| +| `routers` | Decide **qué** petición llega (Host, Path) y **a qué entrypoint** responde | +| `middlewares` | Transformaciones (redirecciones, auth, headers) | +| `services` | Traduce a **con qué puerto interno** del contenedor | +| `tls` | Habilita TLS y define de dónde sale el certificado | + +--- + +## 2. Exponer un proyecto (mínimo funcional) + +```yaml +networks: + default: + driver: bridge + proxy_network: + external: true + +services: + web: + build: { context: ./app, dockerfile: Dockerfile } + container_name: miapp-web + restart: unless-stopped + networks: [ default, proxy_network ] # ← ambas redes + labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.miapp.rule=Host(`miapp.local`)" + - "traefik.http.routers.miapp.entrypoints=websecure" + - "traefik.http.services.miapp.loadbalancer.server.port=80" +``` + +Notas: + +- `traefik.enable=true` es **obligatorio** (`exposedByDefault: false`). +- `traefik.docker.network=proxy_network` fuerza a leer las etiquetas desde esa + red cuando el contenedor está en varias. +- El puerto del service es el **puerto interno**, no el publicado. + +--- + +## 3. Modo A — Dominio público con Let's Encrypt + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + + # 1. Middleware de redirección + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" + + # 2. Router HTTP (:80) → redirige + - "traefik.http.routers.miapp-http.rule=Host(`app.ejemplo.com`)" + - "traefik.http.routers.miapp-http.entrypoints=web" + - "traefik.http.routers.miapp-http.middlewares=redirect-to-https" + + # 3. Router HTTPS (:443) → sirve el contenido + - "traefik.http.routers.miapp-https.rule=Host(`app.ejemplo.com`)" + - "traefik.http.routers.miapp-https.entrypoints=websecure" + - "traefik.http.routers.miapp-https.tls=true" + - "traefik.http.routers.miapp-https.tls.certresolver=letsencrypt" + - "traefik.http.routers.miapp-https.service=miapp-svc" + + # 4. Servicio interno + - "traefik.http.services.miapp-svc.loadbalancer.server.port=80" +``` + +Requisitos: + +1. DNS `A/AAAA` apuntando a la IP del servidor. +2. Puertos **80 y 443** abiertos (el challenge HTTP-01 de Let's Encrypt usa el 80). +3. `~/srv/core/data/acme.json` existente y con permisos `600`. + +> Traefik emite el certificado la primera vez que recibe tráfico HTTPS por ese +> Host. Para forzarlo: `docker logs -f core-proxy` y hacer un `curl` al dominio. + +### Mejorar el router HTTP con challenge sin romper la redirección + +```yaml + - "traefik.http.routers.miapp-http.tls=true" + - "traefik.http.routers.miapp-http.tls.certresolver=letsencrypt" + - "traefik.http.routers.miapp-http.middlewares=redirect-to-https" +``` + +Esto permite que Let's Encrypt valide por el puerto 80 aunque todo el tráfico +se redirija a HTTPS. + +--- + +## 4. Modo B — Dominio local `.local` con certificado auto-firmado + +### 4.1 Generar el certificado + +```bash +cd ~/srv/core/data/certs +openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout miapp.local.key -out miapp.local.crt \ + -subj "/CN=miapp.local" \ + -addext "subjectAltName=DNS:miapp.local" +chmod 600 miapp.local.key +``` + +Certificado multi-dominio: + +```bash +openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout intranet.local.key -out intranet.local.crt \ + -subj "/CN=intranet.local" \ + -addext "subjectAltName=DNS:intranet.local,DNS:app.intranet.local,DNS:api.intranet.local" +``` + +### 4.2 Declararlo en el provider `file` + +`~/srv/core/data/dynamic_tls.yml` (montado en `/etc/traefik/dynamic_tls.yml`): + +```yaml +tls: + certificates: + - certFile: /etc/traefik/certs/miapp.local.crt + keyFile: /etc/traefik/certs/miapp.local.key +``` + +> La ruta es **la de dentro del contenedor**, porque el directorio se monta en +> `/etc/traefik/certs`. + +### 4.3 Etiquetar el proyecto (sin certresolver) + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.miapp.rule=Host(`miapp.local`)" + - "traefik.http.routers.miapp.entrypoints=websecure" + - "traefik.http.routers.miapp.tls=true" + - "traefik.http.services.miapp.loadbalancer.server.port=80" +``` + +Recargar: + +```bash +cd ~/srv/core && docker compose restart traefik +``` + +### 4.4 Resolver el nombre en el cliente + +```bash +echo "192.168.1.50 miapp.local" | sudo tee -a /etc/hosts +``` + +--- + +## 5. Modo C — Solo HTTP (entornos internos) + +```yaml +labels: + - "traefik.enable=true" + - "traefik.docker.network=proxy_network" + - "traefik.http.routers.miapp.rule=Host(`miapp.local`)" + - "traefik.http.routers.miapp.entrypoints=web" + - "traefik.http.services.miapp.loadbalancer.server.port=80" +``` + +--- + +## 6. Varios dominios en un mismo proyecto + +Dos approaches: + +**a) Múltiples reglas en un router** (OR lógico): + +```yaml + - "traefik.http.routers.miapp.rule=Host(`app.local`) || Host(`www.app.local`)" +``` + +**b) Un router por dominio**, apuntando al mismo service: + +```yaml + - "traefik.http.routers.www.rule=Host(`www.app.local`)" + - "traefik.http.routers.www.entrypoints=websecure" + - "traefik.http.routers.www.tls=true" + - "traefik.http.routers.www.service=miapp-svc" +``` + +Ambos necesitan los certificados correspondientes en `dynamic_tls.yml` o en +Let's Encrypt (agregando los dominios a la regla del router con certresolver). + +--- + +## 7. Errores frecuentes + +| Error | Causa | Solución | +|---|---|---| +| `404` en todos los dominios | `traefik.enable` ausente | Agregar la etiqueta | +| `404` solo en un dominio | Regla `Host()` mal escrita | Verificar comillas invertidas: `` Host(`dominio`) `` | +| `502 Bad Gateway` | Puerto interno incorrecto | Confirmar `EXPOSE` del Dockerfile y el puerto real | +| `too many redirects` | Router HTTP con `redirectscheme` sin HTTPS válido | Verificar que exista el router `websecure` | +| `TLS handshake error` / `no certificate` | `dynamic_tls.yml` mal cargado o certificado expirado | Rutas internas correctas + `docker compose restart traefik` | +| `cannot generate ACME certificate` | DNS mal, puertos cerrados o `acme.json` sin permisos | `dig`, revisar firewall y `chmod 600 acme.json` | +| El router no cambia al editar el compose | Etiquetas cacheadas | `docker compose up -d --force-recreate` | + +Verificación: + +```bash +curl -sI http://miapp.local # 301 → https://miapp.local +curl -sk https://miapp.local -I # respuesta del backend (-k ignora el cert) +``` + +--- + +## 8. Cheatsheet de etiquetas + +| Etiqueta | Efecto | +|---|---| +| `traefik.enable=true` | Publica el contenedor | +| `traefik.docker.network=` | Red desde la que leer etiquetas | +| `...routers.X.rule=Host(\`d\`)` | Enruta por dominio | +| `...routers.X.rule=PathPrefix(\`/api\`)` | Enruta por ruta | +| `...routers.X.entrypoints=web\|websecure` | Puerto de escucha (80 / 443) | +| `...routers.X.tls=true` | Habilita TLS en ese router | +| `...routers.X.tls.certresolver=letsencrypt` | Emite/obtiene el certificado | +| `...routers.X.middlewares=mw1,mw2` | Aplica middlewares (orden izquierda→derecha) | +| `...routers.X.service=svc` | Fuerza el service (si hay más de uno) | +| `...services.X.loadbalancer.server.port=N` | Puerto interno del contenedor | +| `...middlewares.X.redirectscheme.scheme=https` | Redirige HTTP → HTTPS | +| `...middlewares.X.redirectscheme.permanent=true` | 301 en vez de 302 | +| `...middlewares.X.basicauth.users=file` | Protección básica | diff --git a/init_project.sh b/init_project.sh new file mode 100755 index 0000000..38fa3ad --- /dev/null +++ b/init_project.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash + +# ============================================================================== +# Script de aprovisionamiento de proyectos Docker (Estructura Limpia) +# ============================================================================== +# Uso: ./init_project.sh [puerto-interno] +# +# Genera en $SRV_DIR/projects/ la estructura base: +# app/ -> código fuente, repo o Dockerfile +# data/{db,storage}/ -> volúmenes persistentes +# .env -> variables de entorno (chmod 600) +# docker-compose.yml -> plantilla conectada a proxy_network + etiquetas Traefik +# +# La plantilla incluye HTTP con redirección a HTTPS y HTTPS con Let's Encrypt, +# más un bloque comentado alternativo para dominios .local con certificado +# auto-firmado (ver docs/http-https.md). +# ============================================================================== +set -euo pipefail + +# Directorio base de los proyectos. Configurable con SRV_DIR. +BASE_DIR="${SRV_DIR:-$HOME/srv}" +PROJECTS_DIR="$BASE_DIR/projects" +NETWORK_NAME="proxy_network" + +# Colores para salida de terminal +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# Validar argumentos +if [ $# -lt 2 ]; then + echo -e "${RED}Uso:${NC} $0 [puerto-interno]" + echo -e "Ejemplo: $0 api-ventas api.midominio.com 80" + exit 1 +fi + +PROJECT_NAME="$1" +PROJECT_DOMAIN="$2" +INTERNAL_PORT="${3:-80}" +TARGET_DIR="$PROJECTS_DIR/$PROJECT_NAME" + +echo -e "${BLUE}==>${NC} Inicializando entorno para: ${GREEN}$PROJECT_NAME${NC}" + +# 1. Asegurar red compartida externa +if ! docker network ls --format '{{.Name}}' | grep -wq "$NETWORK_NAME"; then + echo -e "${YELLOW}Creando red externa $NETWORK_NAME...${NC}" + docker network create "$NETWORK_NAME" +fi + +# 2. Comprobar que no exista el proyecto +if [ -d "$TARGET_DIR" ]; then + echo -e "${RED}Error:${NC} El directorio $TARGET_DIR ya existe." + exit 1 +fi + +# 3. Crear árbol de carpetas limpias +# - app/: Destino para clonar repositorios o poner tu código fuente / Dockerfile +# - data/: Para persistencia de volúmenes de base de datos o archivos +mkdir -p "$PROJECTS_DIR" +mkdir -p "$TARGET_DIR/app" +mkdir -p "$TARGET_DIR/data/db" +mkdir -p "$TARGET_DIR/data/storage" + +# Asignar permisos restrictivos al directorio raíz del proyecto +chmod 700 "$TARGET_DIR" + +# 4. Crear archivo .env base personalizable +cat < "$TARGET_DIR/.env" +# ============================================================================== +# Variables de entorno - $PROJECT_NAME +# ============================================================================== +APP_NAME=$PROJECT_NAME +APP_DOMAIN=$PROJECT_DOMAIN +PORT=$INTERNAL_PORT + +# Agrega aquí las variables y secretos de tu proyecto. +# Este archivo tiene permisos 600 y NO debe subirse a Git. +EOF + +chmod 600 "$TARGET_DIR/.env" + +# 5. Crear plantilla docker-compose.yml lista para tu propia imagen o build local +cat < "$TARGET_DIR/docker-compose.yml" +services: + # Servicio principal conectado al Proxy Reverso + web: + # Opción A: Construir tu propio Dockerfile dentro de app/ + build: + context: ./app + dockerfile: Dockerfile + + # Opción B: Usar una imagen existente de tu registro o Docker Hub + # (comentá el bloque build: de arriba y descomentá la línea image:) + + image: ${PROJECT_NAME}:latest + container_name: ${PROJECT_NAME}-web + restart: unless-stopped + env_file: + - .env + volumes: + - ./app:/app + - ./data/storage:/app/storage + networks: + - default + - ${NETWORK_NAME} + labels: + - "traefik.enable=true" + - "traefik.docker.network=${NETWORK_NAME}" + + # ====================================================================== + # MODO A — Dominio público con Let's Encrypt + # ====================================================================== + + # 1. Middleware de redirección HTTP -> HTTPS + - "traefik.http.middlewares.${PROJECT_NAME}-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.${PROJECT_NAME}-redirect.redirectscheme.permanent=true" + + # 2. Router HTTP (puerto 80): redirige al HTTPS + - "traefik.http.routers.${PROJECT_NAME}-http.rule=Host(\`${PROJECT_DOMAIN}\`)" + - "traefik.http.routers.${PROJECT_NAME}-http.entrypoints=web" + - "traefik.http.routers.${PROJECT_NAME}-http.middlewares=${PROJECT_NAME}-redirect" + + # 3. Router HTTPS (puerto 443): atiende el tráfico seguro + - "traefik.http.routers.${PROJECT_NAME}-https.rule=Host(\`${PROJECT_DOMAIN}\`)" + - "traefik.http.routers.${PROJECT_NAME}-https.entrypoints=websecure" + - "traefik.http.routers.${PROJECT_NAME}-https.tls=true" + - "traefik.http.routers.${PROJECT_NAME}-https.tls.certresolver=letsencrypt" + - "traefik.http.routers.${PROJECT_NAME}-https.service=${PROJECT_NAME}-svc" + + # 4. Servicio: puerto interno del contenedor + - "traefik.http.services.${PROJECT_NAME}-svc.loadbalancer.server.port=${INTERNAL_PORT}" + + # ====================================================================== + # MODO B — Dominio .local con certificado auto-firmado + # ====================================================================== + # Reemplazá los routers de arriba por estos, agregá el certificado en + # ~/srv/core/data/dynamic_tls.yml y quitá el certresolver (el certificado + # lo aporta el provider "file"). Ver docs/http-https.md + # + # - "traefik.http.routers.${PROJECT_NAME}.rule=Host(\`${PROJECT_DOMAIN}\`)" + # - "traefik.http.routers.${PROJECT_NAME}.entrypoints=websecure" + # - "traefik.http.routers.${PROJECT_NAME}.tls=true" + # - "traefik.http.services.${PROJECT_NAME}.loadbalancer.server.port=${INTERNAL_PORT}" + +# ============================================================================== +# Servicios internos (base de datos, colas, etc.) +# ============================================================================== +# IMPORTANTE: conectalos solo a la red "default" para que queden aislados +# del proxy inverso y no sean accesibles desde internet. +# +# db: +# image: postgres:14 +# container_name: ${PROJECT_NAME}-pg +# restart: unless-stopped +# networks: +# - default +# environment: +# POSTGRES_USER: \${POSTGRES_USER} +# POSTGRES_PASSWORD: \${POSTGRES_PASSWORD} +# POSTGRES_DB: \${POSTGRES_DB} +# volumes: +# - ./data/db:/var/lib/postgresql +# # Exponer solo en localhost, nunca público: +# ports: +# - "127.0.0.1:5432:5432" + +# Redes: default (aislada para servicios internos) y proxy_network (para el reverse proxy) +networks: + default: + driver: bridge + ${NETWORK_NAME}: + external: true +EOF + +echo -e "${GREEN}✓${NC} Estructura limpia creada en: ${BLUE}$TARGET_DIR${NC}" +echo -e " ├── ${BLUE}app/${NC} <- Pega aquí tu código, Dockerfile o clona tu repo" +echo -e " ├── ${BLUE}data/${NC} <- Directorio para volúmenes persistentes" +echo -e " ├── ${YELLOW}.env${NC} <- Variables de entorno" +echo -e " └── ${YELLOW}docker-compose.yml${NC}<- Orquestación enlazada al proxy" +echo "" +echo -e "${YELLOW}Próximos pasos:${NC}" +echo -e " 1. Copia tus archivos en: ${BLUE}$TARGET_DIR/app/${NC}" +echo -e " 2. Ajusta ${YELLOW}$TARGET_DIR/docker-compose.yml${NC} según tu imagen o build" +echo -e " 3. Levanta el proxy si aún no está: ${GREEN}cd $HOME/srv/core && docker compose up -d${NC}" +echo -e " 4. Inicia el proyecto: ${GREEN}cd $TARGET_DIR && docker compose up -d${NC}"