Todos los recursos

No te hace falta una plataforma gestionada por cada servicio para llevar una operación pequeña en serio. Un solo VPS con Docker Compose y un reverse proxy Traefik corre varias apps sin enredos: HTTPS automático, ruteo por hostname, sin un panel de control por servicio, por lo que cuesta una suscripción de café. Esta guía te lleva por el montaje completo, un ejemplo real y el fallo que de verdad te golpea: los backups.

Autoaloja todo tu stack en un solo VPS

En resumen

  • Una máquina, tres capas: un host Linux endurecido, Docker Compose para definir cada servicio y Traefik al frente para el ruteo más TLS automático con Let's Encrypt.
  • Sumas una app poniéndole labels a su contenedor, no tocando la config del proxy. Traefik vigila el socket de Docker y arma las rutas al vuelo.
  • Blinda antes de exponer: un usuario no-root, SSH solo con llave, un firewall que deje pasar únicamente 80/443/SSH y un archivo env que nunca llega a git.
  • Una sola máquina es un solo punto de fallo. Los backups fuera de la máquina, automáticos y con restore probado son la parte que de verdad no se negocia. Que "esté corriendo" no es tener un backup.
  • Así es como Ilustrari corre su propio stack. Aguanta sorprendentemente bien antes de que te haga falta una segunda máquina. El techo real es tu disciplina de backup/restore, no el CPU.

La mayoría de los proyectos pequeños recurren a una plataforma gestionada por cada servicio, una para la app, otra para la base de datos, otra para la cola, y acaban pagando precios de plataforma y juntando tres dashboards antes de tener un solo usuario. Casi nunca te hace falta nada de eso. Un solo VPS corriendo Docker Compose detrás de un reverse proxy Traefik te aloja varias apps con HTTPS automático, rutea cada una a su propio subdominio y renueva los certificados sin que tengas que pensar en ello. Esta guía te da el montaje completo de punta a punta: el endurecimiento del host, que va primero; la estructura de Compose más Traefik; un ejemplo real de cómo sumar una app de verdad; y los hábitos operativos, los backups por encima de todo, que deciden si esto es un montaje tranquilo o un incidente a las 2 de la mañana. Así es como Ilustrari corre su propio stack hoy.

Nota

Esto es una concesión deliberada, no una respuesta universal. Una sola máquina significa un solo radio de impacto: si se cae, todo lo que vive en ella queda fuera hasta que restaures. Es un riesgo aceptable para una operación de una persona o un producto pequeño si, y solo si, tus backups están fuera de la máquina y de verdad probaste un restore. Si corres algo donde unos minutos de downtime cuestan dinero de verdad, esta guía te sirve para staging, no para producción.

01 · Prerrequisitos: blinda el host antes que nada

No te brinques esta sección para llegar a la parte divertida. A un VPS con IP pública lo empiezan a sondear a los pocos minutos de arrancar, y la diferencia entre "todo bien" y "comprometido" es la media hora que le metes al principio.

Necesitas:

  • Un VPS con al menos 2 vCPU y 4 GB de RAM para correr cómodo un proxy más unos cuantos contenedores de app y una base de datos pequeña. Con 2 GB te alcanza para un par de servicios livianos. Por debajo de eso te toca pelear con el OOM killer.
  • Un dominio con DNS que controles tú, para apuntar subdominios a la máquina.
  • Acceso SSH por llave. Nunca auth por contraseña en un host público.

El checklist del primer login, en orden:

  1. Crea un usuario no-root y dale sudo. Trabaja con ese usuario, y deja root para cuando de verdad haga falta.
  2. SSH solo con llave. Sube tu llave pública y luego, en la config del demonio SSH, pon PasswordAuthentication no y PermitRootLogin no, y recarga SSH. Confirma que todavía puedes entrar desde una segunda terminal antes de cerrar la primera. Dejarte por fuera de tu propia máquina es el error clásico que uno mismo se provoca.
  3. Un firewall que por defecto niega todo. Deja pasar solo la entrada por 22 (SSH), 80 y 443. Todo lo demás, incluido el puerto de tu base de datos, queda cerrado al mundo y se alcanza solo por la red de Docker o un túnel SSH.
  4. Actualizaciones de seguridad automáticas del sistema, para que no seas tú la razón de que un CVE viejo siga abierto.
  5. Instala Docker y el plugin de Compose desde el repo oficial de Docker, no del paquete viejo de la distro. Mete tu usuario al grupo docker para no tener que usar sudo en cada comando.

Atención

