Infraestructura
Alquilé un VPS, lo aseguré y ahí corren mis aplicaciones. No es un detalle de implementación: es la diferencia entre escribir código y sostener un producto. Abajo está cómo está armado y por qué cada pieza está donde está.
Un VPS de 2 vCPU y 8 GB de RAM, con Ubuntu Server LTS. Sin panel de control.
Cómo está armado
Dos aplicaciones distintas, con dominios distintos, compartiendo un solo servidor. El único proceso que escucha en internet es el reverse proxy; todo lo demás se habla por redes internas de Docker que no salen a ningún lado.
Borde — expuesto a internet
Caddy
Los únicos puertos abiertos del servidor son 80, 443 y SSH. Emite y renueva los certificados TLS de los dos dominios sin intervención.
Aplicaciones — red compartida
caudal-web
gunicorn + Django. Sin puertos publicados: Caddy lo alcanza por el nombre del servicio.
cuadrosjaci-web
API de Django REST Framework. El frontend son archivos estáticos que sirve el proxy.
Datos — redes aisladas, una por app
caudal-db
PostgreSQL. Verificado que no responde desde la red del proxy: solo lo alcanza su propia aplicación.
cuadrosjaci-db
PostgreSQL, en su propia red. Una app comprometida no llega a la base de la otra.
Lo que sostiene el servidor
Acceso
La única forma de entrar al servidor por red es una clave criptográfica. No hay contraseña que adivinar.
- El login de root por SSH está cerrado, y la autenticación por contraseña también.
- El usuario de trabajo se creó directamente sin contraseña: no existe una que se pueda filtrar.
- Firewall en denegar-todo por defecto. Los únicos puertos abiertos a internet son SSH, 80 y 443, y los tres tienen dueño conocido.
- Actualizaciones de seguridad desatendidas, activas y verificadas.
Respaldos
Respaldo diario automático, y la restauración se prueba de verdad en vez de asumirse.
- Un timer de systemd, no cron: si el servidor estaba apagado a la hora prevista, el timer corre la tarea al arrancar. Cron simplemente la saltea.
- Retención escalonada: diarios, semanales y mensuales. Responde a una amenaza distinta del disco muerto — un import que duplica todo se descubre semanas después, y ahí lo que salva es la profundidad del historial.
- El verificador restaura el dump en una base descartable y compara las filas contra el origen. Un dump que 'no falló' y un respaldo que sirve no son lo mismo.
- El verificador ignora a propósito las tablas volátiles, como las de sesión. Un verificador que grita lobo todos los días se termina ignorando, y ese es el peor resultado posible.
Convenciones
Los nombres de contenedores y volúmenes son obligatorios, porque el mantenimiento del servidor descubre qué hacer a partir de ellos.
- Una aplicación que respeta la convención queda respaldada y mantenida sola. Una que no la respeta queda sin backup en silencio: no hay error, simplemente no aparece. Es la peor clase de falla, y por eso la convención no es opcional.
- Ningún contenedor de aplicación o base publica puertos al host. La razón no es estética: Docker escribe reglas de iptables por debajo del firewall, así que publicar un puerto deja la base expuesta a internet aunque el firewall diga lo contrario.
- Límites de memoria y CPU por servicio. No es por ahorrar —sobra RAM— sino para que una aplicación con problemas no deje sin recursos a las demás.
- Rotación de logs en cada servicio. Sin eso los logs de Docker crecen sin techo y llenan el disco en silencio.
Decisiones, y por qué
Ubuntu pelado, sin panel de control
En vez decPanel, Plesk, o un panel Docker-native
Los paneles tradicionales están pensados para hosting compartido de PHP: gestionan vhosts, cuentas FTP y bases por usuario, nada de lo cual aplica a un stack de contenedores. Además compiten por los puertos 80 y 443 y tocan la configuración de red del sistema, lo que rompe el bridge de Docker. Los paneles Docker-native sí eran opción real, pero agregan una abstracción más que hay que depurar además de la aplicación.
Cerrar el acceso viejo recién después de probar el nuevo
En vez deaplicar todo el hardening de una vez
Si el usuario nuevo no puede entrar o no tiene permisos, y ya cerraste el login de root, quedaste afuera del servidor. Verificar en el medio convierte un error irreversible en uno trivial.
Un temporizador que revierte solo los cambios de SSH
En vez deconfiar en la sesión abierta como red de seguridad
Validar la sintaxis de la configuración no comprueba que vos sigas pudiendo entrar: una regla mal escrita o un permiso raro pasan la validación y te dejan afuera igual. Antes de aplicar el cambio dejo programada su reversión automática a los 15 minutos, y la cancelo recién después de verificar el acceso en una sesión nueva. La sesión abierta sirve solo mientras no se cierre; el temporizador no depende de eso.
Sin fail2ban
En vez deinstalarlo por costumbre
Con la autenticación por contraseña deshabilitada, la fuerza bruta no tiene contra qué acertar: su aporte queda reducido a limpiar ruido de los logs. Es una herramienta buena resolviendo un problema que acá no existe. Queda anotada la condición para revisarlo: si algún día expongo un servicio con login por contraseña, la decisión cambia.
Mover la base de datos a mi propio servidor
En vez deseguir en un Postgres gestionado con backups incluidos
Gané control sobre el dato más sensible del proyecto y latencia mínima: la aplicación y la base quedan en la misma red interna, sin salir a internet en cada consulta. El intercambio fue explícito y no gratuito: el proveedor gestionado venía cubriendo los respaldos, y desde la mudanza no los cubre nadie. Por eso los backups dejaron de ser una tarea pendiente y pasaron a ser bloqueantes.
Sin Cloudflare adelante, por ahora
En vez deproxear el tráfico y ganar caché y protección DDoS gratis
Meter un segundo proxy delante choca con el bloqueo por IP de la aplicación, y hay que acertar tres cosas a la vez: el conteo de proxies de la librería de bloqueo, los rangos de confianza del reverse proxy, y el método de emisión del certificado. Si el conteo queda mal, el sistema bloquea la IP del proxy en vez de la del atacante — o directamente deja afuera a los usuarios legítimos. Tres cosas que hay que acertar juntas, en una app que todavía no había llegado a producción. A la escala actual, lo que se gana es marginal.
Lo que todavía no está resuelto
Publico esto a propósito. Un servidor sin puntos débiles conocidos es un servidor que no se auditó.
Los respaldos todavía son solo locales
Los backups viven en el mismo disco que los datos. Eso cubre los errores lógicos —un import que duplica todo, un borrado accidental— que es la falla más frecuente. No cubre perder el servidor: si muere el disco, se van los datos y los respaldos juntos. Mientras tanto, el proveedor gestionado del que migré sigue encendido a propósito: al no haber copia fuera del servidor, esa base pasó a ser parte del diseño de respaldo y no un resto de la migración.
Cuándo lo resuelvo Antes de apagar el proveedor anterior. Ese es el momento exacto en que la falta de copia externa pasa de aceptable a peligrosa.