At some point, every workflow automation setup hits a ceiling. Execution limits kick in, pricing tiers punish growth, or you just want full control over your data. Self-hosting n8n on a fresh Ubuntu 26.04 LTS server is a clean way out of that corner.
Ubuntu 26.04 LTS, released April 23, 2026, shipped with Linux kernel 7.0, PostgreSQL 18 and Docker 29 available directly from apt, and sudo-rs replacing the legacy sudo binary. Those aren’t cosmetic changes. A newer kernel means better I/O scheduling for containerized workloads, and having Postgres 18 and Docker 29 in the default repos makes the whole deployment story cleaner than it was on 24.04.
This guide covers two install paths: Docker (recommended for production) and npm with Node.js (useful for local dev or memory-constrained VPS instances). It also covers Nginx reverse proxy setup, TLS with Let’s Encrypt, and the troubleshooting scenarios that show up under real automation traffic.
Before starting: n8n is not a lightweight process. Budget at least 1 vCPU and 2GB RAM for a hobby instance. For scheduled workflows with webhook traffic, AI node calls, or large JSON payloads, plan for 2 vCPU and 4GB or more.
️ Server Preparation
Get the base system into a known-good state before touching n8n. A stale kernel module or misconfigured DNS resolver is responsible for more mysterious install failures than any n8n bug.
Step 1: Update and reboot
sudo apt update && sudo apt full-upgrade -y
sudo rebootReboot after a full upgrade on a fresh LTS release, especially if kernel packages were updated.
Step 2: Create a dedicated service account
Running n8n as root is a habit that tends to follow people from testing into production. Don’t do it.
sudo adduser n8nadmin
sudo usermod -aG sudo n8nadmin
su - n8nadminStep 3: Set timezone
Workflow scheduling depends on the system clock. Cron-based triggers will silently misfire if the server timezone doesn’t match what you assumed when building the workflow.
sudo timedatectl set-timezone Asia/Jakarta
timedatectlStep 4: Configure the firewall
Open SSH, HTTP, and HTTPS. Do not open port 5678. n8n’s default port should never be exposed directly to the internet. All external traffic routes through Nginx.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseMethod 1: Install n8n with Docker (Recommended)
Docker isolates n8n’s Node.js runtime from your host dependencies, makes version upgrades a one-line operation, and sidesteps Node version drift entirely.
Step 1: Install Docker Engine
Ubuntu 26.04 includes Docker 29 in its default repos, but the version there lags Docker’s own release cadence. Pull from Docker’s official repo for production:
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" |
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginsudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
docker --versionStep 2: Set up the project directory and environment file
mkdir -p ~/n8n-docker && cd ~/n8n-docker
mkdir -p ~/.n8ncat > .env <<EOF
N8N_HOST=n8n.yourdomain.com
N8N_PROTOCOL=https
N8N_PORT=5678
WEBHOOK_URL=https://n8n.yourdomain.com/
GENERIC_TIMEZONE=Asia/Jakarta
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=$(openssl rand -base64 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
EOFThe N8N_ENCRYPTION_KEY is critical. n8n uses it to encrypt every stored credential: API keys, OAuth tokens, database passwords inside your workflows. Lose that key and every saved credential in the instance becomes unrecoverable. Back it up somewhere outside the server, not just in the .env file sitting next to the container.
Step 3: Write the Docker Compose file
services:
postgres:
image: postgres:18
restart: unless-stopped
environment:
- POSTGRES_USER=${DB_POSTGRESDB_USER}
- POSTGRES_PASSWORD=${DB_POSTGRESDB_PASSWORD}
- POSTGRES_DB=${DB_POSTGRESDB_DATABASE}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_POSTGRESDB_USER}"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_HOST=${N8N_HOST}
- N8N_PROTOCOL=${N8N_PROTOCOL}
- WEBHOOK_URL=${WEBHOOK_URL}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- DB_TYPE=${DB_TYPE}
- DB_POSTGRESDB_HOST=${DB_POSTGRESDB_HOST}
- DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT}
- DB_POSTGRESDB_DATABASE=${DB_POSTGRESDB_DATABASE}
- DB_POSTGRESDB_USER=${DB_POSTGRESDB_USER}
- DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:The port binding is 127.0.0.1:5678:5678, not 0.0.0.0:5678:5678. That keeps n8n reachable only from localhost, forcing all external traffic through Nginx. It’s the difference between secure by design and secure until someone forgets a firewall rule.
Step 4: Launch the stack
docker compose up -d
docker compose logs -f n8nFirst boot with a Postgres backend takes a few extra seconds while migrations run. Wait for the log output to confirm n8n is listening on port 5678 before moving on. An idle-looking 10 to 15 seconds is normal.
Method 2: Install n8n via npm and Node.js
Docker isn’t always the right call. On a memory-constrained VPS where every extra container’s overhead matters, or for local development against the n8n source, the npm route works fine.
Step 1: Install Node.js via NodeSource
n8n officially supports Node.js versions 20.19 through 24.x. Support for Node 18 was dropped after its EOL in April 2025, so anything below 20.19 won’t start cleanly. Node 22 is the safest pick right now as an active LTS line.
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -vStep 2: Install n8n globally
sudo npm install -g n8nStep 3: Run n8n under PM2
A bare terminal session dies when you disconnect. Use PM2 instead of screen. It survives crashes, restarts on reboot, and provides log rotation.
sudo npm install -g pm2
pm2 start n8n --name n8n
pm2 save
pm2 startupThe pm2 startup command prints a systemd command you need to run with sudo. Copy-paste it exactly as printed. It’s user-specific.
Step 4: Configure environment variables via PM2 ecosystem file
cat > ~/n8n-ecosystem.config.js <<EOF
module.exports = {
apps: [{
name: 'n8n',
script: 'n8n',
env: {
N8N_HOST: 'n8n.yourdomain.com',
N8N_PROTOCOL: 'https',
WEBHOOK_URL: 'https://n8n.yourdomain.com/',
GENERIC_TIMEZONE: 'Asia/Jakarta',
N8N_ENCRYPTION_KEY: 'paste-your-generated-key-here'
}
}]
}
EOF
pm2 delete n8n
pm2 start ~/n8n-ecosystem.config.js
pm2 saveNginx Reverse Proxy Setup
Exposing n8n’s Node process directly to the internet means no HTTP/2, no easy TLS management, and no protection layer between your automation platform and the open web. Nginx fixes all of that.
sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/n8nserver {
listen 80;
server_name n8n.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}The Upgrade and Connection headers aren’t decorative. n8n’s editor UI relies on WebSocket connections for real-time execution feedback. Without those two headers, the UI loads but silently fails to show live execution status. This is one of the most common issues in n8n community forums, and it’s almost always a missing WebSocket header, not an n8n bug.
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxEnable HTTPS with Let’s Encrypt
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d n8n.yourdomain.comCertbot handles certificate issuance and rewrites the Nginx config to redirect HTTP to HTTPS automatically. Confirm auto-renewal is registered:
sudo systemctl list-timers | grep certbotSecurity Hardening
A working install and a safe install aren’t the same thing. n8n instances that get compromised are almost never breached through an exotic zero-day. It’s exposed ports, default credentials, or weak encryption keys.
- Enable user management. Set
N8N_USER_MANAGEMENT_DISABLED=false, or leave it at the default in recent versions which enable it out of the box. Every login should require authentication. - Rotate the encryption key carefully. Changing
N8N_ENCRYPTION_KEYafter credentials are already stored breaks every saved credential. Export credentials first or plan a full re-authentication pass before rotating. - Restrict webhook exposure. If a workflow doesn’t need a public webhook, disable the webhook node. Dormant, forgotten webhooks are a quiet way for automation platforms to become an entry point for abuse.
- Apply fail2ban for the Nginx layer. Brute-force login attempts against the n8n UI look like ordinary HTTP traffic to a naive firewall. fail2ban with an Nginx auth filter catches repeated failed logins at the network edge.
- Pin n8n and Docker version bumps to manual review. Keep
unattended-upgradesrunning for security patches, but blind auto-updates can break active production workflows overnight on a platform that ships updates frequently.
Performance Tuning for Real Workloads
A demo instance running two workflows a day doesn’t need tuning. A production instance processing webhook bursts or syncing data every five minutes does.
- CPU and concurrency: n8n’s default execution mode runs workflows inline within the main process. Under sustained load, switch to queue mode with a separate worker process using
EXECUTIONS_MODE=queuebacked by Redis. This decouples webhook ingestion from execution so a slow workflow doesn’t block incoming requests. - Database: SQLite is fine for testing, not for anything with concurrent executions. Postgres handles concurrent writes far better and won’t lock up under load the way SQLite does when multiple workflows write execution data simultaneously.
- Disk I/O: Execution data accumulates fast without pruning. Set
EXECUTIONS_DATA_PRUNE=trueandEXECUTIONS_DATA_MAX_AGEto a sane retention window, around 336 hours, to prevent the Postgres database from bloating over months. - Memory: Node.js has a default heap ceiling that can cause crashes on memory-tight VPS instances handling large JSON payloads. Set
NODE_OPTIONS=--max-old-space-size=2048(adjusted to available RAM) if you see out-of-memory crashes during heavy data-transform steps. - Network: Watch outbound connection limits under high webhook concurrency. A default Ubuntu install’s
ulimit -ncan become a bottleneck. Bump it via/etc/security/limits.confif you’re seeingEMFILEerrors in logs.
Troubleshooting Common Issues
n8n container restarts in a loop
Run docker compose logs n8n first. The most common cause is a database connection failure. Either Postgres hasn’t finished initializing, or credentials don’t match between the .env file and what Postgres actually has. Also check whether the encryption key changed between restarts.
Webhooks return 404 externally but work on localhost
Almost always a mismatch between WEBHOOK_URL and the actual public domain. Confirm WEBHOOK_URL ends with a trailing slash. n8n is particular about that.
Editor UI loads but live execution status doesn’t appear
Missing WebSocket headers in the Nginx config. Re-check the proxy_set_header Upgrade and Connection "upgrade" lines covered above.
EACCES: permission denied on the npm install path
Don’t reach for sudo npm install -g as a permanent fix. Reconfigure npm’s prefix to a user-owned directory:
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrcHigh CPU with no active workflows
Check for a runaway polling trigger. Some polling-based nodes default to short intervals that hammer the CPU if misconfigured. Review trigger node intervals under the Poll Times setting.
Certbot renewal fails silently
Nginx config errors introduced after the initial certbot run can block renewal. Run sudo certbot renew --dry-run periodically to catch this before the certificate actually expires.
Long-Term Maintenance Checklist
- Back up the
~/.n8nvolume and the Postgres database on a schedule. Apg_dumpnightly, retained for at least two weeks, is a reasonable baseline. - Pin your Docker image tag once you’ve validated a version in production rather than tracking
latestblindly. - Monitor resource usage with something lightweight. Even a simple
docker statscron job piped to a log file catches memory creep before it becomes an outage. - Document every custom or community node installed. Community nodes aren’t held to the same review bar as core nodes, and dependency conflicts during upgrades are easier to diagnose when you know exactly what’s installed.
- Test workflow imports and exports periodically as part of your disaster recovery plan. A database backup alone doesn’t guarantee a clean restore if the encryption key isn’t backed up alongside it.

