🌐 Gestión de Subdominios
Despliegue completo de aplicaciones en subdominios con Docker, Nginx y SSL.
Índice
- Caso de Estudio: labs.josejordan.dev
- Flujo de Trabajo Completo
- Replicar para Otros Subdominios
- Gestión de Aplicaciones
- Script de Automatización
Caso de Estudio: labs.josejordan.dev
Este es el primer subdominio completamente funcional. Vamos a ver el stack completo.
Arquitectura
Internet
↓
Cloudflare DNS (proxy naranja)
↓
Hetzner Cloud Firewall (80, 443)
↓
VPS Ubuntu 24.04
↓
UFW Firewall
↓
Nginx (puerto 443) + SSL (Let's Encrypt)
↓
Proxy a localhost:3000
↓
Contenedor Docker (nginxdemos/hello)
Flujo de Trabajo Completo
Paso 1: Configurar DNS en Cloudflare
- Ve a Cloudflare Dashboard
- Selecciona tu dominio (ej:
josejordan.dev) - Ve a DNS → Records
- Click en Add record
- Configura:
- Type: A
- Name: labs (se convertirá en
labs.josejordan.dev) - IPv4 address: YOUR_SERVER_IP
- Proxy status: ☁️ Activado (naranja)
- TTL: Auto
- Click en Save
Verificar propagación DNS:
# Desde tu ordenador o el VPS
ping labs.josejordan.dev
# Debería resolver a tu IP del VPS (o IP de Cloudflare si proxy está activado)
Paso 2: Crear aplicación con Docker Compose
# Crear directorio para la aplicación
mkdir -p ~/apps/labs && cd ~/apps/labs
# Crear docker-compose.yml
cat > docker-compose.yml <<'YAML'
services:
web:
image: nginxdemos/hello
restart: unless-stopped
ports: ["3000:80"]
YAML
# Levantar el contenedor
docker compose up -d
# Verificar que está corriendo
docker ps
# Probar localmente
curl http://localhost:3000
Deberías ver HTML del contenedor.
Paso 3: Configurar Nginx como Proxy Reverso
IMPORTANTE: Primero, deshabilitar el sitio default:
Crear configuración para el subdominio (solo HTTP inicialmente):
sudo tee /etc/nginx/sites-available/labs >/dev/null <<'NGINX'
server {
listen 80;
listen [::]:80;
server_name labs.josejordan.dev;
# Si usas Cloudflare:
real_ip_header CF-Connecting-IP;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
NGINX
# Habilitar el sitio
sudo ln -s /etc/nginx/sites-available/labs /etc/nginx/sites-enabled/labs
# Verificar configuración
sudo nginx -t
# Recargar Nginx
sudo systemctl reload nginx
Probar que funciona:
# Desde el VPS
curl -H "Host: labs.josejordan.dev" http://localhost
# Deberías ver el HTML del contenedor (no "Welcome to nginx!")
Paso 4: Configurar HTTPS con Certbot
Obtener certificado SSL automático:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cf.ini \
--cert-name labs.josejordan.dev \
-d labs.josejordan.dev
Actualizar configuración de Nginx con HTTPS:
sudo tee /etc/nginx/sites-available/labs >/dev/null <<'NGINX'
# HTTP -> redirige a HTTPS
server {
listen 80;
listen [::]:80;
server_name labs.josejordan.dev;
return 301 https://$host$request_uri;
}
# HTTPS + proxy
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name labs.josejordan.dev;
# Certbot (DNS-01)
ssl_certificate /etc/letsencrypt/live/labs.josejordan.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/labs.josejordan.dev/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# IP real detrás de Cloudflare
real_ip_header CF-Connecting-IP;
# Forzar no-caché y anular headers del upstream
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
# Reverse proxy
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Cabeceras de seguridad
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
NGINX
# Verificar y recargar
sudo nginx -t && sudo systemctl reload nginx
Paso 5: Configurar Cloudflare
SSL/TLS: - Modo: Full (strict)
Cache Rule (para contenido dinámico):
1. Ve a: Caching → Cache Rules → Create Rule
2. Nombre: Bypass cache - labs.josejordan.dev
3. Cuando: http.host eq "labs.josejordan.dev"
4. Acción: Bypass cache
5. Guardar
Workers & Pages:
- ⚠️ NO usar rutas comodín tipo *.josejordan.dev/*
- Solo usar rutas específicas como josejordan.dev/*
Paso 6: Verificar Funcionamiento
# Ver configuración de Nginx
sudo nginx -t
# Ver certificados instalados
sudo certbot certificates
# Ver puertos escuchando
sudo ss -tulpn | grep -E ':80|:443'
# Ver logs de Nginx
sudo tail -f /var/log/nginx/access.log
Probar en navegador:
Deberías ver: - ✅ Candado verde (HTTPS seguro) 🔒 - ✅ Página de demo de Nginx - ✅ Información del contenedor Docker
Verificar caché:
# A través de Cloudflare
curl -sS -D - -o /dev/null "https://labs.josejordan.dev/?_bust=$(date +%s)"
# Debe mostrar: cf-cache-status: DYNAMIC o MISS (no HIT)
Replicar para Otros Subdominios
Template rápido
Para crear api.josejordan.dev que corre en el puerto 3001:
1. DNS en Cloudflare:
2. Aplicación Docker:
mkdir -p ~/apps/api && cd ~/apps/api
cat > docker-compose.yml <<'YAML'
services:
api:
image: tu-imagen:tag
restart: unless-stopped
ports: ["3001:8080"] # 3001 externo, 8080 interno del contenedor
environment:
- NODE_ENV=production
YAML
docker compose up -d
3. Certificado SSL:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cf.ini \
--cert-name api.josejordan.dev \
-d api.josejordan.dev
4. Nginx:
sudo tee /etc/nginx/sites-available/api >/dev/null <<'NGINX'
server {
listen 80;
listen [::]:80;
server_name api.josejordan.dev;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name api.josejordan.dev;
ssl_certificate /etc/letsencrypt/live/api.josejordan.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.josejordan.dev/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
real_ip_header CF-Connecting-IP;
location / {
proxy_pass http://127.0.0.1:3001; # Cambiar al puerto correcto
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
NGINX
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
5. Cache Rule en Cloudflare:
6. Probar:
Gestión de Aplicaciones
Ver aplicaciones activas
# Ver todos los contenedores
docker ps
# Ver por directorio
ls -la ~/apps/
# Ver qué puertos están ocupados
sudo ss -tulpn | grep LISTEN
Ver logs de una aplicación
Reiniciar una aplicación
Actualizar una aplicación
cd ~/apps/labs
# Descargar nueva versión de la imagen
docker compose pull
# Recrear contenedores con nueva imagen
docker compose up -d
Parar una aplicación
cd ~/apps/labs
# Parar sin eliminar
docker compose stop
# Parar y eliminar contenedores
docker compose down
# Parar, eliminar contenedores y volúmenes
docker compose down -v
Ver uso de recursos
Script de Automatización
Puedes usar el script scripts/new-subdomain.sh para automatizar el proceso.
Uso:
# Crear nuevo subdominio
./scripts/new-subdomain.sh nombre-subdominio 3002
# Ejemplo: blog.josejordan.dev en puerto 3005
./scripts/new-subdomain.sh blog 3005
El script hará:
1. ✅ Crear directorio en ~/apps/nombre-subdominio
2. ✅ Generar docker-compose.yml básico
3. ✅ Generar configuración de Nginx
4. ✅ Habilitar sitio en Nginx
5. ✅ Generar certificado SSL
6. ✅ Recargar Nginx
7. ✅ Mostrar instrucciones finales
Aún debes hacer manualmente:
- Configurar DNS en Cloudflare
- Personalizar docker-compose.yml con tu aplicación
- Configurar Cache Rule en Cloudflare (si es necesario)
Subdominios Activos
labs.josejordan.dev ✅
- Estado: Activo
- Puerto: 3000
- Imagen: nginxdemos/hello
- SSL: Let's Encrypt (DNS-01)
- Ruta:
~/apps/labs
Buenas Prácticas
Estructura consistente
~/apps/
├── labs/
│ ├── docker-compose.yml
│ ├── .env # Variables de entorno
│ └── data/ # Volúmenes persistentes
├── api/
│ ├── docker-compose.yml
│ └── logs/
└── blog/
└── docker-compose.yml
Usar .env para configuración
Restart policies
Siempre configurar restart para que los contenedores se reinicien tras un reinicio del VPS:
Backup de configuraciones
# Backup de configuraciones de Nginx
sudo tar -czf nginx-backup-$(date +%Y%m%d).tar.gz /etc/nginx/sites-available/
# Backup de docker-compose files
tar -czf apps-backup-$(date +%Y%m%d).tar.gz ~/apps/
Logs rotativos
Docker ya rota logs automáticamente, pero puedes configurar límites:
services:
web:
image: nginx:alpine
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Health checks
services:
web:
image: nginx:alpine
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Checklist para Nuevo Subdominio
- [ ] Registro DNS A en Cloudflare
- [ ] Proxy naranja activado
- [ ] Directorio creado en
~/apps/nombre-app - [ ]
docker-compose.ymlconfigurado - [ ] Contenedor levantado (
docker compose up -d) - [ ] Certificado SSL emitido
- [ ] Configuración de Nginx creada
- [ ] Sitio habilitado en Nginx
- [ ] Nginx recargado
- [ ] Cache Rule configurada (si es dinámico)
- [ ] Probado en navegador (HTTPS + candado verde)
- [ ] Verificado bypass de caché (si aplica)
Troubleshooting
Subdominio no carga
# 1. Verificar DNS
ping labs.josejordan.dev
# 2. Verificar contenedor
docker ps | grep labs
# 3. Verificar Nginx
sudo nginx -t
sudo systemctl status nginx
# 4. Ver logs
sudo tail -f /var/log/nginx/error.log
Certificado SSL no funciona
# Verificar que existe
sudo certbot certificates
# Ver configuración Nginx
sudo cat /etc/nginx/sites-available/labs
# Verificar que Nginx encuentra los archivos
sudo ls -la /etc/letsencrypt/live/labs.josejordan.dev/
Veo contenido antiguo (caché)
# Purgar caché en Cloudflare
# Dashboard → Caching → Purge Cache → Custom → Hostname: labs.josejordan.dev
# Verificar headers
curl -I https://labs.josejordan.dev
Puerto ya en uso
# Ver qué está usando el puerto
sudo ss -tulpn | grep :3000
# Cambiar puerto en docker-compose.yml o parar el otro contenedor