How To Install n8n on Ubuntu 26.04 LTS

Install n8n on Ubuntu 26.04

Every workflow automation project eventually hits the same wall: the free tier of whatever SaaS tool you’re using starts throttling executions, or the pricing model punishes you for scaling. That’s usually the moment sysadmins get pulled into a conversation that starts with “can we just self-host this?” If the tool in question is n8n, the answer is almost always yes — and Ubuntu 26.04 LTS (“Resolute Raccoon”) happens to be a genuinely solid base for it right now.

Ubuntu 26.04 landed on April 23, 2026, and it brought more under-the-hood changes than most LTS releases do — Linux kernel 7.0, GNOME 50, a Wayland-only session, sudo-rs replacing the legacy sudo binary, PostgreSQL 18 in the default repos, and Docker 29 baked into the packaging ecosystem. None of that is cosmetic. A newer kernel means better I/O scheduling and cgroup handling for containerized workloads, and having PostgreSQL 18 and Docker 29 available directly from apt without third-party PPAs makes the whole deployment story cleaner than it was on 24.04.

This guide walks through installing n8n on a fresh Ubuntu 26.04 LTS server — both via Docker (the method most production environments should use) and via npm with Node.js (useful for local development, quick testing, or resource-constrained VPS instances). It also covers reverse proxying with Nginx, TLS termination, systemd service management, firewall rules, and the troubleshooting scenarios that actually show up when n8n is running real automation traffic — not just a demo workflow that fires once a day.

A quick reality check before diving in: n8n is not a lightweight script. It runs a Node.js process, optionally a queue worker, and typically a Postgres database once you move past SQLite for anything beyond a toy deployment. Budget at least 1 vCPU and 2GB of RAM for a hobby instance, and 2 vCPU / 4GB+ if you’re running scheduled workflows with webhook traffic, AI node calls, or large JSON payloads passing through the execution log.

Prerequisites and Server Preparation

Before touching n8n itself, get the base system into a known-good state. Skipping this step is exactly how people end up debugging a “permission denied” error three hours later that has nothing to do with n8n and everything to do with a stale kernel module or a misconfigured DNS resolver.

1. Update the System

sudo apt update && sudo apt full-upgrade -y
sudo reboot

Reboot after a full-upgrade on a fresh LTS release, especially if kernel packages were touched. Ubuntu 26.04’s initial kernel is 7.0, and early point releases often ship kernel bumps within the first few weeks — don’t assume the image you deployed from is fully patched.

2. Create a Dedicated User

Running n8n as root is a bad habit that tends to follow people from testing into production. Create a service account instead:

sudo adduser n8nadmin
sudo usermod -aG sudo n8nadmin
su - n8nadmin

3. Set the Hostname and Timezone

Workflow scheduling in n8n depends on the system clock, and cron-based triggers will silently misfire if your server’s timezone doesn’t match what you assumed when building the workflow.

sudo timedatectl set-timezone Asia/Jakarta
timedatectl

Swap in whatever timezone matches your actual operating region — this trips up more people than you’d expect, particularly teams distributed across timezones who build workflows assuming UTC.

4. Configure the Firewall

Ubuntu ships with ufw as a friendly wrapper around iptables/nftables. Lock things down early rather than as an afterthought:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Note that port 5678 (n8n’s default port) is deliberately not opened here. It should never be exposed directly to the internet — all traffic should route through Nginx, which is covered later.

Method 1: Installing n8n with Docker (Recommended for Production)

Docker is the method n8n’s own documentation leans toward, and for good reason. It isolates n8n’s Node.js runtime from your host system’s dependencies, makes version upgrades a one-line operation, and sidesteps the entire category of “works on my machine” problems that come from Node version drift.

Step 1: Install Docker Engine

Ubuntu 26.04 includes Docker 29 in its default repositories, but the version in the Ubuntu repos tends to lag behind Docker’s own release cadence. Pulling straight from Docker’s official repo is still the better move 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-plugin

Verify the daemon is active and add your user to the docker group so you’re not typing sudo before every command:

sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
docker --version

Step 2: Set Up the Project Directory and Environment File

mkdir -p ~/n8n-docker && cd ~/n8n-docker
mkdir -p ~/.n8n

Create a .env file to keep secrets out of the compose file itself — a habit worth building regardless of what you’re deploying:

cat > .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)
EOF

That N8N_ENCRYPTION_KEY matters more than people realize on first read. n8n uses it to encrypt stored credentials — API keys, OAuth tokens, database passwords baked into 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:

Notice the port binding: 127.0.0.1:5678:5678, not 0.0.0.0:5678:5678. This keeps n8n reachable only from localhost, forcing all external traffic through the reverse proxy where TLS and access control actually live. It’s a small detail, but it’s the difference between “secure by design” and “secure until someone forgets the firewall rule.”

Step 4: Launch the Stack

docker compose up -d
docker compose logs -f n8n

Wait for the log output to show n8n listening on port 5678 before moving on. First boot with a Postgres backend takes a few extra seconds while migrations run — don’t panic if it looks idle for 10-15 seconds.

Method 2: Installing n8n with npm and Node.js

Docker isn’t always the right call — maybe you’re running on a memory-constrained VPS where every extra container’s overhead matters, or you’re doing local node development against the n8n source. For those cases, the npm route still works fine.

Step 1: Install Node.js via NodeSource

n8n officially supports Node.js versions between 20.19 and 24.x inclusive. Anything below 20.19 will refuse to start cleanly since support for Node 18 was dropped after its EOL in April 2025. Node 22 is the safest pick right now — it’s an active LTS line and comfortably within n8n’s supported range.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v

Step 2: Install n8n Globally

sudo npm install -g n8n

