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:
commit
33c368936e
13
.env.global
Normal file
13
.env.global
Normal 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
46
.gitignore
vendored
Normal 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
404
README.md
Normal 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
110
core/data/certs/README.md
Normal 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
19
core/data/dynamic_tls.yml
Normal 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
18
core/data/traefik.yml
Normal 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
23
core/docker-compose.yml
Normal 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
245
docs/arquitectura.md
Normal 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
247
docs/http-https.md
Normal 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
187
init_project.sh
Executable 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}"
|
||||||
Loading…
Reference in New Issue
Block a user