Meter un usuario al grupo docker equivale prácticamente a darle root, porque cualquiera en ese grupo puede montar el filesystem del host dentro de un contenedor. Para la única persona que administra la máquina está bien. Nunca metas una aplicación ni un runner de CI en el grupo docker. Si esa cuenta se compromete, el atacante se queda con toda la máquina, no con un solo contenedor.

02 · La estructura: Compose define los servicios, Traefik los pone al frente

La arquitectura son tres capas, y vale la pena tenerla clara en la cabeza antes de escribir una sola línea de YAML.

  • Docker Compose es tu fuente de verdad. Cada servicio, cada app, la base de datos, el propio proxy, es un bloque en un archivo Compose con una imagen fijada, un volumen con nombre para todo lo que tenga que sobrevivir a un reinicio y un lugar en una red de Docker compartida.
  • Traefik es el único punto de entrada público. Escucha en 80 y 443, vigila el socket de Docker y descubre los servicios solo a partir de sus labels. Nunca editas a mano un archivo de rutas. Describes el ruteo en el contenedor al que le toca.
  • Una red compartida deja que Traefik alcance cada app por su nombre de servicio, mientras que las apps en sí no quedan publicadas al host. Solo Traefik abre puertos públicos.

El modelo mental que hace que todo encaje: Traefik se configura solo a partir de los labels. Sumar una app es cuestión de ponerle labels, no de editar el proxy. Esa es justamente la razón por la que esto se mantiene manejable cuando sumas el tercer, cuarto y quinto servicio.

Dos tipos de configuración

Traefik separa la config en estática (se define una sola vez al arrancar, como entrypoints, el resolver de certificados, qué providers vigilar) y dinámica (se descubre de forma continua, como tus routers y services, leídos de los labels de los contenedores). Confundir las dos es el tropiezo más común con Traefik: un ajuste que va en la config estática no hace nada, sin avisar, cuando lo metes en un label, y al revés.

03 · Levanta Traefik con TLS automático

Aquí va el servicio del proxy. La config estática la dejo en flags de línea de comandos para que se vea todo de una vez. Más adelante puedes moverla a un archivo. Las piezas clave son los dos entrypoints, la redirección de HTTP a HTTPS y el resolver de Let's Encrypt que guarda los certificados en un volumen montado.

services:
  traefik:
    image: traefik:v3.3
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      # Forzar HTTP hacia HTTPS en cada router
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      # Let's Encrypt vía challenge HTTP-01
      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.le.acme.email=tu@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./letsencrypt:/letsencrypt
      # Socket en solo lectura: Traefik lee los labels pero no controla Docker
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - edge

networks:
  edge:
    name: edge

Aquí van varias decisiones que quedan dentro y que vale la pena entender en vez de copiar a ciegas:

  • exposedbydefault=false significa que un contenedor es invisible para Traefik hasta que lo activas con traefik.enable=true. Es el default seguro. Nunca publicas por accidente un servicio que se te quedó olvidado.
  • El socket de Docker se monta en solo lectura. Traefik solo necesita leer la metadata de los contenedores. Un socket en solo lectura limita el daño si algún día vulneran a Traefik. Aun así da acceso de peso, así que trata al proxy como un componente de confianza.
  • acme.json es un volumen, no un archivo que subes al repo. Guarda tus llaves privadas y tus certificados. Respáldalo, nunca lo subas y asegúrate de que tenga permisos estrictos (el storage de Let's Encrypt tiene que estar en modo 600 o Traefik se niega a usarlo).

Importante

Usa el entorno staging de Let's Encrypt mientras armas el DNS y los labels. Producción tiene rate limits estrictos, y un loop mal configurado puede dejarte el dominio limitado por una semana. Agrega el flag del servidor CA de staging, confirma que los certificados se emiten de punta a punta y luego quítalo para que Traefik emita los certificados reales una sola vez.

04 · Suma una app: ruteo por label

Ahora la recompensa. Aquí va una app real, un servicio Next.js del tipo que corre Ilustrari, conectada a la misma red y expuesta a través de Traefik solo con labels. Fíjate que no publica ningún puerto al host. El único público es Traefik.

  app:
    image: ghcr.io/me/my-next-app:1.4.0
    restart: unless-stopped
    env_file: .env.app
    networks:
      - edge
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`app.example.com`)
      - traefik.http.routers.app.entrypoints=websecure
      - traefik.http.routers.app.tls.certresolver=le
      # Indícale a Traefik en qué puerto interno escucha la app
      - traefik.http.services.app.loadbalancer.server.port=3000

