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
This commit is contained in:
Marcos Elias Rios Nuñez 2026-10-15 11:34:39 -03:00
commit 33c368936e
10 changed files with 1312 additions and 0 deletions

13
.env.global Normal file
View File

@ -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/<proyecto>/.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

46
.gitignore vendored Normal file
View File

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

404
README.md Normal file
View File

@ -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/<proyecto>/` 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
│ └── <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 <cont> 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/<proyecto>/` 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.

110
core/data/certs/README.md Normal file
View File

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

19
core/data/dynamic_tls.yml Normal file
View File

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

18
core/data/traefik.yml Normal file
View File

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

23
core/docker-compose.yml Normal file
View File

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

245
docs/arquitectura.md Normal file
View File

@ -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://<IP-SERVIDOR>: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: <proyecto>: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 <proyecto>-web composer install --no-interaction --prefer-dist
docker exec -it <proyecto>-web php artisan key:generate --force
docker exec -it <proyecto>-web php artisan migrate --force
docker exec -it <proyecto>-web php artisan optimize:clear
```
Verificar permisos si el usuario `www-data` no puede escribir:
```bash
docker exec -it <proyecto>-web chown -R www-data:www-data /var/www/<app>/storage
docker exec -it <proyecto>-web chmod -R 775 /var/www/<app>/storage /var/www/<app>/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 <cont-pg> pg_dump -U <user> <db> > ~/srv/backups/<proyecto>-$(date +%F).sql
# MySQL
docker exec <cont-mysql> mysqldump -u<user> -p <db> > ~/srv/backups/<proyecto>-$(date +%F).sql
# Archivos
tar czf ~/srv/backups/<proyecto>-files-$(date +%F).tar.gz ~/srv/projects/<proyecto>/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>/` |
| Nombre del contenedor | `<nombre>-web` |
| Red del proxy | `proxy_network` (externa) |
| Red interna | `default` (bridge, aislada) |
| Secretos | `~/srv/projects/<nombre>/.env` con `chmod 600` |
| Dominios locales | `<proyecto>.local` |
| Certificados `.local` | `~/srv/core/data/certs/` + `dynamic_tls.yml` |
| Certificados públicos | Let's Encrypt vía `acme.json` |

247
docs/http-https.md Normal file
View File

@ -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.<scope>.<component>.<atributo>=<valor>
│ │ │
│ │ └─ 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>` | 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 |

187
init_project.sh Executable file
View File

@ -0,0 +1,187 @@
#!/usr/bin/env bash
# ==============================================================================
# Script de aprovisionamiento de proyectos Docker (Estructura Limpia)
# ==============================================================================
# Uso: ./init_project.sh <nombre-del-proyecto> <dominio-o-url> [puerto-interno]
#
# Genera en $SRV_DIR/projects/<proyecto> 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 <nombre-del-proyecto> <dominio-o-url> [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 <<EOF > "$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 <<EOF > "$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}"