Caption: n8n turns app triggers, webhooks, API calls, data transforms, and human review steps into self-hosted automation workflows.
Introduction
n8n is an open-source workflow automation platform for connecting services, APIs, databases, queues, and internal tools without writing a full application for every integration. A workflow can start from a webhook, schedule, chat event, database change, or manual trigger; pass data through conditional branches and code steps; call third-party APIs; then send notifications, update records, or hand work to a human for review. It sits in the same broad category as Zapier, Make, cron scripts, and internal integration services, but it gives self-hosters more control over where credentials, execution logs, webhook payloads, and workflow definitions live.
Self-hosting n8n is useful when automations touch private systems: CRM records, finance exports, support tickets, source-code events, homelab alerts, customer emails, or internal databases. Running it yourself means you choose the network, database, backup policy, retention settings, credential key, update window, and outbound firewall rules. You can keep sensitive workflows close to the services they automate while still giving non-developers a visual editor for routine operational tasks.
This guide installs n8n on Ubuntu 24.04 LTS using Docker Compose, PostgreSQL, persistent volumes, an external task runner, Nginx, and Let's Encrypt certificates. The example publishes the editor at https://n8n.example.com, stores Compose files under /opt/n8n, exposes the app only on localhost port 5678, and uses Nginx as the public HTTPS reverse proxy. The guide also covers first-run checks, backup commands, updates, and when to move to Redis-backed queue mode.
Why Choose n8n?
- Visual automation builder: Workflows can combine triggers, app nodes, HTTP calls, conditions, loops, code, and data transformations in a browser.
- Open-source control: The self-hosted edition runs on your server with a transparent Docker image, documented environment variables, and exportable workflows.
- API-friendly design: Webhooks, REST calls, database nodes, and custom code make it practical for internal systems that do not have polished SaaS integrations.
- Credential ownership: OAuth tokens, API keys, and database passwords stay in your n8n database and encrypted credential store.
- Docker-first operations: The official docs recommend Docker for most self-hosting needs, and the n8n hosting repository provides Compose examples for PostgreSQL and workers.
- Scales beyond one process: PostgreSQL, external task runners, and optional Redis queue mode give a path from a small VPS to heavier automation workloads.
- Good fit for homelabs and teams: n8n can replace scattered cron jobs, fragile glue scripts, and one-off webhook handlers with a visible run history.
n8n can execute code and connect to many systems, so treat it as trusted automation infrastructure. A compromised n8n instance may expose credentials, send unwanted API calls, change business records, or trigger webhooks repeatedly. The install is straightforward; safe day-two operations are the important part.
Prerequisites
Hardware Recommendations:
- 2 vCPU and 2-4 GB RAM for a personal instance or light team usage
- 4 vCPU and 8 GB RAM for frequent workflow executions, larger data transformations, or many active users
- SSD storage for PostgreSQL data, credential metadata, execution logs, binary files, and backups
- Off-server backup storage such as another VPS, NAS, encrypted object storage bucket, or backup appliance
- Additional CPU and memory if workflows run heavy JavaScript, process large files, or call slow external APIs
Software and Accounts:
- Ubuntu 24.04 LTS server with sudo access
- A domain such as
n8n.example.com - DNS
AorAAAArecord pointing the hostname to the server - Docker Engine with the Docker Compose v2 plugin
- Nginx, Certbot, OpenSSL, curl, ufw, jq, rsync, and PostgreSQL client tools
- A password manager for generated database passwords, runner tokens, encryption keys, SMTP credentials, and recovery notes
Security Notes:
- Put n8n behind HTTPS before creating users, credentials, or production webhooks.
- Set a stable
N8N_ENCRYPTION_KEYand store it outside the server; losing it can make saved credentials unusable. - Keep
/opt/n8n/.env, backups, and exported workflows private. - Limit filesystem access for workflows with
N8N_RESTRICT_FILE_ACCESS_TO=/filesunless you intentionally need broader host file access. - Review outbound network access if workflows should only call approved services.
- Back up PostgreSQL and the n8n data volume before upgrades.
Start from a patched host with a tight firewall:
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw nginx certbot python3-certbot-nginx jq rsync postgresql-client
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Installation Guide
This deployment uses the official docker.n8n.io/n8nio/n8n:stable image with PostgreSQL. PostgreSQL stores workflows, credentials metadata, execution records, users, projects, tags, and settings. The mounted n8n data volume still matters because it stores the settings file, instance data, and other local assets. The external runner follows the current official hosting example and keeps task execution separated from the main process.
1. Install Docker Engine
Install Docker and confirm that Compose v2 is available:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker
docker --version
docker compose version
If docker compose version fails, fix Docker before continuing. These examples use the modern Compose plugin syntax, not the legacy docker-compose Python package.
2. Create the Project Directory
Create a private working directory and a host folder for files that workflows may read or write:
sudo mkdir -p /opt/n8n/local-files
sudo chown -R "$USER":"$USER" /opt/n8n
chmod 700 /opt/n8n
cd /opt/n8n
The local-files directory is mounted inside the container as /files. Keeping workflow file access inside that directory is safer than exposing broad host paths.
3. Generate Environment Values
Create an .env file for Compose interpolation. The commands below generate strong values and set the public domain used by the reverse proxy:
cd /opt/n8n
POSTGRES_PASSWORD="$(openssl rand -base64 36 | tr -d '
')"
POSTGRES_NON_ROOT_PASSWORD="$(openssl rand -base64 36 | tr -d '
')"
N8N_ENCRYPTION_KEY="$(openssl rand -hex 32)"
RUNNERS_AUTH_TOKEN="$(openssl rand -hex 32)"
cat > .env <<EOF
N8N_VERSION=stable
N8N_DOMAIN=n8n.example.com
GENERIC_TIMEZONE=UTC
POSTGRES_USER=n8n_admin
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n_app
POSTGRES_NON_ROOT_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
EOF
chmod 600 .env
Change N8N_DOMAIN before running the stack if your hostname is different. Keep N8N_ENCRYPTION_KEY in your password manager as well as in .env.
4. Create the PostgreSQL Initialization Script
The official PostgreSQL example creates a less-privileged database user during first boot. Save this as init-data.sh:
cat > init-data.sh <<'EOF'
#!/bin/bash
set -e
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No non-root PostgreSQL user variables were provided."
fi
EOF
chmod 700 init-data.sh
This script only runs when the PostgreSQL data volume is initialized. If you change database users later, apply migrations manually or recreate the volume after a verified backup.
5. Create the Docker Compose File
Save this as compose.yaml in /opt/n8n:
volumes:
db_storage:
n8n_storage:
services:
postgres:
image: postgres:16
restart: always
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
volumes:
- db_storage:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh:ro
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_HOST=${N8N_DOMAIN}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_EDITOR_BASE_URL=https://${N8N_DOMAIN}/
- N8N_WEBHOOK_URL=https://${N8N_DOMAIN}/
- N8N_PROXY_HOPS=1
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${GENERIC_TIMEZONE}
- N8N_RESTRICT_FILE_ACCESS_TO=/files
- NODE_ENV=production
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_storage:/home/node/.n8n
- ./local-files:/files
depends_on:
postgres:
condition: service_healthy
n8n-runner:
image: n8nio/runners:${N8N_VERSION}
restart: always
environment:
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
depends_on:
- n8n
Caption: A single-server n8n deployment can keep PostgreSQL, the main application, and an external task runner on a private Docker network while Nginx handles public HTTPS.
Validate the Compose file before starting containers:
cd /opt/n8n
docker compose config --services
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=80 n8n
The service list should include postgres, n8n, and n8n-runner. If PostgreSQL is unhealthy, inspect docker compose logs postgres before restarting repeatedly.
6. Configure Nginx and HTTPS
Create an Nginx server block that forwards traffic to localhost port 5678 and passes proxy headers required by n8n behind a reverse proxy:
sudo tee /etc/nginx/sites-available/n8n >/dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name n8n.example.com;
client_max_body_size 50m;
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-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/n8n
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d n8n.example.com --email admin@example.com --agree-tos --redirect
After Certbot finishes, visit https://n8n.example.com. Create the owner account, store recovery details, and avoid adding real credentials until HTTPS and backups are working.
Configuration
Set the public URL variables deliberately. n8n's reverse-proxy guidance says webhook URLs should use N8N_WEBHOOK_URL, and the instance should know it is one proxy hop behind Nginx with N8N_PROXY_HOPS=1. N8N_EDITOR_BASE_URL helps links in emails and OAuth redirects point at the public editor URL. N8N_HOST, N8N_PROTOCOL, and N8N_PORT describe how the service presents itself.
Keep credentials encrypted with a stable N8N_ENCRYPTION_KEY. If the key changes without a planned rotation, existing saved credentials may no longer decrypt. For production, store .env in a restricted directory, keep a copy of the encryption key in a password manager, and include it in disaster recovery documentation.
File access deserves special care. The example mounts ./local-files to /files and sets N8N_RESTRICT_FILE_ACCESS_TO=/files, so workflows that use file nodes have a predictable sandbox. If a workflow must read another host path, mount only that path and document why it is needed.
For email invitations, password resets, and notification flows, configure SMTP from the n8n environment variable reference after the base deployment works. Test with a non-production account first, because bad SMTP settings can block onboarding or leak workflow alerts to the wrong inbox.
Usage
Caption: A healthy n8n workflow has a clear trigger, credential boundary, transformation step, destination action, and observable execution history.
After the first login, create a small workflow before connecting sensitive systems:
- Add a Manual Trigger node.
- Add a Set or Edit Fields node with a sample message.
- Add an HTTP Request node that calls a safe test endpoint or an internal health-check service.
- Run the workflow manually and inspect the execution data.
- Rename the workflow, add tags, and export a copy from the editor.
Use this checklist before enabling production workflows:
- Confirm the editor loads at
https://n8n.example.comwith a valid certificate. - Confirm webhook test URLs display the public HTTPS hostname, not
localhost:5678. - Confirm
docker compose psshows all services healthy or running. - Confirm a manual workflow can execute through the external runner.
- Create a low-privilege credential for each integration instead of using owner-level API keys.
- Decide how long to retain execution data based on privacy, debugging, and storage needs.
Screenshots and Visuals
The diagrams in this guide are original architecture visuals, not screenshots from the n8n product UI. They are intended to show the operating model: public traffic terminates at Nginx, containers communicate on a private network, PostgreSQL stores durable state, and workflows should be designed with explicit trigger, transform, action, and observation points.
Caption: Protect n8n by combining HTTPS, restricted filesystem mounts, encrypted credentials, database backups, and off-server recovery copies.
Troubleshooting
- Webhook URLs show localhost: Set
N8N_WEBHOOK_URL=https://n8n.example.com/, keepN8N_PROXY_HOPS=1, recreate the n8n container, and confirm Nginx forwardsX-Forwarded-*headers. - Saved credentials fail after restore: Verify that the restored
.envcontains the sameN8N_ENCRYPTION_KEYused when credentials were created. - PostgreSQL is unhealthy: Run
docker compose logs postgres, check thatinit-data.shis executable and mounted, and confirm.envhas matching database names and passwords. - Runner does not execute jobs: Check
docker compose logs n8n-runner, confirmRUNNERS_AUTH_TOKENmatches both services, and verify the broker URI ishttp://n8n:5679on the Compose network. - Uploads or binary data fail: Increase
client_max_body_sizein Nginx if needed, verify the n8n data volume is writable, and check whether workflows are trying to read outside/files. - The editor works but OAuth redirects fail: Confirm the external app uses
https://n8n.example.com/rest/oauth2-credential/callbackstyle redirect URLs from your instance, not an internal hostname. - Disk usage grows quickly: Review execution retention settings, binary data usage, and old backups. Large payloads can fill a small VPS faster than the application itself.
Scaling, Securing, and Next Steps
Back up both PostgreSQL and n8n's data volume. A simple maintenance script can capture a database dump and a compressed copy of the Docker volume mount, then sync both off-server:
cd /opt/n8n
mkdir -p backups
BACKUP_DATE="$(date +%F-%H%M%S)"
docker compose exec -T postgres pg_dump -U n8n_admin -d n8n > "backups/n8n-${BACKUP_DATE}.sql"
docker run --rm -v n8n_n8n_storage:/data -v "$(pwd)/backups:/backup" alpine tar -czf "/backup/n8n-storage-${BACKUP_DATE}.tar.gz" -C /data .
rsync -av --chmod=600 backups/ backupuser@backup.example.com:/srv/backups/n8n/
Test restores on a separate machine. A backup is only useful if the database dump, n8n storage, .env, and encryption key can be restored together.
For updates, take a backup, pull the latest stable image, restart, and watch logs:
cd /opt/n8n
docker compose pull
docker compose down
docker compose up -d
docker compose logs --tail=120 n8n
For heavier workloads, move to queue mode with Redis and workers. The official hosting repository includes a withPostgresAndWorker example that adds Redis, EXECUTIONS_MODE=queue, a worker service, runner services, and queue health checks. Queue mode is useful when long-running workflows should run outside the main editor process or when you need more execution throughput. Keep the same N8N_ENCRYPTION_KEY and runner token across main and worker services.
The outcome of this guide is a private, HTTPS-secured n8n instance with PostgreSQL durability, a separate task runner, predictable file access, and a clear path for backups and scaling. From here, build one production workflow at a time, use least-privilege credentials, export critical workflows after changes, and monitor failures before letting automations touch important systems unattended.