ilustrari.com y varios proyectos paralelos corren en un solo VPS: un contenedor Docker por app, Traefik al frente, TLS automático y redeploys que apenas tumban un request. Aquí te muestro el armado completo, el razonamiento detrás, la postura de seguridad y el punto exacto donde deja de ser buena idea.

En resumen
- Un VPS, un proxy Traefik que es dueño del 80/443, un contenedor por app, nada más toca el internet público.
- Traefik lee las rutas desde labels de Docker, así que sumar una app son cuatro labels, no recargar una config.
- El estado vive fuera de la máquina (Supabase): perder el VPS es volver a desplegar, no restaurar.
- Los redeploys casi no tienen downtime: nunca mates el contenedor viejo antes de que el nuevo esté sano.
- Es una sola máquina, y el techo es claro: sin escalado horizontal, los parches corren por tu cuenta, y esto no te da un SLA de cinco nueves.
Cada app que lanzo pide su propio dominio, su propio certificado TLS y su propio runtime. La respuesta automática es una plataforma administrada por cada app, y funciona, justo hasta que las facturas se te acumulan y caes en cuenta de que estás pagando una suscripción mensual con tal de no aprenderte dos archivos de configuración. Esta es la apuesta contraria: un VPS, un contenedor Docker por app, Traefik al frente y visibilidad total de la máquina. Para cuando termines vas a tener el mismo armado que corro para ilustrari.com y algunas herramientas internas, la postura de seguridad que lo sostiene, y el punto honesto donde esto deja de ser la decisión correcta.
No te voy a vender una proporción mágica de apps por dólar. Te voy a mostrar cómo se conecta todo, decirte cuáles dos flags hacen casi todo el trabajo, y decirte dónde se rompe. Si vienes de ops o de seguridad, esto se va a sentir como volver a casa. Si vienes de plataformas, es la forma más barata que conozco de entender de verdad tu propia infraestructura.
01 · Por qué una sola máquina en vez de una plataforma por app
Un dev en solitario no tiene un problema de tráfico. Tienes un problema de costo y atención. Las plataformas administradas te resuelven un problema de escalado que todavía no tienes, y te lo cobran cada mes por el lujo. Un solo VPS con unos cuantos gigas de RAM corre más contenedores de los que crees, y todo está a una sesión de SSH de distancia, sin dashboard, sin página de precios por servicio, sin ese plan de "habla con ventas" que te esconde justo la perilla que necesitas.
La concesión es honesta y la quiero poner sobre la mesa desde temprano: la máquina es tuya. Los parches, los backups y el firewall ahora corren por tu cuenta. Yo lo veo como una ventaja, no como un impuesto. Vengo de liderazgo de IT, seguridad e ingeniería de telemática, así que prefiero ver toda la superficie de ataque a confiar en que un panel de control me la esconda. Superficie escondida sigue siendo superficie.
El modelo mental es chiquito a propósito. El VPS hace exactamente dos cosas:
- Corre un solo reverse proxy que es dueño de los puertos 80 y 443 y termina el TLS de todo.
- Corre un contenedor por app, y ninguno expone un puerto público directamente.
El proxy es lo único con lo que el internet llega a hablar. Todo lo demás vive en una red privada de Docker y, desde afuera, no se ve. Esa sola frase es toda la historia de seguridad, y el resto de esta nota es solo volverla cierta y mantenerla cierta.
Nota
Este es un patrón de un solo operador. Es excelente para un fundador lanzando productos reales y francamente malo para un equipo que necesita failover entre regiones. Ten claro cuál de los dos eres antes de copiarlo.
02 · Traefik como puerta de entrada
Traefik es el reverse proxy. Lo elegí sobre nginx por una razón concreta: Traefik lee su enrutamiento desde labels de Docker. No editas un archivo de config central ni lo recargas cada vez que sumas una app. Levantas la app, le pones labels al contenedor, y Traefik se entera solo. Para un operador en solitario esa diferencia suma, cada deploy es un archivo menos que mantener sincronizado.
Este es el proxy en sí, como servicio de Compose:
services:
traefik:
image: traefik:v3.1
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.le.acme.tlschallenge=true
- --certificatesresolvers.le.acme.email=tu@correo.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- edge
networks:
edge:
external: true
Las dos líneas que cargan todo el peso
Casi todo este archivo es boilerplate. Dos ajustes son lo único que importa.
- exposedbydefault=false, ningún contenedor recibe una ruta pública a menos que yo lo habilite a propósito con un label. Esa postura de negar por defecto es la razón entera por la que confío en este diseño. Un contenedor nuevo que levanto para un experimento rápido no se puede alcanzar desde el internet hasta que yo decida lo contrario. El default es "apagado", y "apagado" es el único default seguro.
- El resolver acme que llamé le maneja los certificados de Let's Encrypt con el challenge de TLS. El HTTPS es automático, las renovaciones pasan solas, y nunca en mi vida he entrado por SSH a rotar un cert. Ese es justo el tipo de trabajo repetitivo que quiero que haga una máquina, no yo.
El permiso del socket de Docker
Fíjate que el socket se monta en solo lectura, con el sufijo «:ro». Traefik necesita observar los eventos de los contenedores para enterarse de las rutas nuevas; no necesita controlar el daemon. El socket de Docker equivale a root en el host. Quien pueda escribir en él se adueña de la máquina. Por eso mantengo ese permiso tan estrecho como el proxy lo aguante.
Atención
Montar el socket de Docker en lectura-escritura dentro de un proxy que termina el TLS público es regalarle al internet un camino directo a root si ese proxy llega a comprometerse. Déjalo en «:ro». Si más adelante necesitas acceso de escritura para algún plugin, mejor mete un socket-proxy en medio.
03 · Un contenedor por app
Ahora una app de verdad. Así se ve el propio sitio de ilustrari.com, un solo contenedor de Next.js:
services:
web:
image: ghcr.io/ilustrari/site:latest
restart: unless-stopped
expose:
- "3000"
environment:
- NODE_ENV=production
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 10s
timeout: 3s
retries: 3
labels:
- traefik.enable=true
- traefik.http.routers.site.rule=Host(`ilustrari.com`)
- traefik.http.routers.site.entrypoints=websecure
- traefik.http.routers.site.tls.certresolver=le
- traefik.http.services.site.loadbalancer.server.port=3000
networks:
- edge
networks:
edge:
external: true
La palabra más importante aquí es expose, no ports. La app escucha en el 3000 dentro de la red compartida edge, y solo Traefik la alcanza. No hay forma de llegar desde el internet público al puerto 3000 que no sea pasando por el proxy, que es donde viven la terminación de TLS y el enrutamiento. Si escribes ports: "3000:3000" por costumbre, acabas de abrir un hueco que se salta tu propia puerta de entrada. No lo hagas.
Sumar un proyecto nuevo es mecánico. Digamos que quiero un build de staging de Agent Orchestra, mi herramienta visual de orquestación multi-agente, al lado del sitio en producción. Son los mismos cuatro labels de router con otra regla de host:
labels:
- traefik.enable=true
- traefik.http.routers.orch.rule=Host(`staging.ilustrari.com`)
- traefik.http.routers.orch.entrypoints=websecure
- traefik.http.routers.orch.tls.certresolver=le
- traefik.http.services.orch.loadbalancer.server.port=8080
Lo levantas, y el proxy lo agarra en segundos, cert de TLS nuevo incluido. Sin tocar ningún archivo central, sin recargar nada.
A dónde va el estado (y por qué va fuera de la máquina)
Las apps que necesitan base de datos se apoyan en Supabase, que vive totalmente fuera del VPS. Eso es una decisión deliberada, no un accidente. Mantener el estado persistente fuera de la máquina deja al VPS casi sin estado: perder la máquina es volver a desplegar, no restaurar de un backup a las 2 de la mañana. El estado es justo lo que más te conviene tener lejos de un único punto de falla. Infuse, mi bóveda de secretos de API, sigue el mismo instinto. La máquina corre la app, y los secretos que importan viven en algo hecho para cuidarlos.
Consejo
Trata el VPS como ganado, no como mascota. Si puedes levantar todo con «docker compose up -d» desde una máquina limpia, tu repo de Git y tu registry, y estar de vuelta en línea en minutos, lo armaste bien. Si recuperarte depende de un snapshot que ojalá sea reciente, lo que armaste es un pasivo.
04 · Redeploys que apenas tumban un request
El cero downtime de verdad necesita una orquestación que a propósito no corro aquí. Eso es otro nivel y otra factura. Pero puedes acercarte lo suficiente como para que ningún humano lo note. El truco es una sola regla: nunca borres el contenedor viejo antes de que el nuevo esté sano.
docker pull ghcr.io/ilustrari/site:latest
docker compose up -d --no-deps --wait web
El flag --wait se queda esperando hasta que el contenedor se reporte sano, lo que solo sirve de algo si definiste un healthcheck, así que defínelo (ahí está, en el snippet de Compose de arriba). Esta es la secuencia que te da el cambio casi sin costuras:
- Compose levanta el contenedor nuevo al lado del viejo.
- Traefik ve al nuevo unirse a la red edge y empieza a mandarle tráfico.
- El healthcheck pasa; --wait retorna; Compose reemplaza al contenedor viejo.
- Por un solapamiento breve los dos están vivos. Los requests en vuelo del viejo terminan, los nuevos caen en el nuevo.
Es "cero downtime más o menos", y para una operación de una sola persona ese es justo el nivel correcto. Prefiero un 99.9% honesto que entiendo por completo a una promesa frágil de 100% que no puedo explicar a las 3 de la mañana.
Para las piezas con base de datos hay una disciplina que no te puedes saltar: corre las migraciones como un paso aparte antes del deploy, y mantenlas compatibles hacia atrás por un release. Durante el solapamiento, el contenedor viejo sigue hablándole a la base de datos. Si tu migración borró una columna que él necesita, acabas de romper justo los requests que estabas tratando de proteger. Expande el esquema, despliega el código nuevo, y contrae en un release posterior. Es el clásico baile de expandir y contraer; el VPS único solo lo hace barato de practicar.
05 · La postura de seguridad que no te puedes saltar
La máquina es tuya, así que su seguridad es tuya. Esta no es una sección opcional. Es la renta que pagas por la economía. Lo innegociable, en orden de prioridad:
- Solo llaves SSH, nada de contraseñas. Deshabilita la autenticación por contraseña por completo. Los bots encuentran tu IP a las pocas horas de levantar el servidor; la auth por contraseña es una invitación abierta al fuerza bruta.
- Un firewall bien cerrado. Permite el 80, el 443 y tu puerto de SSH. Niega todo lo demás de entrada. Tus contenedores de app no necesitan puertos públicos, ese es el punto entero de la sección 03, así que el firewall y la disciplina del «expose» se refuerzan mutuamente.
- Updates de seguridad desatendidos. Parchea el kernel y el daemon automáticamente. Un fundador en solitario no va a entrar cada semana a correr updates a mano, así que pon a la máquina a hacerlo.
- fail2ban en SSH para cortar el ruido, y límites de recursos por contenedor (el límite de memory del Compose de arriba) para que un proceso desbocado no pueda ahogar a los demás.
Importante
El socket de Docker y el firewall son tus dos controles más valiosos. Deja el socket en solo lectura y las reglas de entrada en negar por defecto antes de preocuparte por cualquier otra cosa. Todo lo demás, más allá de esos dos, es defensa en profundidad, no el cimiento.
Nada de esto es exótico. Es lo mínimo, la higiene aburrida y de toda la vida que la plataforma administrada hacía por ti calladita y te cobraba dentro de la suscripción. Ser dueño de la máquina significa ser dueño también de las partes aburridas.
06 · Dónde deja de ser buena idea
No voy a fingir que esto escala para siempre, porque fingir es justo lo que hace que te agarren desprevenido. Sé honesto sobre el techo:
- Es una sola máquina. Si el VPS muere, todas las apps se caen hasta que la reconstruyas. La mitigación es todo lo de arriba, imágenes en un registry, config en Git, para que reconstruir sea cosa de minutos, no de una tarde arruinada. Pero "minutos de downtime" sigue siendo downtime.
- No hay escalado horizontal real. Un Traefik, un host. El día que una sola app de verdad necesite varios nodos, este diseño es la herramienta equivocada y te toca buscar un orquestador en serio, ahí, no antes de tiempo.
- Los vecinos ruidosos existen. Pon límites de memoria y CPU por servicio. Un job de build que se porta mal o un experimento con fuga de memoria jamás debería poder tumbar tu sitio de marketing.
- Un SLA de cinco nueves no es esto. Si un cliente lo necesita por contrato, lo digo de frente y armamos algo distinto. Venderle a alguien un diseño de un solo VPS bajo una promesa de cinco nueves no es honestidad anti-hype. Es mala praxis.
Ahora, para lanzar productos reales como fundador en solitario, la economía es genuinamente difícil de superar: una máquina, una factura predecible y visibilidad completa de cada capa. Sé exactamente qué está corriendo, por qué y cuánto cuesta.
Hospedar lo tuyo no es un paso hacia atrás. Es elegir entender tu propia infraestructura en lugar de pagar para que otro la entienda por ti. Docker te da aislamiento, Traefik te da una puerta inteligente con TLS gratis, y Git más un registry hacen que todo sea reproducible. Empieza por el proxy, suma una app, y luego la siguiente con cuatro labels; el día que de verdad un solo VPS se te quede corto lo vas a saber, y te vas a mover con los ojos bien abiertos.
Puntos clave
- Un proxy es dueño del 80/443 y del TLS; a todo lo demás solo se llega pasando por él, negar por defecto es toda la historia de seguridad.
- Usa expose, nunca ports, en los contenedores de app; monta el socket de Docker en solo lectura.
- Mantén el estado fuera de la máquina para que el VPS sea desechable: recuperarte es un redeploy, no un restore.
- Los deploys casi sin downtime salen de una sola regla, contenedor nuevo sano antes de matar el viejo, más migraciones compatibles hacia atrás.
- Sé honesto sobre el techo: es una sola máquina, la seguridad y los parches corren por tu cuenta, y los clientes de cinco nueves necesitan otra arquitectura.
Preguntas frecuentes
¿Correr todo en un solo VPS no es un único punto de falla?
Sí, y no lo escondo. Todo el diseño está pensado para que ese punto único falle con elegancia: las imágenes viven en un registry, la config en Git, y el estado fuera de la máquina en Supabase. Recuperarte es un redeploy de minutos, no un restore a base de fe. Si tu negocio no tolera minutos de downtime, este es el patrón equivocado y te conviene armar todo pensando en failover.
¿Por qué Traefik en lugar de nginx o Caddy?
Traefik lee el enrutamiento directo desde labels de Docker, así que sumar una app nunca implica editar una config central y recargarla. Caddy también es excelente y el auto-TLS ahí es igual de fácil. Si te gusta más su modelo de archivo de config, úsalo. nginx es sólido como roca, pero mantienes la config a mano, que es justo el trabajo repetitivo que intento quitarme de encima en un flujo en solitario. Elige el que tenga la ergonomía que encaje con cómo despliegas de verdad.
¿Cuántas apps aguanta de verdad un solo VPS?
Más de las que crees, pero no te voy a dar un número inventado, depende por completo del apetito de memoria y CPU de cada app. La respuesta honesta es poner límites de recursos por contenedor, mirar el uso real y dejar que los números te digan. Un puñado de apps de Next.js y Node de bajo tráfico conviven cómodas en unos cuantos gigas de RAM. Un solo servidor de modelo, hambriento de memoria, podría llenar la máquina él solito.
¿Es seguro montar el socket de Docker dentro de Traefik?
Es aceptable cuando se monta en solo lectura, que es lo que hace la config. Traefik solo necesita observar los eventos de los contenedores, nunca controlar el daemon. El socket equivale a root, así que dejarlo en lectura-escritura significaría que un proxy comprometido equivale a un host comprometido. Si alguna vez necesitas escritura para un plugin, mete un socket-proxy dedicado delante en lugar de ampliarle el permiso a Traefik.
¿Cómo evito downtime durante las migraciones de base de datos?
Corre las migraciones como un paso aparte antes del deploy, y mantenlas compatibles hacia atrás por un release. Durante el solapamiento breve, el contenedor viejo sigue consultando la base de datos, así que una migración destructiva rompe justo los requests que intentas proteger. Expande el esquema primero, despliega el código nuevo, y contrae en un release posterior. Es el clásico patrón de expandir y contraer.
¿Cuándo me conviene salir de este setup?
Cuando una sola app de verdad necesite más de un nodo, cuando un contrato exija garantías de alta disponibilidad que esto no puede cumplir, o cuando pases más tiempo cuidando la máquina que lanzando. Hasta que uno de esos sea cierto, irte a un orquestador es pagar por un escalado que no tienes. Vas a saber el día que te quedaste chico. No migres por adelantado por ansiedad.
¿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 WhatsAppPrimera conversación gratis. Te responde el fundador.
Recursos relacionados

Autoaloja todo tu stack en un solo VPS
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.

Despliega Next.js detrás de Traefik con TLS automático
Un recorrido de principio a fin del patrón exacto que uso en ilustrari.com: un contenedor Next.js que no expone nada al exterior, un proxy Traefik dueño del 80/443 y un certificado Let's Encrypt que se emite y se renueva solo. Terminas con un deploy funcionando y una idea clara de por qué cada label está donde está.

Despliegues casi sin caída a un VPS desde GitHub Actions
El docker compose up a secas detiene el contenedor viejo antes de que el nuevo esté listo, y cada request que cae en ese hueco recibe un rechazo. Este es el patrón de build-and-ship que uso en un solo VPS: Actions construye la imagen, la sube a un registro y luego entra por SSH para cambiar el contenedor. Con un health check de verdad y tags por SHA, el hueco se reduce a casi nada y un deploy malo se revierte en una línea. Terminas con un pipeline funcionando y claridad sobre dónde se esconde de verdad la caída.