Bitácora
Lo que aprendimos al publicar un sitio en un servidor propio: despliegue automático, cabeceras de seguridad y errores reales
Cómo publicamos un sitio Next.js en un servidor virtual con Easypanel: despliegue desde la rama principal, las seis cabeceras de seguridad, la base de datos protegida por filas y lo que salió mal.
Redacción Mandato IA · Publicado el · Actualizado el · 14 min de lectura
En este artículo
- Cómo está montado
- Despliegue automático sin red de contención
- El orden correcto para estrenar un dominio
- Las seis cabeceras de seguridad, una por una
- Cómo comprobamos que la CSP funcionaba de verdad
- La base de datos: seguridad a nivel de filas
- Dos ejemplos para tu propio caso
- Errores comunes (los nuestros, primero)
- Lo que todavía falta
A fines de septiembre de 2026 publicamos nuestro sitio principal en un servidor propio. No en una plataforma que lo hace todo por ti, sino en un servidor virtual (VPS) que administramos nosotros, con un panel que facilita el trabajo pero no lo hace desaparecer. Este artículo cuenta cómo quedó montado, qué cabeceras de seguridad pusimos y por qué, cómo protegemos la base de datos, y los errores que cometimos en el camino.
Lo escribimos para el profesional o estudio pequeño que piensa llevar su sitio, y quizá su n8n, a un servidor propio. No es una recomendación para todos: es lo que nos pasó y lo que todavía nos falta.
Cómo está montado
El sitio está hecho con Next.js. El servidor es un VPS modesto, de 2 núcleos y 8 GB de memoria, con Ubuntu 24.04 y Easypanel instalado. Easypanel es un panel que se instala en tu propio servidor y, por debajo, usa Docker para correr cada aplicación y un proxy (Traefik) para recibir el tráfico de cada dominio y gestionar los certificados de seguridad. En el mismo servidor corre también nuestro n8n, en un proyecto separado.
La aplicación se compila con un Dockerfile de varias etapas y con la opción de Next.js output: standalone, que genera una carpeta autocontenida con solo los archivos necesarios para producción. La imagen final pesó alrededor de 339 MB. El panel está conectado al repositorio en GitHub: cada vez que subimos un cambio a la rama principal, GitHub avisa al panel por un webhook y el panel reconstruye y republica el sitio solo. Tarda unos 90 segundos.
| Pieza | Qué usamos | Qué hace |
|---|---|---|
| Servidor | VPS con Ubuntu 24.04 | La máquina donde corre todo |
| Panel | Easypanel | Crea servicios, conecta el repositorio, asigna dominios |
| Proxy | Traefik (lo instala el panel) | Recibe el tráfico, enruta cada dominio, pide los certificados |
| Compilación | Dockerfile con output: standalone | Imagen liviana, sin instalar todas las dependencias en producción |
| Despliegue | Webhook de GitHub hacia el panel | Subir a la rama principal publica solo |
| Certificados | Let's Encrypt | HTTPS gratuito con renovación automática |
Despliegue automático sin red de contención
El despliegue automático es cómodo y peligroso por la misma razón: no hay un paso intermedio. No tenemos un entorno de pruebas entre nuestra computadora y el sitio publicado. Un cambio que se sube a la rama principal está al aire en minuto y medio, bueno o malo. Por eso adoptamos una regla fija: antes de cada subida corremos la compilación de producción en local, y para cambios de peso levantamos el servidor de producción en la computadora y miramos las páginas que tocamos.
La compilación local atrapa errores de tipos, páginas que no se pueden generar e importaciones rotas. Un error de lógica o un texto equivocado pasa igual, pero es la diferencia entre un error pequeño y un sitio que no arranca.
El orden correcto para estrenar un dominio
Acordamos un orden de seguridad antes de tocar el dominio real: compilar, probar dentro del servidor, probar en el subdominio gratuito que da el panel (ya con HTTPS real) y recién entonces apuntar el dominio. El dominio nunca debía mostrar algo sin verificar. Ese orden funcionó para el sitio. Lo que no funcionó fue el certificado.
Agregamos el dominio en el panel antes de que el DNS apuntara al servidor. Traefik intentó pedir el certificado a Let's Encrypt, y Let's Encrypt fue a verificar el dominio a la dirección vieja, una página de dominio estacionado. Falló. El problema es que, después de fallar, no volvió a intentarlo: ni al cambiar el DNS, ni al reescribirse la configuración, ni al volver a desplegar el servicio. Lo único que funcionó fue reiniciar Traefik, lo que cortó todo el tráfico web del servidor, n8n incluido, durante unos 15 a 20 segundos.
La validación más común de Let's Encrypt (llamada HTTP-01) consiste en que su servidor pide un archivo a tu dominio por el puerto 80. Si el dominio todavía apunta a otra máquina, la validación no puede pasar. El orden correcto, que ahora seguimos siempre, es: cambiar el DNS primero, esperar a que los servidores de nombres respondan con la dirección nueva y recién entonces agregar el dominio en el panel. Hecho así, el certificado sale al primer intento.
Las seis cabeceras de seguridad, una por una
Las cabeceras de seguridad son instrucciones que el servidor manda junto con cada página y que el navegador obedece. No cuestan nada en rendimiento y cierran varios tipos de ataque. En Next.js se configuran en el archivo de configuración, en la función headers. Pusimos seis, más una opción que quita una cabecera que sobra.
1. Strict-Transport-Security (HSTS)
Le dice al navegador que el sitio solo se visita por HTTPS y que cualquier intento futuro por HTTP se convierta solo en HTTPS. Lo pusimos con una duración de un año. No agregamos la opción que extiende la regla a todos los subdominios, a propósito: teníamos subdominios sin certificado propio y forzarlos los habría dejado inaccesibles. Cuidado con esta cabecera: el navegador la recuerda, así que en la práctica no tiene marcha atrás durante el tiempo que fijaste.
2. X-Content-Type-Options: nosniff
Impide que el navegador adivine el tipo de un archivo. Si el servidor dice que algo es texto, se trata como texto y no como código. Según MDN, además bloquea scripts y hojas de estilo que lleguen con un tipo que no corresponde.
3. X-Frame-Options: SAMEORIGIN
Impide que otro sitio muestre el nuestro dentro de un marco. Es la defensa contra el clickjacking, el ataque en el que se pone una página real, invisible, debajo de botones falsos para que la persona haga clic sin saber en qué. MDN aclara que la directiva frame-ancestors de la CSP es la versión moderna; dejamos las dos, con valores coherentes entre sí.
4. Referrer-Policy: strict-origin-when-cross-origin
Cuando alguien sale de nuestro sitio hacia otro, el navegador le cuenta al otro sitio solo el dominio de origen, no la ruta completa. Así un tercero no se entera de qué página exacta estaba leyendo el visitante. Es también el valor que OWASP recomienda.
5. Permissions-Policy
Renuncia a permisos del navegador que el sitio no usa: cámara, micrófono, ubicación, USB. Escrito con una lista vacía, el permiso queda apagado para la página y para cualquier marco dentro de ella. Si algún día se colara un script ajeno, no podría ni pedir esos permisos. El permiso de pagos lo abrimos solo para el dominio de la plataforma de cobro, que va incrustada en una página.
6. Content-Security-Policy (CSP)
Es la más poderosa y la más difícil. Es una lista blanca de lo que la página tiene permitido cargar y a dónde puede hablar. Todo lo que no está en la lista, el navegador lo bloquea. La escribimos midiendo, no adivinando: abrimos siete páginas del sitio publicado con un navegador automatizado y anotamos todos los orígenes que pedían de verdad. Salieron los de la analítica y el píxel de medición, y nada más. El de la base de datos lo agregamos leyendo el código, porque solo aparece al enviar un formulario.
Y aquí va la concesión, dicha sin maquillaje: nuestra CSP permite scripts en línea («unsafe-inline»). MDN advierte que eso anula buena parte del propósito de la CSP, porque los scripts en línea son uno de los caminos más comunes de un ataque XSS. La alternativa correcta es un nonce, un valor aleatorio nuevo en cada visita. Pero la documentación de Next.js es clara: usar nonces obliga a que todas las páginas se generen en cada visita; se pierde la generación estática y aumenta la carga del servidor. Para un sitio que no muestra contenido escrito por terceros, en un VPS chico, decidimos que no valía. Lo que la CSP sí cierra: scripts de dominios ajenos, envío de datos a servidores que no están en la lista, enmarcar el sitio y reescribir la base de las direcciones.
Además quitamos la cabecera X-Powered-By, que Next.js manda por defecto y anuncia con qué está hecho el sitio. OWASP recomienda eliminarla: no es un agujero, pero le ahorra trabajo a quien busca sitios por tecnología cuando aparece una falla conocida.
// next.config: las seis cabeceras (valores de ejemplo, ajusta los tuyos)
const seguridad = [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), usb=()' },
{ key: 'Content-Security-Policy', value: "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" },
];
export default {
poweredByHeader: false,
async headers() {
return [{ source: '/:path*', headers: seguridad }];
},
};La CSP del ejemplo es un punto de partida mínimo: si tu sitio usa analítica, un píxel o una base de datos externa, tienes que agregar esos orígenes, o se van a bloquear.
Cómo comprobamos que la CSP funcionaba de verdad
Aprendimos algo incómodo: una CSP que no se aplicó da exactamente el mismo resultado que una perfecta, cero violaciones. Por eso una revisión que solo mira si hay errores no prueba nada. Hicimos tres controles a propósito, cosas que deberían fallar: pedir datos a un dominio ajeno, cargar un script de un servicio externo y abrir una conexión WebSocket a un servidor de afuera. Las tres quedaron bloqueadas. Recién entonces «cero violaciones» en diez páginas quiso decir algo.
Dos detalles más que no conviene volver a descubrir. Primero, Next.js arma las cabeceras al compilar, no en cada visita: si la CSP usa una variable de entorno, esa variable tiene que existir durante la compilación. Segundo, una conexión WebSocket (wss://) no hereda el permiso de su gemela https:// del mismo dominio. Hay que declararla aparte. De eso dependía una demostración en vivo de la portada, que escucha respuestas en tiempo real.
La base de datos: seguridad a nivel de filas
Los formularios del sitio guardan datos en una base Postgres administrada (Supabase). El navegador habla directamente con la base usando una clave pública, que por definición cualquiera puede ver en el código de la página. Lo que impide que esa clave sirva para leer los datos de otros es la seguridad a nivel de filas (RLS): reglas en la base que deciden qué puede hacer cada rol con cada fila. Según la documentación de Supabase, con RLS activada y sin ninguna regla, la clave pública no puede leer ni escribir nada.
Revisamos las seis tablas una por una. Todas tienen RLS activa. Las que reciben formularios solo permiten insertar: se puede mandar un mensaje, pero no leer los de otros. La tabla más sensible no tiene ninguna regla, así que está cerrada del todo para la clave pública y solo se toca desde el servidor. La de reseñas solo deja leer las publicadas y aprobadas; las pendientes no son legibles, y eso importa porque su código de un solo uso es la prueba de que quien opina compró.
Un susto que tuvimos: una consulta de prueba con la clave pública devolvió un código 200, de éxito. Parecía una fuga. Era una lista vacía: la base respondió bien, pero el filtro de filas no dejó pasar ninguna. Otro: en el panel de Supabase aparece un botón «Disable RLS» en cada tabla. Es la acción de apagarla, no el estado. Verlo en todas significa que todas están protegidas.
Dos ejemplos para tu propio caso
Ejemplo ficticio 1: el viernes por la tarde
Un estudio contable ficticio, «Ortega y Asociados», tiene su sitio con despliegue automático. Un viernes a las 18:00 alguien corrige un texto, borra sin querer una línea de una importación y sube el cambio. Sin compilación previa, el panel intenta construir, falla, y según cómo esté configurado puede quedar el sitio anterior o ninguno. Con la regla de compilar antes de subir, el error aparece en la computadora, en diez segundos, y nadie fuera del estudio se entera.
Ejemplo ficticio 2: el formulario de contacto
La abogada ficticia «Sofía Ruiz» guarda las consultas de su sitio en una base de datos y usa la clave pública en el navegador. Si activa RLS y crea una sola regla que permite insertar al rol anónimo, cualquiera puede enviarle una consulta, pero nadie puede leer las consultas de otros con esa misma clave. Si se olvida de activar RLS, la misma clave que está a la vista en su página permite leer todo. La diferencia es una casilla y una regla.
Errores comunes (los nuestros, primero)
- Agregar el dominio en el panel antes de mover el DNS. El certificado falla y puede no reintentarse solo.
- Usar el traductor automático del navegador en el panel. A nosotros nos rompió la interfaz: inyecta etiquetas, la aplicación del panel se cae al guardar y nada se guarda. Además traducía los nombres de los proyectos, lo que invita a borrar el equivocado. Marcamos el panel como «Nunca traducir».
- Confiar en un token de acceso a GitHub vencido. El panel mostraba que no encontraba el repositorio. La prueba rápida: si el selector de repositorios lista los privados, el token sirve.
- Pegar claves en un chat o en un documento mientras configuras. Es más común de lo que parece. OWASP recomienda revocar, reemplazar y borrar cualquier secreto expuesto.
- Correr varias compilaciones a la vez en la misma carpeta. Se pisan los archivos intermedios.
La caché corrupta del servidor de desarrollo
Este error no fue del servidor de producción, pero nos hizo perder tiempo más de una vez. En la computadora, el servidor de desarrollo empezó a responder «no encontrado» (404) en todas las páginas menos la portada. Parecía que el código se había roto. No: era la caché del compilador de desarrollo de Next.js, guardada en la carpeta .next, que se había corrompido. La solución es detener el servidor, borrar esa carpeta a mano y volver a arrancarlo. Ahora, antes de asumir que algo se rompió, pedimos el código de respuesta de dos o tres páginas: si todas dan 404 menos la portada, es la caché.
Lo que todavía falta
No queremos cerrar esto como si estuviera terminado. Hoy, si el servidor se cae, nadie recibe un aviso al instante. Tenemos un flujo de n8n que avisa cuando falla otro flujo, pero no puede avisar si el propio n8n está caído, porque corre en la misma máquina. Lo que falta es un monitor externo, en otra infraestructura, que consulte el sitio y n8n cada pocos minutos y avise por correo o Telegram si no responden. Está anotado como pendiente.
También dependemos de las copias de seguridad semanales del proveedor del servidor. Una semana es mucho tiempo para algunos datos. Y un consejo que nos aplicamos a nosotros mismos: una copia que nunca se restauró a modo de prueba es una suposición, no un respaldo. Y para cuando el volumen de n8n crezca, tenemos planificado pasarlo al modo cola, con procesos trabajadores separados.
Si nos preguntas si lo volveríamos a hacer, la respuesta es sí, porque el costo es fijo y los datos pasan por una máquina que controlamos. Pero lo haríamos con el monitor externo y un plan de respaldos probado desde el primer día, y no después.
Preguntas frecuentes
¿Necesito un servidor propio para mi sitio profesional?
No necesariamente. Las plataformas que publican el sitio por ti son más simples y se encargan de los certificados y las actualizaciones. Un servidor propio conviene cuando ya tienes otras herramientas que alojar, como n8n, y alguien que pueda mantenerlo.
¿Qué es Easypanel?
Un panel que se instala en tu propio servidor para crear y publicar aplicaciones sin hacerlo todo por consola. Por debajo usa Docker y un proxy que gestiona los dominios y los certificados. Puede conectarse a un repositorio y desplegar solo cuando subes cambios.
¿Con cuáles cabeceras de seguridad empiezo?
Las cinco simples no tienen casi riesgo: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy y Permissions-Policy. La CSP conviene escribirla midiendo qué carga tu sitio de verdad y probándola con controles que deberían fallar.
¿Es grave usar unsafe-inline en la CSP?
Reduce mucho la protección contra ataques XSS en línea. Es una concesión que tomamos sabiendo el costo, porque la alternativa en Next.js obliga a generar todas las páginas en cada visita. Si tu sitio muestra contenido escrito por otras personas, la decisión debería ser otra.
¿Es seguro poner la clave pública de la base de datos en el sitio?
Solo si la seguridad a nivel de filas está activa en todas las tablas y las reglas permiten lo mínimo. Con RLS activada y sin reglas, la clave pública no puede leer ni escribir nada.
¿Qué hago si mi servidor de desarrollo da 404 en todo menos la portada?
Probablemente es la caché del compilador. Detén el servidor, borra la carpeta .next y vuelve a arrancarlo antes de buscar errores en el código.
Fuentes
- Next.js: opción output (standalone) (consultado el 7 de octubre de 2026)
- Next.js: configuración de cabeceras (headers) (consultado el 7 de octubre de 2026)
- Next.js: cómo configurar una Content Security Policy (consultado el 7 de octubre de 2026)
- Easypanel: servicio de tipo App (fuente GitHub, despliegue automático, compilación) (consultado el 7 de octubre de 2026)
- Let's Encrypt: tipos de validación (HTTP-01) (consultado el 7 de octubre de 2026)
- MDN: Strict-Transport-Security (consultado el 7 de octubre de 2026)
- MDN: X-Content-Type-Options (consultado el 7 de octubre de 2026)
- MDN: X-Frame-Options (consultado el 7 de octubre de 2026)
- MDN: Clickjacking (consultado el 7 de octubre de 2026)
- MDN: Referrer-Policy (consultado el 7 de octubre de 2026)
- MDN: Permissions-Policy (consultado el 7 de octubre de 2026)
- MDN: Content Security Policy (guía) (consultado el 7 de octubre de 2026)
- OWASP: HTTP Headers Cheat Sheet (consultado el 7 de octubre de 2026)
- OWASP: Content Security Policy Cheat Sheet (consultado el 7 de octubre de 2026)
- OWASP: Secrets Management Cheat Sheet (consultado el 7 de octubre de 2026)
- Supabase: Row Level Security (consultado el 7 de octubre de 2026)
- Documentación de n8n: modo cola (consultado el 7 de octubre de 2026)
Orientación general, no asesoría profesional. Revisa la fuente oficial y la fecha antes de decidir. Cómo escribimos · Avisar de un error