{"id":561,"date":"2026-05-23T19:46:03","date_gmt":"2026-05-23T18:46:03","guid":{"rendered":"https:\/\/hostcreed.com\/blog\/self-hosted-n8n-vps-guide\/"},"modified":"2026-05-23T19:46:03","modified_gmt":"2026-05-23T18:46:03","slug":"self-hosted-n8n-vps-guide","status":"publish","type":"post","link":"https:\/\/hostcreed.com\/blog\/self-hosted-n8n-vps-guide\/","title":{"rendered":"Self-Hosted n8n on a VPS: A Practical Guide to Keeping Your Workflows Safe"},"content":{"rendered":"<h2>Why Self-Host n8n? It&#8217;s About Trust and Control<\/h2>\n<p>We&#8217;ve been using n8n for years now. It&#8217;s a beast of a tool for workflow automation\u2014think Zapier meets cron, but open source. Most people run it on their laptop or a shared server, but that&#8217;s fine until you need real security. Self-hosting n8n on a VPS isn&#8217;t just about bragging rights. It&#8217;s about control. Your API keys, your data, your business logic\u2014none of it leaves your infrastructure. That matters when you&#8217;re dealing with sensitive integrations or just don&#8217;t want to rely on someone else&#8217;s uptime.<\/p>\n<h2>Choosing the Right VPS: Start Small, Scale Smart<\/h2>\n<p>You don&#8217;t need a monster machine. We usually start with 2 vCPUs, 4GB RAM, and 40GB SSD. Hetzner&#8217;s CX22 or DigitalOcean&#8217;s $12\/month plan works well. Avoid overprovisioning\u2014n8n is lightweight, but it&#8217;s the database and reverse proxy that eat resources. PostgreSQL will happily run on 2GB if you&#8217;re not doing crazy queries. Always pick a provider with good EU or US presence. Latency matters when you&#8217;re triggering webhooks. We often use Hetzner&#8217;s ARM instances\u2014CX22 with 2 vCPUs and 4GB RAM\u2014for cost savings. The performance is fine for n8n. DigitalOcean&#8217;s $12 plan is also solid. Avoid overprovisioning; n8n&#8217;s CPU usage is low, but the database can spike during heavy workflows.<\/p>\n<h2>OS Setup: Ubuntu or Debian, No Exceptions<\/h2>\n<p>Ubuntu 22.04 LTS or Debian 12. Both are rock-solid. We avoid CentOS\u2014it&#8217;s aging. Install Docker and Docker Compose first. That&#8217;s non-negotiable. Even if you later use npm, Docker makes updates a breeze. Update the system, install Docker, and then we&#8217;re off to the races. Always run `sudo apt update &#038;&#038; sudo apt upgrade -y` before anything else.<\/p>\n<h2>Installation: Docker Is the Way<\/h2>\n<p>Docker is the way. We use a `docker-compose.yml` file. Here&#8217;s a stripped-down example:<\/p>\n<pre><code>version: '3'\nservices:\n  n8n:\n    image: n8nio\/n8n:latest\n    ports:\n      - \"5678:5678\"\n    environment:\n      - N8N_BASIC_AUTH_ACTIVE=true\n      - N8N_BASIC_AUTH_USER=admin\n      - N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}\n      - N8N_HOST=${N8N_HOST}\n      - N8N_PROTOCOL=https\n    volumes:\n      - n8n_data:\/home\/node\/.n8n\nvolumes:\n  n8n_data:\n<\/code><\/pre>\n<p>Replace `${N8N_PASSWORD}` with a strong, randomly generated password. Use `openssl rand -base64 32` to generate one. Never use &#8220;admin&#8221; or &#8220;password&#8221;. We learned that the hard way after a brute-force attempt. Pin the version in your `docker-compose.yml` to avoid surprises. Example: `image: n8nio\/n8n:1.20.3`. This prevents automatic updates that might break things.<\/p>\n<h2>Firewall and SSH Hardening: Lock It Down<\/h2>\n<p>UFW is your friend. Allow SSH (port 22) and HTTPS (port 443). Block everything else. `sudo ufw allow OpenSSH` and `sudo ufw allow &#8216;Nginx Full&#8217;`. Then enable it with `sudo ufw enable`. Disable root login in SSH\u2014use a non-root user with sudo privileges. Keys only. No passwords. Install fail2ban to block brute-force SSH attempts. It watches logs and bans IPs after too many failures. `sudo apt install fail2ban` and enable it. It&#8217;s a must for any public-facing server.<\/p>\n<h2>Reverse Proxy with Nginx and Let&#8217;s Encrypt: HTTPS Everywhere<\/h2>\n<p>Nginx with Let&#8217;s Encrypt. Get a domain first. We use Cloudflare DNS, but any registrar works. Point your domain to your VPS IP. Then install Certbot:<\/p>\n<pre><code>sudo apt install certbot python3-certbot-nginx\nsudo certbot --nginx -d yourdomain.com\n<\/code><\/pre>\n<p>This auto-renews every 90 days. In your Nginx config, set up a server block:<\/p>\n<pre><code>server {\n    listen 80;\n    server_name yourdomain.com;\n    return 301 https:\/\/$server_name$request_uri;\n}\n\nserver {\n    listen 443 ssl http2;\n    server_name yourdomain.com;\n\n    ssl_certificate \/etc\/letsencrypt\/live\/yourdomain.com\/fullchain.pem;\n    ssl_certificate_key \/etc\/letsencrypt\/live\/yourdomain.com\/privkey.pem;\n\n    location \/ {\n        proxy_pass http:\/\/localhost: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}\n<\/code><\/pre>\n<p>That&#8217;s it. No more exposing port 5678. Everything goes through HTTPS. Enable HSTS for extra security\u2014`add_header Strict-Transport-Security &#8220;max-age=31536000; includeSubDomains&#8221; always;`. Rate limiting in Nginx helps too. Add `limit_req_zone $binary_remote_addr zone=one:10m;` and `limit_req zone=one burst=10 nodelay;` to the location block to prevent abuse.<\/p>\n<h2>Environment Variables and Secrets: Keep It Secure<\/h2>\n<p>Use a `.env` file in your project directory. Never commit it to Git. Store API keys, database passwords, and the n8n password there. Example:<\/p>\n<pre><code>N8N_HOST=yourdomain.com\nN8N_PROTOCOL=https\nN8N_BASIC_AUTH_USER=admin\nN8N_BASIC_AUTH_PASSWORD=your_generated_password\nN8N_ENCRYPTION_KEY=your_encryption_key\n<\/code><\/pre>\n<p>The encryption key is crucial. It&#8217;s used to encrypt credentials in n8n. Generate it with `openssl rand -base64 32` and keep it safe. If you lose it, you lose access to encrypted data. Use Docker secrets for sensitive data. In Docker Compose, create a `secrets\/n8n_password.txt` file and reference it: `environment: &#8211; N8N_BASIC_AUTH_PASSWORD_FILE=\/run\/secrets\/n8n_password`.<\/p>\n<h2>Database Choice: PostgreSQL for Production<\/h2>\n<p>PostgreSQL is recommended for production. It scales better and handles concurrent writes. Use Docker Compose to add a PostgreSQL service:<\/p>\n<pre><code>  postgres:\n    image: postgres:15\n    environment:\n      POSTGRES_USER=n8n\n      POSTGRES_PASSWORD=${DB_PASSWORD}\n      POSTGRES_DB=n8n\n    volumes:\n      - postgres_data:\/var\/lib\/postgresql\/data\n<\/code><\/pre>\n<p>Then in your n8n service, set:<\/p>\n<pre><code>environment:\n  - DB_TYPE=postgresdb\n  - DB_POSTGRESDB_HOST=postgres\n  - DB_POSTGRESDB_PORT=5432\n  - DB_POSTGRESDB_DATABASE=n8n\n  - DB_POSTGRESDB_USER=n8n\n  - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}\n<\/code><\/pre>\n<p>For smaller setups, SQLite is fine. It&#8217;s simpler, but not great for high traffic. PostgreSQL connection pooling with PgBouncer can help if you have many concurrent workflows. But for most setups, the default connection limits are fine.<\/p>\n<h2>Backup Strategies: Automate and Test<\/h2>\n<p>Automate it. Use `cron` to run a daily backup. We use `rsync` to copy the volume to an offsite location. Or use `duplicity` for encrypted backups. Example cron job:<\/p>\n<pre><code>0 2 * * * rsync -avz \/path\/to\/n8n_data\/ \/backup\/location\/\n<\/code><\/pre>\n<p>Store backups in another cloud provider or a different VPS. Don&#8217;t keep them on the same machine. Test your backups monthly. Restore a copy to a test server and verify data integrity. We once found a corrupted backup after six months\u2014don&#8217;t be us.<\/p>\n<h2>Monitoring and Logging: Stay Alert<\/h2>\n<p>Check logs with `docker logs n8n`. Use `pm2` or `systemd` for process management if you&#8217;re not using Docker. Monitor resource usage with `htop` or `netdata`. Set up alerts for high CPU or disk usage. We run Uptime Kuma on a separate container to monitor the n8n instance. It checks the UI every 5 minutes and alerts us if it&#8217;s down. Pair it with a Slack webhook for instant notifications.<\/p>\n<h2>Keeping n8n Updated: Pin Versions, Plan Ahead<\/h2>\n<p>With Docker, just pull the latest image: `docker-compose pull &#038;&#038; docker-compose up -d`. Pin the version in your `docker-compose.yml` to avoid surprises. Example: `image: n8nio\/n8n:1.20.3`. This prevents automatic updates that might break things. Before updating, check the n8n changelog. Some updates deprecate environment variables or change database schemas. We once updated and had to adjust our workflow triggers\u2014planning avoids that.<\/p>\n<h2>Security Best Practices: Beyond the Basics<\/h2>\n<p>Regularly update Docker images, OS packages, and n8n itself. Limit access to the n8n UI\u2014use basic auth or, better yet, IP whitelisting. Review integrations for permission creep. If a workflow doesn&#8217;t need write access, don&#8217;t grant it. Audit logs help here\u2014n8n logs user actions. Enable them. Limit webhook exposure. Only allow necessary IP ranges or use signed requests. n8n&#8217;s webhook node can be configured to require a secret token. Use it. Rate limiting in Nginx helps too. Add `limit_req_zone $binary_remote_addr zone=one:10m;` and `limit_req zone=one burst=10 nodelay;` to the location block to prevent abuse.<\/p>\n<h2>Conclusion: It&#8217;s Worth the Effort<\/h2>\n<p>Self-hosting n8n on a VPS isn&#8217;t hard. It&#8217;s a bit of upfront work, but it pays off in control and security. We&#8217;ve been doing it for over a year with zero incidents. The key is to treat it like any other production system: harden the server, automate backups, and monitor everything. Once you set it up, it just works. And that&#8217;s the point.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Running n8n yourself isn&#8217;t just about control\u2014it&#8217;s about keeping your data under lock and key. Here&#8217;s how we do it right.<\/p>\n","protected":false},"author":1,"featured_media":560,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[67],"tags":[268],"class_list":["post-561","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-hosting-tutorials","tag-workflow-automation"],"_links":{"self":[{"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/posts\/561","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/comments?post=561"}],"version-history":[{"count":0,"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/posts\/561\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/media\/560"}],"wp:attachment":[{"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/media?parent=561"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/categories?post=561"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hostcreed.com\/blog\/wp-json\/wp\/v2\/tags?post=561"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}