DEV Community

How to Install n8n on an Ubuntu Server (Production-Ready)

n8n is an open-source workflow automation platform. It lets you connect services, APIs, and databases through a visual editor without writing hundreds of lines of code — though you can still use JavaScript or Python when custom logic is needed.

In this guide, you'll deploy n8n on an Ubuntu server using Docker Compose, with PostgreSQL as the database, Redis as the queue manager, dedicated workers for background execution, Caddy as a reverse proxy with automatic SSL, and best practices for a production environment.

Architecture

Diagram made with https://savnet.co


Prerequisites

  • A server running Ubuntu 22.04 or 24.04 (1 vCPU, 2 GB RAM minimum; 2 vCPU, 4 GB RAM recommended)
  • Root access for initial setup
  • A domain or subdomain pointing (A record) to your server's IP
  • Ports 80 and 443 open in the firewall
  • Basic command-line knowledge

Don't have a server yet?

If you're looking for affordability and good performance, DigitalOcean is a solid choice. You can get started with this referral link:

https://m.do.co/c/2c579acd7121


1. Initial Connection and System Update

SSH in as root and update the system packages:

ssh root@your-server
apt update && apt upgrade -y
Enter fullscreen mode Exit fullscreen mode

2. Create a Dedicated Admin User

We'll create an n8n user with sudo privileges. From this point on, all work will be done with this user, not root.

# Create the user
adduser --gecos "" n8n

# Add to sudo group
usermod -aG sudo n8n

# Verify
id n8n
Enter fullscreen mode Exit fullscreen mode

Before logging out, copy your public key from root to the new user so you can connect directly as n8n:

# Copy authorized SSH key from root to n8n
mkdir -p /home/n8n/.ssh
cp ~/.ssh/authorized_keys /home/n8n/.ssh/authorized_keys
chown -R n8n:n8n /home/n8n/.ssh
chmod 700 /home/n8n/.ssh
chmod 600 /home/n8n/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Now log out and reconnect as n8n:

exit
ssh n8n@your-server
Enter fullscreen mode Exit fullscreen mode

Why a dedicated user?

Running services directly as root is a bad security practice. If someone compromises the n8n process, they'd have full access to the server. A dedicated user limits the potential damage.


3. Install Docker and Docker Compose

# Required packages
sudo apt install -y ca-certificates curl gnupg

# Add Docker's official GPG key
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

# Add repository
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

# Install Docker Engine and Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Add your user to the docker group (to avoid using sudo with every command)
sudo usermod -aG docker $USER

# Verify
docker --version
docker compose version

# Reboot to apply group changes
sudo init 6
ssh n8n@your-server
Enter fullscreen mode Exit fullscreen mode

4. Create Directory Structure

sudo mkdir -p /opt/n8n/{data/postgres,data/redis,data/n8n,data/caddy,backups}
sudo chown -R $USER:$USER /opt/n8n/data/{postgres,redis,data/caddy,backups}

# The data/n8n directory must belong to UID 1000 (the 'node' user inside the container)
sudo chown -R 1000:1000 /opt/n8n/data/n8n
cd /opt/n8n
Enter fullscreen mode Exit fullscreen mode

5. Configure Environment Variables

Create a .env file to store sensitive configuration:

Tips before you start:

  • Make sure your domain points to the server. Before proceeding, go to your DNS provider and add an A record pointing to your server's IP. Caddy needs to resolve the domain to obtain the SSL certificate.
  • Generate a secure PostgreSQL password and an encryption key for n8n with these commands:
  openssl rand -base64 24   # For POSTGRES_PASSWORD
  openssl rand -hex 20      # For N8N_ENCRYPTION_KEY
Enter fullscreen mode Exit fullscreen mode

Save the values for the .env file.

nano /opt/n8n/.env
Enter fullscreen mode Exit fullscreen mode

Contents:

# Domain
N8N_DOMAIN=n8n.yourdomain.com

# PostgreSQL
POSTGRES_USER=n8n
POSTGRES_PASSWORD=your_secure_postgres_password
POSTGRES_DB=n8n

# n8n
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=your_secure_postgres_password
N8N_ENCRYPTION_KEY=your_encryption_key
N8N_HOST=${N8N_DOMAIN}
N8N_PROTOCOL=https
WEBHOOK_URL=https://${N8N_DOMAIN}/
N8N_PORT=5678

# Queue mode (Redis)
EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis
QUEUE_BULL_REDIS_PORT=6379
Enter fullscreen mode Exit fullscreen mode

Protect the file:

chmod 600 /opt/n8n/.env
Enter fullscreen mode Exit fullscreen mode

6. Create the docker-compose.yml File