Step 3: Run n8n Under a Process Manager

Running n8n start in a raw terminal session means the moment you disconnect, the process dies. Use PM2 instead of a bare screen session — it survives crashes, restarts on reboot, and gives you actual log rotation:

sudo npm install -g pm2
pm2 start n8n --name n8n
pm2 save
pm2 startup

That last command prints a systemd command you need to run with sudo to enable PM2 on boot — copy-paste it exactly as printed, it’s user-specific.

Step 4: Set Environment Variables

Since there’s no .env file being read automatically in this setup, export variables through a systemd override or a shell profile, or better, generate an ecosystem file for PM2:

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 save

Configuring Nginx as a Reverse Proxy

Exposing n8n’s Node process directly to the internet is a bad idea — no HTTP/2, no easy TLS management, and no protection layer between your automation platform and the open web. Nginx solves all of that with about fifteen lines of config.

sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/n8n
server {
    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/Connection headers aren’t decorative — n8n’s editor UI relies on WebSocket connections for real-time execution feedback, and without those two headers set correctly, the UI will load but silently fail to show live execution status. This is a genuinely common support ticket in the n8n community forums, and it’s almost always a missing WebSocket header, not an n8n bug.

Enable the site and reload:

sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Enabling HTTPS with Let’s Encrypt

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d n8n.yourdomain.com

Certbot will handle the certificate issuance and rewrite the Nginx config to redirect HTTP to HTTPS automatically. Confirm auto-renewal is registered:

sudo systemctl list-timers | grep certbot

Security Hardening Beyond the Basics

A working install and a safe install aren’t the same thing. n8n instances that get compromised are almost never breached through some exotic zero-day — it’s exposed ports, default credentials, or weak encryption keys.

  • Enable n8n’s built-in user management. Set N8N_USER_MANAGEMENT_DISABLED=false (or leave it default, since recent versions enable it out of the box) so every login requires authentication rather than relying on network obscurity alone.
  • Rotate the encryption key carefully. Changing N8N_ENCRYPTION_KEY after credentials are already stored breaks every saved credential. If rotation is genuinely needed, export credentials first or plan a full re-authentication pass.
  • Restrict webhook exposure. If a workflow doesn’t need a public webhook, don’t leave the webhook node active — 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.
  • Keep unattended-upgrades running for security patches, but pin n8n and Docker version bumps to manual review — workflow automation platforms change fast enough that blind auto-updates can break active production workflows overnight.

Performance Tuning for Real Workloads

A demo instance running two workflows a day doesn’t need tuning. A production instance processing webhook bursts from an e-commerce platform 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 (EXECUTIONS_MODE=queue) backed by Redis. This decouples webhook ingestion from execution, so a slow workflow doesn’t block incoming requests.

Database performance: SQLite is fine for testing, genuinely not fine for anything with concurrent executions. Postgres, as configured above, 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 if you’re not pruning it. Set EXECUTIONS_DATA_PRUNE=true and EXECUTIONS_DATA_MAX_AGE to a sane retention window (say, 336 hours) — otherwise the Postgres database bloats and query performance on the executions table degrades over months.

Memory: Node.js has a default heap ceiling that can bite you on memory-tight VPS instances running large JSON payloads through workflows. Set NODE_OPTIONS=--max-old-space-size=2048 (adjust to available RAM) if you see out-of-memory crashes during heavy data-transform steps.

Network: If workflows call external APIs heavily, watch outbound connection limits. A default Ubuntu install’s ulimit -n (open file descriptor limit) can become a bottleneck under high webhook concurrency — bump it via /etc/security/limits.conf if you’re seeing EMFILE errors in logs.

Troubleshooting Common Issues

n8n container restarts in a loop. Check docker compose logs n8n first. The most common cause is a database connection failure — either Postgres hasn’t finished initializing yet (the depends_on healthcheck should prevent this, but double-check credentials match between the .env file and what Postgres actually has) or 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, or Nginx routing traffic to the wrong path. Confirm WEBHOOK_URL ends with a trailing slash — n8n is picky about that.

Editor UI loads but executions don’t show live status. Missing WebSocket headers in the Nginx config, covered above. Re-check the proxy_set_header Upgrade and Connection "upgrade" lines.

“EACCES: permission denied” on the npm install path. Happens when npm’s global directory isn’t owned by the current user. Fix it properly rather than reaching for sudo npm install -g as a permanent habit — 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 ~/.bashrc

High CPU usage with no active workflows. Check for a runaway polling trigger — some polling-based nodes (RSS feeds, database change triggers) default to short intervals that can hammer the CPU if misconfigured. Review trigger node intervals under the “Poll Times” setting.

Certbot renewal fails silently. Nginx configuration errors introduced after the initial certbot run (like a duplicate server_name block) can block renewal. Run sudo certbot renew --dry-run periodically to catch this before the cert actually expires.

Best Practices for Long-Term Maintenance

  • Back up the ~/.n8n volume (or the Docker named volume) and the Postgres database on a schedule — pg_dump nightly, 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 latest blindly. n8n ships updates frequently, and not every release is drama-free on upgrade.
  • Monitor with something lightweight — even a simple docker stats cron job piped to a log file catches memory creep before it becomes an outage.
  • Document every custom node or community package 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/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.
r00t is a Linux Systems Administrator and open-source advocate with over ten years of hands-on experience in server infrastructure, system hardening, and performance tuning. Having worked across distributions such as Debian, Arch, RHEL, and Ubuntu, he brings real-world depth to every article published on this blog. r00t writes to bridge the gap between complex sysadmin concepts and practical, everyday application — whether you are configuring your first server or optimizing a production environment. Based in New York, US, he is a firm believer that knowledge, like open-source software, is best when shared freely.

Related Posts