HTTPS para varias apps en un solo servidor
Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta
Un solo VPS es más que suficiente para varias apps pequeñas: un blog, una API interna, una página de estado, ninguna necesita un servidor para ella sola. Lo que sí necesitan es que cada una responda en HTTPS en su propio subdominio, con un certificado que de verdad sea válido y de verdad se renueve, sin que tengas que correr certbot renew a mano cada cierto tiempo o escribir una configuración de nginx y una tarea cron por separado para cada sitio. Una sola instancia de Caddy al frente de todo hace esto con mucha menos configuración de la que esperarías, emitiendo y renovando un certificado por dominio desde un solo archivo.
Esta guía configura tres apps detrás de una instancia de Caddy en un solo servidor, cada una accesible en su propio subdominio con el HTTPS manejado automáticamente, y después cubre lo que Railyard automatiza sobre el mismo problema, de modo que no quede ningún Caddyfile que mantener.
Lo que necesitas
- Un VPS con al menos 2 GB de RAM. Un droplet Basic de DigitalOcean de 2 GB ($12 USD/mes) o un Hetzner CX23 (2 vCPU, 4 GB de RAM, €5.99/mes sin IVA) tiene espacio de sobra para varios contenedores pequeños más Caddy.
- Un dominio que controles, con acceso para agregar registros DNS.
- Docker y el plugin de Compose instalados en el servidor.
- Los puertos 80 y 443 abiertos y que no estén ya en uso por otro servidor web.
Paso 1: apuntar el DNS al servidor antes que nada
Caddy pide un certificado la primera vez que cada dominio se solicita por HTTPS, y Let’s Encrypt valida esa solicitud comprobando que el dominio realmente resuelve hacia este servidor. Crea un registro A por subdominio ahora mismo, para que tenga tiempo de propagarse antes de que arranques Caddy:
blog.example.com. A 203.0.113.10
api.example.com. A 203.0.113.10
status.example.com. A 203.0.113.10
Los tres pueden apuntar a la misma IP; Caddy los distingue por el encabezado Host de la petición, igual que cualquier reverse proxy.
Paso 2: organizar las apps en contenedores separados
Este ejemplo corre tres cosas: una app de blog y una API, cada una su propio contenedor pequeño, y una página de estado servida como simples archivos estáticos sin backend alguno. Cada app vive en su propio directorio con su propio Dockerfile:
.
├── blog/
│ └── Dockerfile
├── api/
│ └── Dockerfile
├── status/
│ └── index.html
├── docker-compose.yml
└── Caddyfile
Nada de esto depende de lo que realmente haya dentro de blog o api; una app Django de una guía anterior, una API en Node, o cualquier otra cosa que escuche en un puerto dentro de su contenedor funciona igual. Lo único que le importa a Caddy es que cada contenedor exponga un puerto en la red de Docker.
Paso 3: escribir el Caddyfile
{
email ops@example.com
}
blog.example.com {
reverse_proxy blog:8000
}
api.example.com {
reverse_proxy api:8001
}
status.example.com {
root * /srv/status
file_server
}
Cada dominio tiene su propio bloque. blog.example.com y api.example.com redirigen a sus contenedores por el nombre del servicio de Docker Compose; Caddy resuelve ese nombre en la red interna igual que resolvería cualquier otro nombre de host. status.example.com no tiene backend alguno, Caddy simplemente sirve archivos directo desde disco con file_server. El bloque email de arriba es lo que Let’s Encrypt usa para avisarte si un certificado está por fallar al renovarse; es opcional pero vale la pena configurarlo.
Valida un Caddyfile antes de confiar en él en producción:
docker run --rm -v "$(pwd)/Caddyfile:/etc/caddy/Caddyfile" caddy:2 caddy validate --config /etc/caddy/Caddyfile
Deberías ver “Valid configuration” impreso, no un error de sintaxis.
Paso 4: escribir docker-compose.yml
services:
blog:
build: ./blog
restart: unless-stopped
expose:
- "8000"
api:
build: ./api
restart: unless-stopped
expose:
- "8001"
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./status:/srv/status:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- blog
- api
volumes:
caddy_data:
caddy_config:
Solo caddy publica los puertos 80 y 443 hacia el host; blog y api usan expose, lo que hace que sus puertos sean alcanzables desde otros contenedores de la misma red de Compose pero no desde fuera del servidor en absoluto. Ese es justamente el punto de poner todo detrás de un solo proxy: los contenedores de las apps nunca son alcanzables directamente, solo a través de Caddy, y solo en el dominio que se supone que deben responder. El volumen caddy_data es lo que hace que los certificados sobrevivan a un reinicio; perderlo significa que Caddy pide unos nuevos a Let’s Encrypt la próxima vez que arranca, lo cual es seguro pero cuenta contra los límites de tasa de Let’s Encrypt si pasa seguido.
Paso 5: arrancar y revisar los certificados
docker compose up -d --build
docker compose logs -f caddy
Deberías ver una línea por cada dominio diciendo que se obtuvo un certificado, algo como “certificate obtained successfully” con el nombre del dominio al lado. Confirma desde fuera del servidor:
curl -I https://blog.example.com
curl -I https://api.example.com
curl -I https://status.example.com
Cada una debería devolver una respuesta sin advertencia de certificado, y curl -v mostraría un certificado emitido por Let’s Encrypt para ese nombre de host exacto.
Paso 6: agregar una cuarta app después, sin tiempo de inactividad
Agrega un nuevo bloque al Caddyfile, un nuevo servicio a docker-compose.yml, crea su registro DNS, y luego dile al contenedor de Caddy que ya está corriendo que tome el cambio sin reiniciar nada más:
docker compose up -d newapp
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
caddy reload vuelve a leer la configuración y solo toca lo que cambió; los certificados y conexiones de los otros tres dominios no se ven afectados.
Respaldos y actualizaciones
Lo único que vale la pena respaldar aquí es el volumen caddy_data (los certificados, que de todas formas se regeneran) y lo que cada app guarde por su cuenta, como un volumen de base de datos si alguna lo tiene. Respáldalos de la misma forma que lo harías con cualquier app: un dump programado o una instantánea del volumen copiada fuera del servidor.
Para actualizar Caddy mismo, trae la nueva etiqueta de imagen y recrea solo ese contenedor:
docker compose pull caddy
docker compose up -d caddy
Los certificados existentes en caddy_data se conservan; Caddy no necesita volver a emitir nada solo porque el binario cambió de versión.
El atajo: hacerlo con Railyard
Todo lo anterior (el certificado por dominio, el reverse proxy, la verificación de propiedad que evita que alguien más apunte un dominio que no controla hacia tu servidor) es lo que Railyard automatiza por app en lugar de un Caddyfile compartido. Cada app obtiene https://<nombre>.<ip-del-servidor>.sslip.io con certificado desde el momento en que se despliega, sin que haga falta todavía ningún registro DNS. Agregar tu propio dominio significa abrir la pestaña Domains de la app, agregar el nombre de host, y crear el registro A o CNAME que te muestra; un registro TXT (_railyard-verify.<dominio>) prueba que controlas el dominio antes de que arranquen el certificado o el enrutamiento, verificado automáticamente cada 30 segundos. Correr varias apps en el mismo servidor no cambia nada de esto: cada una se enruta y certifica de forma independiente, igual que el Caddyfile de arriba maneja tres dominios en una sola configuración, solo que sin que tengas que escribir o recargar esa configuración tú mismo.
Para un dominio comodín (*.example.com enrutando cada subdominio hacia una app) o muchos subdominios que de otro modo significarían un certificado por cada visita, Networking cubre las dos opciones: TLS bajo demanda (un certificado emitido la primera vez que se visita cada subdominio, el comportamiento por defecto) o un solo certificado para todo el comodín mediante un desafío DNS-01, usando un token de API de Cloudflare o una llave de acceso de Route 53 agregada en Team settings → DNS credentials.
Solución de problemas
El puerto 80 o 443 ya está en uso. Algo más (a menudo un nginx o Apache instalado por la imagen del servidor, o un resto de una configuración anterior) ya está enlazado a él. Encuéntralo con ss -tlnp | grep -E ':80|:443' y detenlo antes de arrancar Caddy.
Un certificado nunca se emite para un dominio pero los demás funcionan bien. Revisa el DNS de ese dominio específico por separado: dig blog.example.com. Un error de tipeo en un registro A es la causa más común, y solo rompe ese dominio porque Caddy pide los certificados de forma independiente por bloque de sitio.
“too many certificates already issued” de Let’s Encrypt. Es un límite de tasa (actualmente 5 certificados idénticos por dominio exacto por semana), que normalmente se alcanza destruyendo y recreando el volumen caddy_data repetidamente durante pruebas. Mantén el volumen entre reinicios, y usa la autoridad de certificación interna de Caddy (tls internal en el bloque del Caddyfile) mientras ajustas la configuración, pasando a certificados reales una vez que funcione.
Errores de permisos en la página de estado o en un volumen montado. file_server necesita acceso de lectura a lo que esté montado en su raíz; un volumen creado por otro contenedor que corre con un usuario distinto suele ser la causa. Corrige la propiedad en el host, o móntalo :ro como en el ejemplo de arriba si nada necesita escribir ahí.
El servidor se queda sin memoria con varios contenedores más Caddy. Cada contenedor pequeño de app suele estar bien, pero revisa el uso real con docker stats antes de asumir que es Caddy; un contenedor de base de datos es un sospechoso más probable que el proxy en sí. Sube de tamaño, o mueve la app más pesada a su propio servidor y sigue apuntando su subdominio hacia donde realmente corre.
Preguntas frecuentes
¿Necesito Docker para esto, o funciona con un Caddy instalado a nivel de sistema? El patrón es idéntico en ambos casos: un Caddyfile con un bloque por dominio, cada uno apuntando a un puerto local en vez de a un nombre de servicio de Docker. systemd gestiona el proceso de Caddy en lugar de Compose.
¿Todas las apps tienen que estar en el mismo servidor? Para esta guía, sí, ya que el punto es tener una sola instancia de Caddy compartida. Nada te impide correr Caddy en un servidor y redirigir hacia apps en otros a través de una red privada, pero en ese punto estás construyendo el mismo tipo de enrutamiento que una plataforma como Railyard ya maneja.
¿Qué pasa si agrego cien subdominios? El TLS bajo demanda emite un certificado por subdominio la primera vez que se visita, lo cual funciona pero significa cien certificados de Let’s Encrypt distintos que rastrear. Un certificado comodín mediante DNS-01 los cubre todos con uno solo, la mejor opción pasando de un puñado de subdominios, ya sea en un Caddyfile escrito a mano o mediante la opción de credenciales DNS propia de Railyard.
¿Esto redirige el HTTP a HTTPS automáticamente? Sí, Caddy lo hace por defecto para cada dominio del Caddyfile sin necesidad de configuración extra.
Tres dominios detrás de una instancia de Caddy y treinta detrás de una sola no son configuraciones realmente distintas, el Caddyfile solo se vuelve más largo. Lo que vale la pena interiorizar es que un reverse proxy que emite sus propios certificados por dominio es más simple de operar que una tarea cron de certbot y una configuración de nginx por sitio, ya sea que escribas ese Caddyfile tú mismo o dejes que otra cosa lo escriba por ti.
Por qué eligen Railyard para esto
- Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
- Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
- Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
- Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
- Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.