Ecosistema Shard
25 subdominios y 16 servicios propios corriendo en un VPS que administro solo, sin acceso root. Incluye la caída que tumbó 26 sitios de una vez y el mecanismo de auto-reparación que salió de ahí.
Qué es
Un VPS de 2 núcleos y 3,8 GB de RAM donde corre todo lo que construyo para mí: un tablero kanban con
API REST propia, una biblioteca que empuja libros a un Kindle por WiFi, un lector RSVP, un mini-ERP,
monitoreo, un gateway de agentes, un catálogo de mis propias apps Android. Veinticinco subdominios
bajo shard.icu, dieciséis servicios activos, siete tareas programadas.
No es un homelab de fin de semana. Es donde practico lo que mi trabajo no me deja practicar: cuando algo se cae acá, el que lo levanta soy yo, y no hay nadie más a quien escalarle.
La restricción que define todo
No tengo root sobre esta máquina. El usuario camilo no está en sudoers. Los únicos puertos
abiertos al exterior son 80 y 443 (Caddy), 22022 (SSH) y 22000 (Syncthing).
Eso obliga a una arquitectura concreta, y resulta ser una buena arquitectura:
app (bind 127.0.0.1:PUERTO)
→ systemd user unit (~/.config/systemd/user/<nombre>.service)
→ Caddy vhost, solo si va pública
→ registro DNS A en Porkbun
Cada servicio escucha solo en loopback. Nada queda expuesto por accidente, porque nada puede
estarlo: lo único que ve internet es Caddy. Las unidades son de usuario con linger activado, así
que sobreviven al cierre de sesión y arrancan solos en el boot — sin tocar un solo archivo de
/etc.
Ese diseño tiene una trampa que aprendí a punta de susto: en Node, app.listen(PORT, cb) hace bind
a 0.0.0.0, no a loopback. Hay que escribir app.listen(PORT, '127.0.0.1', cb) explícitamente. La
verificación es obligatoria antes de exponer nada:
ss -tlnp | grep <PUERTO> # debe decir 127.0.0.1, nunca 0.0.0.0
El día que se cayeron 26 subdominios
10 de agosto de 2026. Todo el ecosistema dejó de responder de golpe, menos dos sitios.
La causa: caddy.service arranca con --config /etc/caddy/Caddyfile, y ese archivo —que necesita
root para editarse— solo contenía dos hosts. Los otros veintitrés los había ido agregando por la
admin API de Caddy en 127.0.0.1:2019, que no pide root. Funcionaba perfecto… y vivía
únicamente en memoria.
Un systemctl reload caddy bastó para borrar los veintitrés.
El diagnóstico fue rápido; lo interesante fue el arreglo, porque la solución obvia —mover todo al Caddyfile— requiere exactamente el permiso que no tengo. Así que resolví el problema de otra forma:
| Pieza | Rol |
|---|---|
caddy-config-canonical.json |
Fuente de verdad: instantánea de la config completa |
caddy-restore-routes.sh |
Compara la config viva contra un host centinela; si falta, hace POST /load del canonical |
caddy-routes-restore.timer |
OnBootSec=30s + cada 2 minutos |
caddy-snapshot-canonical.sh |
Refresca el canonical desde la config viva |
El script es idempotente por diseño: si el host centinela shard.icu responde, sale con código
0 sin tocar nada. Solo actúa cuando detecta que la config es la del Caddyfile pelado. Por eso puede
correr cada dos minutos sin riesgo.
También espera hasta 60 segundos a que la admin API levante, porque en el arranque el timer puede ganarle a Caddy:
for _ in $(seq 1 60); do
curl -sf -m 3 "$ADMIN/config/" >/dev/null 2>&1 && break
sleep 1
done
Ventana máxima de caída hoy: dos minutos. Sin root, y sin depender de que yo esté despierto.
Lo que me enseñó
El estado que solo vive en memoria no existe. La config funcionaba, respondía, servía tráfico real — y desapareció con un comando rutinario. Cualquier configuración aplicada en caliente necesita una fuente de verdad en disco y un mecanismo que la reaplique, o es una bomba de tiempo.
Un centinela le gana a una comparación completa. Podría haber diffeado las 25 rutas contra el canonical. Chequear un único host que solo existe en la config buena es más barato, más rápido y no tiene falsos positivos cuando agrego una ruta nueva.
La restricción mejoró el diseño. Si hubiera tenido root habría editado el Caddyfile, habría recargado, y no tendría auto-reparación. No tenerlo me forzó a construir algo que se arregla solo.
El corolario incómodo
Hay un paso que es fácil olvidar y que rompe todo en silencio: después de agregar una ruta por la admin API hay que correr el snapshot del canonical. Si no, la ruta nueva funciona… hasta el próximo reinicio, cuando el timer restaura fielmente la versión anterior y la borra.
Es exactamente el tipo de detalle que uno solo aprende operando lo que construyó.