{"id":131,"date":"2026-01-26T21:41:13","date_gmt":"2026-01-27T01:41:13","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=131"},"modified":"2026-02-16T11:45:43","modified_gmt":"2026-02-16T15:45:43","slug":"como-instalar-n8n-en-un-vps-con-debian-postgresql-nginx-https","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/01\/como-instalar-n8n-en-un-vps-con-debian-postgresql-nginx-https\/","title":{"rendered":"Gu\u00eda r\u00e1pida: Instalar n8n en un VPS con Debian + PostgreSQL + Nginx + HTTPS"},"content":{"rendered":"\n<p>Si alguna vez te preguntaste c\u00f3mo hacer una instalaci\u00f3n limpia, segura y lista para producci\u00f3n ligera\/mediana de <strong>n8n<\/strong> self-hosted en un servidor <strong>Debian<\/strong>, usando <strong>Docker Compose<\/strong>, <strong>PostgreSQL<\/strong> (mucho m\u00e1s recomendable que <strong>SQLite<\/strong> si planeas crecer), <strong>Nginx<\/strong> como reverse proxy y certificados <strong>HTTPS<\/strong> gratuitos de <strong>Let\u2019s Encrypt<\/strong>, esta gu\u00eda es para ti.<\/p>\n\n\n\n<p>El enfoque es darte un camino s\u00f3lido con buenas pr\u00e1cticas: persistencia de datos, webhooks p\u00fablicos que funcionen, soporte para task runners (para ejecutar c\u00f3digo de forma m\u00e1s eficiente y segura) y <strong>HTTPS<\/strong> obligatorio. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Requisitos previos<\/h2>\n\n\n\n<p>Antes de empezar, aseg\u00farate de tener lo siguiente:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Un <strong>VPS<\/strong> con <strong>Debian 12<\/strong> (Bookworm) o superior: versiones estables y bien soportadas tanto por <strong>Docker<\/strong> como por <strong>n8n<\/strong>.<\/li>\n\n\n\n<li>Acceso root o con privilegios sudo: lo vas a necesitar para instalar paquetes, configurar el firewall y manejar servicios.<\/li>\n\n\n\n<li>Un dominio apuntando al IP del servidor (registro A):  imprescindible para que los webhooks funcionen desde fuera y para tener HTTPS decente.<\/li>\n\n\n\n<li>Recomendado: al menos 4 GB de RAM y 2 vCPU: n8n es ligero al principio, pero con varios workflows activos y task runners la memoria sube r\u00e1pido.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">1. Preparaci\u00f3n del servidor<\/h2>\n\n\n\n<p>Vamos a dejar el sistema actualizado, un poco m\u00e1s seguro y listo para trabajar en producci\u00f3n b\u00e1sica. Primero, actualiza todo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update &amp;&amp; sudo apt upgrade -y<\/code><\/pre>\n\n\n\n<p>Esto refresca la lista de paquetes y aplica los parches de seguridad pendientes. Hazlo siempre antes de instalar cosas nuevas.Instalamos algunas utilidades b\u00e1sicas que vamos a necesitar:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y curl git ufw apt-transport-https ca-certificates gnupg lsb-release<\/code><\/pre>\n\n\n\n<p>Con esto tienes herramientas para descargar cosas de forma segura, configurar el firewall y a\u00f1adir repositorios externos.<\/p>\n\n\n\n<p><strong>Tip de seguridad importante:<\/strong> no trabajes todo el rato como root. Crea un usuario normal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>adduser n8nuser\nusermod -aG sudo n8nuser<\/code><\/pre>\n\n\n\n<p>Elige una contrase\u00f1a decente. Luego cambia a ese usuario:<\/p>\n\n\n\n<p>De ahora en adelante trabajamos como n8nuser (con sudo cuando haga falta). Es mucho m\u00e1s seguro.<\/p>\n\n\n\n<p>Configuramos un firewall b\u00e1sico con <strong>ufw<\/strong> (solo abrimos lo estrictamente necesario):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw allow OpenSSH\nsudo ufw allow 80\/tcp\nsudo ufw allow 443\/tcp\nsudo ufw --force enable<\/code><\/pre>\n\n\n\n<p>Con esto bloqueas todo lo dem\u00e1s. <strong>SSH<\/strong>, <strong>HTTP<\/strong> y <strong>HTTPS<\/strong> son los \u00fanicos puertos abiertos hacia Internet<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Instalar Docker y Docker Compose<\/h2>\n\n\n\n<p>Docker nos va a permitir correr <strong>n8n<\/strong> y <strong>PostgreSQL<\/strong> de forma aislada, reproducible y f\u00e1cil de actualizar.A\u00f1adimos el repositorio oficial de Docker (es la forma recomendada para tener versiones actualizadas):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -fsSL https:\/\/download.docker.com\/linux\/debian\/gpg | sudo gpg --dearmor -o \/usr\/share\/keyrings\/docker-archive-keyring.gpg\n\necho \"deb &#91;arch=$(dpkg --print-architecture) signed-by=\/usr\/share\/keyrings\/docker-archive-keyring.gpg] https:\/\/download.docker.com\/linux\/debian $(lsb_release -cs) stable\" | sudo tee \/etc\/apt\/sources.list.d\/docker.list &gt; \/dev\/null<\/code><\/pre>\n\n\n\n<p>Actualizamos e instalamos:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin<\/code><\/pre>\n\n\n\n<p><strong>Nota:<\/strong> desde hace tiempo <strong>Docker Compose<\/strong> ya viene como plugin (<strong>docker compose<\/strong>, sin gui\u00f3n). Olv\u00eddate del antiguo <strong>docker-compose<\/strong> en Python.<\/p>\n\n\n\n<p>Verificamos que todo qued\u00f3 bien:<\/p>\n\n\n\n<p>docker &#8211;version<br>docker compose version<\/p>\n\n\n\n<p>Si ves las versiones, perfecto, seguimos.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Crear la estructura y el archivo docker-compose.yml<\/h2>\n\n\n\n<p>Lo organizamos todo en un directorio dedicado para que sea f\u00e1cil hacer backups, moverlo o clonar la configuraci\u00f3n:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/n8n &amp;&amp; cd ~\/n8n\nmkdir data postgres-data<\/code><\/pre>\n\n\n\n<p>La carpeta <strong>data<\/strong> va a guardar las configuraciones, credenciales y archivos que genere n8n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">docker-compose.yml<\/h3>\n\n\n\n<p>Aqu\u00ed definimos los servicios: <strong>PostgreSQL<\/strong> (base de datos persistente recomendada para producci\u00f3n), <strong>n8n<\/strong> y los <strong>task runners<\/strong> (para ejecutar workflows de forma m\u00e1s eficiente y desacoplada).<\/p>\n\n\n\n<p>Puntos clave que vale la pena entender:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Usamos networks internas:<\/strong> los contenedores hablan entre s\u00ed sin exponer puertos al exterior.<\/li>\n\n\n\n<li><strong>Volumes:<\/strong> para que los datos sobrevivan reinicios, actualizaciones o incluso recrear contenedores.<\/li>\n\n\n\n<li><strong>Variables de entorno N8N_*:<\/strong> controlan la URL p\u00fablica, el cifrado de credenciales, el modo de ejecuci\u00f3n, etc.<\/li>\n<\/ul>\n\n\n\n<p>Muy importante: cambia todas las contrase\u00f1as y claves por valores largos, \u00fanicos y secretos. Puedes generar una buena clave de cifrado con:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl rand -base64 32<\/code><\/pre>\n\n\n\n<p><strong>Muy importante:<\/strong> gu\u00e1rdala bien, la vas a necesitar si alguna vez tienes que migrar o recuperar datos.<\/p>\n\n\n\n<p>Crea un archivo <strong>.env <\/strong>(es muy importante para no dejar contrase\u00f1as en el <strong>yml<\/strong>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env - \u00a1NO lo subas a git ni lo compartas!\nDOMAIN=app.tudominio.com               # Cambia esto\nPOSTGRES_PASSWORD=tu_contrase\u00f1a_larga_y_segura\nN8N_ENCRYPTION_KEY=$(openssl rand -base64 32)   # Genera una clave fuerte\nN8N_RUNNERS_AUTH_TOKEN=$(openssl rand -base64 48)  # Token secreto para runners<\/code><\/pre>\n\n\n\n<p>Ahora el <strong>docker-compose.yml<\/strong> (copia y pega este archivo en ~\/n8n\/docker-compose.yml):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services:\n  postgres:\n    image: postgres:16\n    restart: always\n    environment:\n      POSTGRES_USER: n8n_user\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_DB: n8n_db\n    volumes:\n      - .\/postgres-data:\/var\/lib\/postgresql\/data\n    networks:\n      - n8n_net\n\n  n8n:\n    image: n8nio\/n8n:latest\n    restart: always\n    environment:\n      - DB_TYPE=postgresdb\n      - DB_POSTGRESDB_HOST=postgres\n      - DB_POSTGRESDB_PORT=5432\n      - DB_POSTGRESDB_DATABASE=n8n_db\n      - DB_POSTGRESDB_USER=n8n_user\n      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}\n      - N8N_PROTOCOL=https\n      - N8N_HOST=${DOMAIN}\n      - WEBHOOK_URL=https:\/\/${DOMAIN}\/\n      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}\n      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true\n      # Task Runners \u2013 recomendado para producci\u00f3n (ejecuta c\u00f3digo de Code nodes de forma aislada y eficiente)\n      - N8N_RUNNERS_ENABLED=true\n      - N8N_RUNNERS_MODE=external\n      - N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0\n      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}\n      # Descomenta si usas Code nodes en Python nativo:\n      # - N8N_NATIVE_PYTHON_RUNNER=true\n    volumes:\n      - .\/data:\/home\/node\/.n8n\n    depends_on:\n      - postgres\n    networks:\n      - n8n_net\n\n  task-runners:\n    image: n8nio\/runners:latest\n    restart: always\n    environment:\n      - N8N_RUNNERS_TASK_BROKER_URI=http:\/\/n8n:5679\n      - N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}\n      - N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT=15\n      # Descomenta si necesitas m\u00f3dulos Python stdlib completos (con precauci\u00f3n):\n      # - N8N_RUNNERS_STDLIB_ALLOW=*\n    depends_on:\n      - n8n\n    networks:\n      - n8n_net\n\nnetworks:\n  n8n_net:\n    driver: bridge<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfPor qu\u00e9 estas variables?<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>WEBHOOK_URL: <\/strong> le dice a n8n cu\u00e1l es la URL p\u00fablica real (imprescindible detr\u00e1s de <strong>proxy<\/strong>\/<strong>Nginx<\/strong> para que los webhooks funcionen desde fuera y aparezcan bien en el editor).<\/li>\n\n\n\n<li><strong>N8N_ENCRYPTION_KEY: <\/strong>cifra las credenciales guardadas (sin esto, datos sensibles quedan en claro).<\/li>\n\n\n\n<li><strong>Task runners en modo external:<\/strong> separan la ejecuci\u00f3n de c\u00f3digo (Code nodes, AI agents, etc.) del proceso principal de <strong>n8n<\/strong>. M\u00e1s seguro y escalable. El token de auth evita que cualquiera conecte runners falsos.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">4. Arrancar n8n por primera vez<\/h2>\n\n\n\n<p>Ya est\u00e1 todo listo. Subimos los servicios:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose up -d<\/code><\/pre>\n\n\n\n<p>Y miramos los logs en tiempo real para ver que no haya errores graves:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose logs -f n8n<\/code><\/pre>\n\n\n\n<p>\u00bfQu\u00e9 debes ver para saber que va bien?<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>El servidor web de <strong>n8n<\/strong> arranc\u00f3 correctamente.<\/li>\n\n\n\n<li>El broker de tareas est\u00e1 escuchando.<\/li>\n\n\n\n<li>Los task runners se conectaron sin problemas.<\/li>\n<\/ul>\n\n\n\n<p>Si algo falla (credenciales de <strong>Postgres<\/strong>, puerto ocupado, etc.), los logs te lo dir\u00e1n clarito.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Configurar Nginx como reverse proxy<\/h2>\n\n\n\n<p>No exponemos directamente el puerto 5678 de <strong>n8n<\/strong> a Internet. Usamos <strong>Nginx<\/strong> para que haga de intermediario, maneje <strong>HTTPS<\/strong> y preserve las <strong>IP<\/strong> reales de los clientes.<\/p>\n\n\n\n<p>Instalamos <strong><a href=\"https:\/\/juredev.com\/blog\/2025\/04\/guia-facil-configura-tu-servidor-web-con-nginx-php-8-2-y-mariadb-en-debian-12\/\">Nginx<\/a><\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y nginx<\/code><\/pre>\n\n\n\n<p>Crea el archivo de configuraci\u00f3n en <strong>\/etc\/nginx\/sites-available\/n8n.conf<\/strong> (o el nombre que prefieras):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server {\n    listen 80;\n    server_name ${DOMAIN};   # Sustituye por tu dominio real o usa $DOMAIN si lo defines como variable\n\n    # Redirige todo HTTP \u2192 HTTPS\n    return 301 https:\/\/$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name ${DOMAIN};\n\n    # Proxy a n8n (puerto interno)\n    location \/ {\n        proxy_pass http:\/\/127.0.0.1:5678;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # Soporte WebSocket (imprescindible para el editor en tiempo real)\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n    }\n}<\/code><\/pre>\n\n\n\n<p>Activa y prueba:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ln -s \/etc\/nginx\/sites-available\/n8n.conf \/etc\/nginx\/sites-enabled\/\nsudo nginx -t\nsudo systemctl reload nginx<\/code><\/pre>\n\n\n\n<p>Con esto <strong>n8n<\/strong> ya deber\u00eda ser accesible por tu dominio (todav\u00eda en <strong>HTTP<\/strong>).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Poner HTTPS con Let&#8217;s Encrypt<\/h2>\n\n\n\n<p>Ahora s\u00ed, ciframos todo con un certificado gratuito y confiable.<\/p>\n\n\n\n<p>Instalamos <strong>Certbot<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y certbot python3-certbot-nginx<\/code><\/pre>\n\n\n\n<p>Y obtenemos el certificado (sustituye por tu dominio real):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo certbot --nginx -d app.tudominio.com<\/code><\/pre>\n\n\n\n<p><strong>Certbot<\/strong> va a:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Verificar que el dominio apunta a tu servidor.<\/li>\n\n\n\n<li>Modificar autom\u00e1ticamente la config de <strong>Nginx<\/strong> para usar <strong>HTTPS<\/strong>.<\/li>\n\n\n\n<li>Configurar la renovaci\u00f3n autom\u00e1tica (via <strong>systemd timer)<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p>\u00a1Listo! Desde ahora todo va por HTTPS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Pruebas finales y recomendaciones pr\u00e1cticas<\/h2>\n\n\n\n<p>Ya casi estamos pa la candela. Algunas cosas que deber\u00edas verificar:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Accede por primera vez a <strong>https:\/\/app.tudominio.com<\/strong> &#8211;&gt; el primer usuario que crees ser\u00e1 el owner (administrador con todos los privilegios).<\/li>\n\n\n\n<li>Prueba crear un webhook simple y ejec\u00fatalo desde fuera (Postman, curl, etc.) para confirmar que la URL p\u00fablica funciona.<\/li>\n\n\n\n<li>Revisa que no quede el puerto 5678 abierto hacia Internet (<strong>sudo ss -tuln | grep 5678<\/strong> deber\u00eda mostrar solo 127.0.0.1).<\/li>\n<\/ul>\n\n\n\n<p><strong>Buenas pr\u00e1cticas finales<\/strong> (no te las saltes):<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Nunca expongas directamente el puerto 5678.<\/li>\n\n\n\n<li>Echa un ojo a los logs despu\u00e9s de cada cambio o actualizaci\u00f3n (docker compose logs -f).<\/li>\n\n\n\n<li>Haz backups peri\u00f3dicos: el volumen data, la base <strong>PostgreSQL<\/strong> (<strong>pg_dump<\/strong>), y el <strong>.env<\/strong> con las claves.<\/li>\n\n\n\n<li>Actualiza los contenedores cada 1\u20132 meses de forma controlada: <strong>docker compose pull &amp;&amp; docker compose up -d<\/strong>.<\/li>\n\n\n\n<li>Si ves que crece mucho la carga, considera activar queue mode completo con Redis (pero eso ya es otro nivel).<\/li>\n<\/ul>\n\n\n\n<p>Resultado: tienes una instancia de n8n segura, escalable a nivel b\u00e1sico\/mediano y preparada para producci\u00f3n ligera.<\/p>\n\n\n\n<p>Si quieres profundizar m\u00e1s, la documentaci\u00f3n oficial es excelente: <a href=\"https:\/\/docs.n8n.io\/hosting\/\">https:\/\/docs.n8n.io\/hosting\/<\/a><\/p>\n\n\n\n<p>Listo. Ya tienes una instancia de n8n bien montada, segura y lista para usarse en serio.<\/p>\n\n\n\n<p>Ojal\u00e1 esta gu\u00eda te ahorre tiempo y errores comunes. Si la usas o la adaptas, me encantar\u00eda saber c\u00f3mo te fue. Seguimos administrando.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Si alguna vez te preguntaste c\u00f3mo hacer una instalaci\u00f3n limpia, segura y lista para producci\u00f3n ligera\/mediana de n8n self-hosted en un servidor Debian, usando Docker Compose, PostgreSQL (mucho m\u00e1s recomendable que SQLite si planeas crecer), Nginx como reverse proxy y certificados HTTPS gratuitos de Let\u2019s Encrypt, esta gu\u00eda es para ti. El enfoque es darte [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18],"tags":[24],"class_list":["post-131","post","type-post","status-publish","format-standard","hentry","category-guia","tag-linux"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/131","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=131"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/131\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=131"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=131"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=131"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}