nano /opt/n8n/docker-compose.yml
Enter fullscreen mode Exit fullscreen mode
services:
  postgres:
    image: postgres:16-alpine
    container_name: n8n-postgres
    restart: unless-stopped
    env_file: .env
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - n8n-network

  redis:
    image: redis:7-alpine
    container_name: n8n-redis
    restart: unless-stopped
    volumes:
      - ./data/redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - n8n-network

  n8n:
    image: n8nio/n8n:latest
    container_name: n8n-app
    restart: unless-stopped
    env_file: .env
    environment:
      - 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}
      - N8N_HOST=${N8N_HOST}
      - N8N_PROTOCOL=${N8N_PROTOCOL}
      - WEBHOOK_URL=${WEBHOOK_URL}
      - N8N_PORT=${N8N_PORT}
      - N8N_METRICS=true
      - EXECUTIONS_DATA_PRUNE=true
      - EXECUTIONS_DATA_MAX_AGE=168
      - EXECUTIONS_MODE=${EXECUTIONS_MODE}
      - QUEUE_BULL_REDIS_HOST=${QUEUE_BULL_REDIS_HOST}
      - QUEUE_BULL_REDIS_PORT=${QUEUE_BULL_REDIS_PORT}
    volumes:
      - ./data/n8n:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - n8n-network

  n8n-worker:
    image: n8nio/n8n:latest
    container_name: n8n-worker
    restart: unless-stopped
    env_file: .env
    environment:
      - EXECUTIONS_MODE=${EXECUTIONS_MODE}
      - QUEUE_BULL_REDIS_HOST=${QUEUE_BULL_REDIS_HOST}
      - QUEUE_BULL_REDIS_PORT=${QUEUE_BULL_REDIS_PORT}
      - 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}
    command: worker
    depends_on:
      - postgres
      - redis
    networks:
      - n8n-network

  caddy:
    image: caddy:2-alpine
    container_name: n8n-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data/caddy:/data
      - ./data/caddy:/config
    depends_on:
      - n8n
    networks:
      - n8n-network

networks:
  n8n-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Why use queue mode from the start?

n8n can run workflows in two ways:

  • Direct mode (default): the main container handles everything. If a workflow takes too long, it blocks subsequent executions.
  • Queue mode: jobs are queued in Redis and processed by independent workers. You can have multiple workers, scale horizontally, and heavy executions won't affect the main container.

With the docker-compose.yml above, queue mode is active from the first docker compose up.


7. Create the Caddyfile

nano /opt/n8n/Caddyfile
Enter fullscreen mode Exit fullscreen mode
n8n.yourdomain.com {
    reverse_proxy n8n:5678
}
Enter fullscreen mode Exit fullscreen mode

Caddy will automatically obtain an SSL certificate from Let's Encrypt and renew it without manual intervention.


8. Configure the Firewall (UFW)

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status verbose
Enter fullscreen mode Exit fullscreen mode

9. Start the Stack

cd /opt/n8n
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

docker compose up

Verify all services are running:

docker compose ps
Enter fullscreen mode Exit fullscreen mode

You should see something like:

docker compose ps

Check the worker logs to confirm it's picking up jobs:

docker compose logs -f n8n-worker
Enter fullscreen mode Exit fullscreen mode

docker compose logs


10. Access n8n and Initial Setup

Open your browser at https://n8n.yourdomain.com. You'll see the registration screen. Create your admin account:

  • Email: your email
  • Full name: your name
  • Password: a strong password

n8n dashboard

You're ready to start creating workflows. All workflows will run through the worker.


11. Scale Workers

If your workflows grow, scaling is as simple as adding more replicas of the n8n-worker service:

docker compose up -d --scale n8n-worker=3
Enter fullscreen mode Exit fullscreen mode

Each additional worker picks up jobs from the same Redis queue. No other changes needed.


13. Production Best Practices

  • Use a fixed tag: Instead of n8nio/n8n:latest, use a specific version like n8nio/n8n:1.80.3 to avoid surprises from updates.
  • Monitoring: n8n exposes Prometheus metrics at /metrics. You can integrate them with a monitoring stack.
  • Regular backups: Schedule a cron job to back up the data/postgres, data/redis, data/n8n, and data/caddy directories. Since they're bind mounts, you can copy them directly with rsync or tar.
  • Security: Don't expose port 5678 directly. Caddy handles the proxy. If you need local access, use a VPN or SSH tunnels.
  • Execution pruning: The EXECUTIONS_DATA_PRUNE=true and EXECUTIONS_DATA_MAX_AGE=168 variables automatically delete old executions after 7 days.

Conclusion

You now have n8n running in production on Ubuntu with PostgreSQL as a persistent database, Redis as the queue manager, dedicated workers, automatic SSL with Caddy, and a structure ready to scale. All running under a dedicated system user — not root.

This queue-mode setup is the same one we use in real-world environments to automate client processes that require high availability and predictable performance, from notifications and CRMs to complex integrations with external APIs.

If you work with networks or visual infrastructure documentation, you can use Savnet to design diagrams for your deployed workflows. And if you need to continuously validate or test automations, SavFalconEye helps maintain confidence in every deployment.


Any questions? Drop them in the comments.

Top comments (0)