Lee los labels como si fueran una frase: activa Traefik para este contenedor; rutea los requests cuyo Host sea app.example.com hacia un router llamado app; sirve ese router en el entrypoint de HTTPS; saca su certificado del resolver le; y reenvía el tráfico al puerto 3000 dentro del contenedor. Apunta un registro A de app.example.com al VPS, corre docker compose up -d y en segundos tienes un sitio HTTPS funcionando con un certificado real.

Sumar una segunda app son esos mismos seis labels, cambiando app por un nuevo nombre de router y un nuevo hostname. Esa repetición es la gracia. No hay un archivo central que vaya creciendo, no hay merge conflicts en una config de rutas, no hay que reiniciar el proxy. Cada servicio carga su propio ruteo.

Fíjalo todo

  • Fija los tags de imagen a versiones concretas, nunca latest. Una actualización sorpresa en un redeploy es justo lo que rompe en el peor momento una máquina que funcionaba. Lo que quieres es que los deploys sean aburridos y deliberados.
  • Fija también la versión mayor de Traefik. v2 y v3 tienen una config bastante distinta, así que un proxy sin fijar que salta de versión mayor en un rebuild te tumba todas las rutas de un solo golpe.

05 · Secretos y las cosas que te muerden

Dos hábitos operativos separan un montaje en el que puedes dormir tranquilo de otro que va acumulando riesgo en silencio.

Mantén los secretos fuera de Compose

Nunca pongas credenciales inline en el archivo Compose. Es el archivo con más probabilidades de terminar en git. Usa un env_file por servicio (el .env.app de arriba), mantén esos archivos fuera del control de versiones con una entrada en gitignore y déjalos en modo 600 para que solo el dueño pueda leerlos. Para una máquina de un solo operador con eso basta. Si más adelante corres cargas no confiables o tienes varios admins, pasa a Docker secrets o a un vault externo en vez de archivos env.

Consejo

Mantén un .env.example versionado con todas las claves presentes pero con los valores en blanco. Documenta lo que necesita cada servicio sin filtrar nada, y convierte el "¿qué env vars espera esto?" de una sesión de arqueología en leer un solo archivo. El .env.app de verdad se queda local y sin versionar.

La base de datos es donde la gente se quema

Es tentador soltar un contenedor de Postgres en el mismo stack de Compose. Para un proyecto pequeño está perfectamente bien, pero solo con un volumen con nombre y un procedimiento de restore probado. Dale a la base de datos un volumen con nombre para que sus datos sobrevivan a recrear el contenedor, y nunca expongas su puerto al host. Alcánzala por la red de Docker usando su nombre de servicio. La trampa no es correr la base de datos. Es dar por hecho que una base de datos que corre es una base de datos segura.

06 · Backups: la única parte que no se negocia

Una sola máquina es un solo punto de fallo. El host se puede caer, un disco puede fallar, una migración mala puede corromper datos, o puedes meter la pata con un comando destructivo. Los backups son lo que hace que todo eso pase de "se acabó la empresa" a "una tarde fastidiosa". Esta es la sección que la gente lee por encima y después lamenta.

Haz estas cuatro cosas:

  1. Haz dump de la base de datos en un horario fijo, no solo un snapshot del volumen. Un dump lógico (pg_dump para Postgres) se puede restaurar entre versiones y pesa mucho menos que copiar el volumen crudo. Córrelo desde cron, varias veces al día si los datos lo ameritan.
  2. Respalda también los volúmenes con estado y acme.json, para que un rebuild completo restaure los certificados y los archivos subidos, no solo la base de datos.
  3. Saca los backups de la máquina. Un backup en el mismo disco que los datos se muere junto con los datos. Súbelos a un object storage o a otro host. Encríptalos, porque contienen todo lo sensible que tienes.
  4. Prueba el restore. Este es el paso que todo el mundo se salta. Levanta el stack en una máquina desechable, restaura desde tu backup más reciente y confirma que la app de verdad arranca limpia. Un backup que nunca restauraste es una ilusión, no un backup.

Atención

Que "esté corriendo" no es un backup, y un backup sin probar casi tampoco. Haz un simulacro de restore antes de confiarle a esta máquina cualquier cosa de verdad, y déjate un recordatorio para repetirlo cada trimestre. La primera vez que restaures no debería ser nunca durante el incidente.

Un solo VPS con Docker, Traefik y backups disciplinados va a llevar una operación pequeña mucho más lejos de lo que la gente cree. Es tranquilo, barato y enteramente tuyo, sin un panel de control por servicio al que estar pendiente. El trabajo que de verdad importa no es el YAML. Es la media hora de endurecimiento del host al principio y el simulacro de backup-restore que corres antes de confiar en él. Acierta en esos dos y lo demás es solo ponerle labels.

