Saltar a contenido

🌐 Gestión de Subdominios

Despliegue completo de aplicaciones en subdominios con Docker, Nginx y SSL.


Índice


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

  1. Ve a Cloudflare Dashboard
  2. Selecciona tu dominio (ej: josejordan.dev)
  3. Ve a DNSRecords
  4. Click en Add record
  5. Configura:
  6. Type: A
  7. Name: labs (se convertirá en labs.josejordan.dev)
  8. IPv4 address: YOUR_SERVER_IP
  9. Proxy status: ☁️ Activado (naranja)
  10. TTL: Auto
  11. 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:

sudo rm /etc/nginx/sites-enabled/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:

https://labs.josejordan.dev

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:

Crear registro A: api → YOUR_SERVER_IP (proxy naranja)

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:

Bypass cache para api.josejordan.dev

6. Probar:

curl -I https://api.josejordan.dev


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

cd ~/apps/labs
docker compose logs -f

Reiniciar una aplicación

cd ~/apps/labs
docker compose restart

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

# Todos los contenedores
docker stats

# Contenedor específico
docker stats labs-web-1

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

# docker-compose.yml
services:
  web:
    image: ${IMAGE_NAME}:${IMAGE_TAG}
    ports:
      - "${PORT}:80"
# .env
IMAGE_NAME=nginx
IMAGE_TAG=alpine
PORT=3000

Restart policies

Siempre configurar restart para que los contenedores se reinicien tras un reinicio del VPS:

services:
  web:
    image: nginx:alpine
    restart: unless-stopped  # ← Importante

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.yml configurado
  • [ ] 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

⬅️ Anterior: SSL y Cloudflare | Volver al índice