Puntos clave

  • Blinda el host primero: usuario no-root, SSH solo con llave, firewall que por defecto niega todo (solo 22/80/443) y actualizaciones automáticas. Son los treinta minutos más baratos que vas a invertir.
  • Deja que Compose defina los servicios y Traefik los ponga al frente. Suma apps poniéndoles labels a los contenedores, no editando una config central del proxy.
  • Monta el socket de Docker en solo lectura, pon exposedbydefault=false, fija cada imagen y la versión mayor de Traefik, y usa el staging de Let's Encrypt mientras armas todo.
  • Mantén los secretos en archivos env por servicio (modo 600, en gitignore) con un .env.example versionado. Deja las bases de datos en volúmenes con nombre y con los puertos cerrados al host.
  • Los backups son lo único que no se negocia: dumps lógicos en horario fijo, sacados de la máquina y encriptados, con un restore que de verdad hayas probado. Que "esté corriendo" no es un backup.

Preguntas frecuentes

¿No es arriesgado correr todo en una sola máquina en vez de usar servicios gestionados?

Es una concesión real, no un defecto escondido. Una máquina es un solo radio de impacto: si se cae, todo lo que tiene encima queda fuera hasta que restaures. Eso es perfectamente aceptable para una operación de una persona o un producto pequeño, siempre que tus backups vivan fuera de la máquina y hayas probado un restore. Los servicios gestionados cambian dinero por esa resiliencia. Esto cambia un poco de resiliencia por bastante dinero y simplicidad. Si unos minutos de downtime te cuestan ingresos de verdad, trata esto como tu montaje de staging y paga por redundancia en producción.

¿Por qué Traefik en vez de Nginx o Caddy?

Traefik descubre los servicios desde los labels de Docker, así que sumar una app es ponerle labels a un contenedor en vez de editar y recargar un archivo de config central. Esa es la propiedad que mantiene manejable una máquina con varias apps. Caddy es una alternativa excelente, con TLS igual de automático y un lenguaje de config más simple. Si prefieres un solo archivo de config en lugar de labels por contenedor, es una buena opción. Nginx también sirve, pero te toca manejar los certificados y las rutas de forma más manual. Quédate con el modelo operativo que de verdad vas a mantener ordenado.

¿Puedo poner mi base de datos Postgres en el mismo stack de Compose?

Sí, para un proyecto pequeño, con dos condiciones. Dale un volumen con nombre para que sus datos sobrevivan a recrear el contenedor, y nunca publiques su puerto al host (alcánzala por la red de Docker usando su nombre de servicio). La condición que la gente se salta es un restore probado: un pg_dump lógico en un horario fijo, enviado fuera de la máquina, que de verdad hayas restaurado al menos una vez. Que una base de datos corra no es lo mismo que que sea segura.

No se me emiten los certificados. ¿Qué reviso primero?

Tres cosas, en orden. Primero, el DNS: ¿el registro A del hostname de verdad apunta a este VPS y ya propagó? El challenge HTTP-01 falla si Let's Encrypt no logra alcanzar tu máquina en el puerto 80. Segundo, el firewall: el puerto 80 tiene que estar abierto a la entrada aunque redirijas a 443, porque el challenge lo usa. Tercero, los rate limits: si has estado reintentando contra producción, puedes estar limitado, así que pásate al CA de staging mientras haces debug y emite los certificados reales una vez que funcione. Confirma también que acme.json esté en modo 600, o Traefik se niega a usarlo.

¿Hasta dónde escala de verdad un solo VPS?

Más lejos de lo que la mayoría supone. Un puñado de apps modestas más una base de datos pequeña corren cómodas en 4 GB de RAM, y puedes crecer redimensionando la máquina (escalado vertical) antes de tener que separar máquinas. El techo que pegas primero casi nunca es el CPU. Es tu disciplina de backup y restore y tu tolerancia a un solo punto de fallo. Cuando una de esas dos se vuelve la restricción real, esa es tu señal para sumar una segunda máquina o mover las piezas con estado a un servicio gestionado, no antes.

¿De verdad necesito blindar el host si es solo un proyecto secundario?

Sí, y son unos treinta minutos. Una IP pública recibe sondeos automatizados a los pocos minutos de arrancar, y un proyecto secundario con la base de datos filtrada sigue siendo una base de datos filtrada. El mínimo que se paga solo: un usuario no-root, SSH solo con llave con las contraseñas y el login de root deshabilitados, un firewall que por defecto niega todo y deja pasar solo 22/80/443, y actualizaciones de seguridad automáticas. Brincártelo no hace la máquina más rápida de montar. La hace más rápida de comprometer.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados