Self-Hosting n8n Made Easy: The Complete Guide to Install, Upgrade, Backup & Restore
If you already know about n8n and want to deploy it on your own server (like a VPS) for greater flexibility, privacy, and scalability, this guide is for you.
Maybe you’ve asked yourself: “What’s the best way to install n8n for production on Ubuntu?” — Docker, Traefik, PostgreSQL, SQLite, Nginx… it can get confusing fast.
This guide cuts through the noise, and it will walk you through:
- ✅ How to install n8n on an Ubuntu server using Docker Compose, Traefik, and PostgreSQL.
- ✅How to upgrade n8n safely to the latest version.
- ✅How to back up and restore your entire stack to prevent data loss.
By the end, you’ll fully understand the lifecycle of a production-ready n8n instance—and you’ll be able to install, upgrade, back up, and restore your own deployment without depending on any other platform.
Contents
Prerequisites
Before you dive into self-hosting n8n, it’s important to understand what’s involved. Running your own automation stack means you’re not only installing n8n—you’re also responsible for keeping the server secure, stable, and updated.
✅ 1. Technical Knowledge
Self-hosting n8n requires familiarity with:
- Server setup and configuration – installing software, managing processes, troubleshooting errors.
- Docker and containers – building, running, and maintaining containerized applications.
- Resource management and scaling – adjusting CPU, memory, and storage as workflows grow.
- Security hardening – setting up firewalls, SSL certificates, and secure access.
- n8n configuration – customizing .env files, enabling features like queue mode, handling updates.
- VPS management – provisioning, monitoring, and maintaining a cloud server.
- DNS and domains – pointing your domain/subdomain to the server and configuring records.
✅ 2. Server Requirements
- Operating System: Ubuntu 22.04 LTS (recommended) – ideally on a fresh VPS or dedicated server.
- Minimum resources: 1 vCPU, 2 GB RAM, 20 GB storage (sufficient for testing).
- Production resources: 2+ vCPU, 4+ GB RAM, 50+ GB storage (for stability and larger workflows).
- Access: Root or sudo privileges to install packages and manage Docker.
✅ 3. Domain Name
- A registered domain or subdomain that points to your server’s IP address.
- Example: n8n.yourdomain.com
- Needed for issuing free HTTPS certificates via Let’s Encrypt.
✅ 4. Email Address
- A valid email address (e.g., admin@yourdomain.com) for SSL certificate registration.
- Let’s Encrypt will use this to send renewal reminders and alerts.
What You Need to Run n8n (and Why)
The Production Stack (at a glance):
- Docker + Docker Compose (on Ubuntu): Orchestrates n8n, PostgreSQL, and Traefik into one repeatable, production-ready setup.
- PostgreSQL: Stores workflows, credentials, executions, and logs reliably.
- Traefik (reverse proxy): Handles HTTPS termination, domain routing, and Let’s Encrypt certificates automatically.
- n8n application: The automation engine you’ll access from your browser.
Why Docker Compose on Ubuntu?
The best practice for production is to run n8n using Docker Compose, because it:
- Starts/stops the entire stack with one command.
- Runs each service in its own container for safety and easier debugging.
- Simplifies upgrades and rollbacks.
- Provides resilience with restart policies and persistent volumes.
- Centralizes environment variables, storage, and scaling in one file.
Why PostgreSQL (not SQLite)?
Use PostgreSQL in production instead of SQLite because it:
- Handles multiple workers and higher throughput.
- Supports queue mode and scaling.
- Enables safer backups and migrations.
- Maintains stronger data integrity under load.
Why Traefik (not Nginx or Caddy)?
For HTTPS and routing, Traefik is the best choice for Dockerized n8n because it:
- Auto-discovers containers with Docker labels.
- Manages Let’s Encrypt SSL certificates automatically.
- Supports sticky sessions and load balancing.
- Integrates seamlessly with Docker Compose.
Automating n8n Management with Scripts
Managing n8n manually can be time-consuming — from installation to upgrades, backups, and restores. To make life easier, I’ve built two n8n Manager scripts that automate the entire lifecycle of a self-hosted n8n instance with just few commands.
What it covers:
- Install → Deploy n8n with Docker Compose, PostgreSQL, and Traefik.
- Upgrade → Safely pull the latest n8n image and restart the stack.
- Backup → Archive database, volumes, SSL certificates, and configuration files.
- Restore → Recover your instance from a backup in minutes.
You can find the script here here: https://github.com/thenguyenvn90/n8n
Quick Start
Download the script:
# Install unzip if not available
sudo apt install unzip -y
# Download and extract
curl -L -o n8n.zip https://github.com/thenguyenvn90/n8n/archive/refs/heads/main.zip
unzip n8n.zip
cd n8n-main
# Make scripts executable
chmod +x *.sh
The n8n_manager.sh script is the main tool to install, upgrade, and cleanup your n8n stack. Check detail here n8n Manager – User Guide
Usage: ./n8n_manager.sh [OPTIONS]
Options:
-a, --available
List available n8n versions
* If n8n is running → show all newer versions than current
* If n8n is not running → show top 5 latest versions
-i, --install <DOMAIN>
Install n8n stack with specified domain
Use -v|--version to specify a version
-u, --upgrade <DOMAIN>
Upgrade n8n stack with specified domain
Use -f|--force to force upgrade/downgrade
Use -v|--version to specify a version
-v, --version <N8N_VERSION>
Install/upgrade with a specific n8n version. If omitted/empty, uses latest-stable
-m, --email <SSL_EMAIL>
Email address for Let's Encrypt SSL certificate
-c, --cleanup
Cleanup all containers, volumes, and network
-d, --dir <TARGET_DIR>
/path/to/n8n: your n8n project directory (default: /home/n8n)
-l, --log-level <LEVEL>
Set log level: DEBUG, INFO (default), WARN, ERROR
-h, --help
Show script usage
The n8n_backup_restore.sh script handles backups and restores of your n8n stack, including volumes, database, configs, and optional cloud storage. Check detail here n8n Backup & Restore Guide
Usage: ./n8n_backup_restore.sh [OPTIONS]
Options:
-b, --backup
Perform backup
-f, --force
Force backup even if no changes detected
-r, --restore <FILE>
Restore from backup file
-d, --dir <DIR>
/path/to/n8n project directory (default: /home/n8n)
-m, --email <EMAIL>
Send email alerts to this address
-s, --remote-name <NAME>
Rclone remote name (e.g. gdrive-user)
-n, --notify-on-success
Email on successful completion
-l, --log-level <LEVEL>
Set log level: DEBUG, INFO (default), WARN, ERROR
-h, --help
Show script usage
⚡ Notes
If you prefer to understand what’s happening under the hood, or want to run the commands yourself, the next section covers the manual steps in detail.
n8n Installation (manually)
In this section, we’ll manually set up a production-ready n8n stack on Ubuntu using Docker Compose. The stack will include:
- n8n – the automation platform
- PostgreSQL – database for storing workflows and credentials
- Traefik – reverse proxy for HTTPS and domain routing
- Let’s Encrypt – automatic SSL certificate management
Step 1: Update your server
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufw
Step 2: Install Docker and Docker Compose
Docker lets us run n8n, PostgreSQL, and Traefik in isolated containers. Docker Compose ties them together into a single stack.
# Step 1: Install required dependencies
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
# Step 2: Add Docker’s official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Step 3: Add the Docker repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Step 4: Install Docker Engine and Compose v2
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Step 5: Allow user to run Docker without sudo
sudo usermod -aG docker ${USER}
# Register the `docker` group membership with current session without changing your primary group
exec sg docker newgrp
Step 3: DNS Setup
To make your n8n instance accessible online, create a dedicated subdomain that points to your server’s IP address.
Add the following DNS record in your domain provider’s DNS settings:
Record Type | Name (Subdomain) | Value (Destination) |
---|---|---|
A | n8n (or your chosen subdomain) | <your_server_IP_address> |
Wait a few minutes for DNS propagation, then verify that your domain resolves to your VPS IP.
DOMAIN="n8n.yourdomain.com"; SERVER_IP="$(curl -s https://api.ipify.org)"; \
DNS_IPS="$( (command -v dig >/dev/null && dig +short A "$DOMAIN") || getent ahostsv4 "$DOMAIN" | awk '{print $1}' )"; \
echo "Server IP: $SERVER_IP"; echo "DNS A records for $DOMAIN:"; echo "$DNS_IPS"; \
echo "$DNS_IPS" | grep -Fxq "$SERVER_IP" && echo "✅ PASS: $DOMAIN points to this server" || echo "❌ FAIL: $DOMAIN does not point to this server"
Step 4: Create docker-compose.yml and .env file
4.1 Prepare the project directory
Create your working directory where we store docker-compose.yml and .env file.
mkdir -p /home/n8n
4.2 Create docker-compose.yml
You can copy the docker-compose.yml here or from my GitHub: main/docker-compose.yml
services:
traefik:
image: traefik:v2.11
container_name: traefik
restart: unless-stopped
command:
- "--api.dashboard=false"
# EntryPoints
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
# Providers
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# ACME RESOLVERS (production)
- "--certificatesresolvers.le.acme.email=${SSL_EMAIL}"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.tlschallenge=true"
# Logging while testing
- "--log.level=INFO"
- "--accesslog=true"
# Health check
- "--ping=true"
- "--ping.entrypoint=traefikping"
- "--entrypoints.traefikping.address=:8082"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
networks:
- n8n-network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8082/ping"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 5
postgres:
image: postgres:14
container_name: postgres
restart: unless-stopped
env_file:
- .env
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- n8n-network
healthcheck:
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_IMAGE_TAG:-latest}
container_name: n8n
restart: unless-stopped
env_file:
- .env
volumes:
- n8n-data:/home/node/.n8n
- ./local-files:/files
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --spider -q http://localhost:5678/healthz || exit 1"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 5
labels:
- "traefik.enable=true"
# Router & TLS
- "traefik.http.routers.n8n.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls=true"
- "traefik.http.routers.n8n.tls.certresolver=le"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
- "traefik.http.routers.n8n.middlewares=n8n-headers,n8n-rate,n8n-retry,n8n-compress"
# Security headers (name: n8n-headers)
- "traefik.http.middlewares.n8n-headers.headers.STSSeconds=315360000"
- "traefik.http.middlewares.n8n-headers.headers.browserXSSFilter=true"
- "traefik.http.middlewares.n8n-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.n8n-headers.headers.forceSTSHeader=true"
- "traefik.http.middlewares.n8n-headers.headers.STSIncludeSubdomains=true"
- "traefik.http.middlewares.n8n-headers.headers.STSPreload=true"
# Rate limiting (tune as needed)
- "traefik.http.middlewares.n8n-rate.ratelimit.average=100"
- "traefik.http.middlewares.n8n-rate.ratelimit.burst=50"
- "traefik.http.middlewares.n8n-rate.ratelimit.period=1s"
# Retry and compression
- "traefik.http.middlewares.n8n-retry.retry.attempts=3"
- "traefik.http.middlewares.n8n-compress.compress=true"
networks:
- n8n-network
networks:
n8n-network:
driver: bridge
volumes:
n8n-data:
external: true
postgres-data:
external: true
letsencrypt:
external: true
4.3 Create the .env configuration file
- Copy the .env file here or from my GitHub main/.env and then update the values for your own DOMAIN, SSL_EMAIL, GENERIC_TIMEZONE, STRONG_PASSWORD, and N8N_ENCRYPTION_KEY.
# Create STRONG_PASSWORD
STRONG_PASSWORD="$(openssl rand -base64 16)"
# Create N8N_ENCRYPTION_KEY
STRONG_PASSWORD="$(openssl rand -base64 32)"
- Then the updated .env file will be:
# === USER SETTINGS ===
# Your domain or subdomain where n8n will be reachable from
DOMAIN=n8n.yourdomain.com
# The email address to use for the TLS/SSL certificate creation
SSL_EMAIL=contact@yourdomain.com
# Optional timezone to set which gets used by Cron and other scheduling nodes
# New York is the default value if not set
GENERIC_TIMEZONE=Asia/Ho_Chi_Minh
# === SECRETS (USE STRONG VALUES!)
STRONG_PASSWORD=q4Hqu/jwNldcqvzvf3f/4A==
# Important: n8n credential encryption (persistent across upgrades/restores)
N8N_ENCRYPTION_KEY=LTZl8zNn721OB3x73nGS1QNKu9mXoqblHw0LFr9ifq4=
RUNNERS_AUTH_TOKEN=${STRONG_PASSWORD}
# === n8n BASIC AUTH ===
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=${STRONG_PASSWORD}
# === n8n ENV ===
N8N_IMAGE_TAG=latest
N8N_PORT=5678
NODE_ENV=production
WEBHOOK_URL=https://${DOMAIN}
N8N_HOST=${DOMAIN}
N8N_PROTOCOL=https
# Behind Traefik (HTTPS), secure cookies should be true
N8N_SECURE_COOKIE=true
N8N_EDITOR_BASE_URL=https://${DOMAIN}
N8N_PUBLIC_API_BASE_URL=https://${DOMAIN}
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
# === PostgreSQL connection used by n8n ===
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=${STRONG_PASSWORD}
# === PostgreSQL container ===
POSTGRES_USER=n8n
POSTGRES_PASSWORD=${STRONG_PASSWORD}
POSTGRES_DB=n8n
# === INTERNAL TASK RUNNER ===
N8N_RUNNERS_ENABLED=true
N8N_RUNNERS_MODE=internal
N8N_RUNNERS_MAX_CONCURRENCY=5
N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
Step 5: Launch the n8n stack
# Navigate to project directory
cd /home/n8n
# Create a directory called local-files for sharing files between the n8n instance and the host system
mkdir -p ./local-files
# Let your host user own the folder; n8n runs as user 1000 in the container
chown -R ${SUDO_USER:-$USER}:${SUDO_USER:-$USER} ./local-files
chmod 755 ./local-files
# Validate YAML & env expansion first
docker compose config
# Pull images (optional but recommended)
docker compose pull
# Manual create volume
for v in n8n-data postgres-data letsencrypt; do docker volume create "$v"; done
docker volume ls | grep -E 'n8n-data|postgres-data|letsencrypt'
# Start everything (Traefik, Postgres, n8n)
docker compose -f docker-compose.yml up -d
# Check health
docker ps --format "table {{.Names}}\t{{.Status}}"
# Example logs:
# NAMES STATUS
# n8n Up 19 seconds (healthy)
# postgres Up 26 seconds (healthy)
# traefik Up 26 seconds (healthy)
The Docker Compose file runs three main containers:
- n8n – the automation app itself.
- Traefik – the reverse proxy that terminates TLS/SSL certificates and routes HTTPS traffic to n8n.
- PostgreSQL – the database container for storing workflows, credentials, and execution history.
It also creates and mounts persistent storage volumes:
Name | Type | Container Mount | Purpose |
n8n-data | Volume | /home/node/.n8n | Stores encryption keys, user sessions, and small metadata. |
postgres-data | Volume | /var/lib/postgresql/data | Stores the PostgreSQL database files (all workflows, credentials, etc.). |
letsencrypt | Volume | /letsencrypt | Stores TLS/SSL certificates managed by Traefik. |
./local-files | Bind | /files | A shared folder between host and n8n. Useful for importing/exporting files inside workflows. |
We use external volumes (named Docker volumes) instead of internal container storage so that:
- Data survives container recreation (upgrades, restarts, rebuilds).
- You can backup and restore volumes independently of the running containers.
- Multiple script runs won’t accidentally wipe your database or certificates.
What about ./local-files (bind mount)?
This maps a folder from the host server filesystem into the container to let workflows read/write files at /files
inside n8n, while actually persisting them on the host at ./local-files
.
- Example: upload/download files via workflows, process CSV/JSON, or generate reports into
/files
.
- Since it maps to a host folder (
./local-files
), you can open those files directly on your VPS.
Step 6: Verify the installation
- Visit: https://n8n.yourdomain.com
- Confirm SSL is active (green padlock in the browser).
🎉 Congratulations—you now have a production-ready n8n instance running on Ubuntu with Docker!
Step 7: First-Time Setup & License Activation
Now, open your browser and visit your n8n instance at the domain or hostname you configured (e.g https://n8n.nextgrowth.ai). You’ll be greeted with the n8n owner account setup page, where you can create your admin account with secure credentials.

After creating your owner account, n8n will show a short onboarding form to help tailor the experience. This step is optional and only helps n8n understand its users better. Once you’ve filled it out, click Get started.

Next, you’ll see a page offering a free license key for some advanced features:

To activate, simply enter your email and click Send me a free license key. You’ll receive a key by email that unlocks these features permanently.
After submitting your email, you’ll receive an email from n8n containing your license key.
To activate:
- Copy the license key from the email.
- In your n8n dashboard, go to Settings → Usage → Plan.
- Paste the key into the activation field.
Your advanced features will now be permanently unlocked for your instance.
Upgrading n8n (manually)
Like any software, keeping n8n updated is critical for security, bug fixes, and new features. With Docker, upgrading is straightforward if you follow the right steps.
# Navigate to the directory containing your docker compose file
cd /home/n8n
# Check your current n8n version
docker exec n8n n8n --version
# Pull latest version
docker compose pull
# Stopping and removing existing containers
docker compose down
# Restart the Stack
docker compose up -d
# Check health
docker ps --format "table {{.Names}}\t{{.Status}}"
# Example logs:
# NAMES STATUS
# n8n Up 21 seconds (healthy)
# postgres Up 28 seconds (healthy)
# traefik Up 28 seconds (healthy)
# Check your current n8n version to see new version
docker exec n8n n8n --version
Backup n8n (manually)
Regular backups are essential to prevent data loss and ensure business continuity. In an n8n stack, you need to back up:
- PostgreSQL database (workflows, credentials, execution history)
- n8n application data (credentials encryption key, settings)
- SSL certificates (Let’s Encrypt files via Traefik)
- Configuration files (.env, docker-compose.yml)
- Shared directory local-files
# Navigate to the directory containing your docker compose file
cd /home/n8n
# Check containers are healthy before backup
docker ps --format "table {{.Names}}\t{{.Status}}"
# Make suare all container up and running
# Create backup folder
DATE=$(date +%F_%H-%M-%S)
BACKUP_DIR=/home/n8n/backups/backup_$DATE
mkdir -p "$BACKUP_DIR"
# Backup volumes
for vol in n8n-data postgres-data letsencrypt; do
docker run --rm \
-v ${vol}:/data \
-v "$BACKUP_DIR:/backup" \
alpine sh -c "tar czf /backup/volume_${vol}_$DATE.tar.gz -C /data ."
done
# Backup bind mount volume
tar -czf local-files_backup_$DATE.tar.gz -C /home/n8n local-files
# Dump PostgreSQL database
docker exec postgres pg_dump -U n8n -d n8n > "$BACKUP_DIR/n8n_postgres_dump_$DATE.sql"
# Backup config files
cp /home/n8n/.env "$BACKUP_DIR/.env.bak"
cp /home/n8n/docker-compose.yml "$BACKUP_DIR/docker-compose.yml.bak"
# Compress everything into one archive
tar -czf "/home/n8n/backups/n8n_backup_$DATE.tar.gz" -C "$BACKUP_DIR" .
# Generate checksum (optional)
sha256sum "/home/n8n/backups/n8n_backup_$DATE.tar.gz" > "/home/n8n/backups/n8n_backup_$DATE.tar.gz.sha256"
# Check your backup folder to see the tarball files
ls /home/n8n/backups
# Get the full path of backup tarball
ls /home/n8n/backups/n8n_backup_$DATE.tar.gz
Restore n8n (manually)
In case of system failure or migration, you can restore your n8n stack from backups.
# Navigate to the directory containing your docker compose file
cd /home/n8n
# Extract the archive
RESTORE_DIR=/home/n8n/restore_$(date +%s)
mkdir -p "$RESTORE_DIR"
tar -xzf /home/n8n/backups/n8n_backup_<timestamp>.tar.gz -C "$RESTORE_DIR"
ll "$RESTORE_DIR"
# Now inside $RESTORE_DIR you’ll have:
# local-files_backup_*.tar.gz
# volume_n8n-data_*.tar.gz
# volume_postgres-data_*.tar.gz (or just a SQL dump)
# volume_letsencrypt_*.tar.gz
# n8n_postgres_dump_*.sql (if backup included it)
# .env.bak and docker-compose.yml.bak
# Stop the current stack
docker compose down --volumes --remove-orphans
# Restore .env and docker-compose.yml
cp -f "$RESTORE_DIR/.env.bak" /home/n8n/.env
cp -f "$RESTORE_DIR/docker-compose.yml.bak" /home/n8n/docker-compose.yml
# Restore bind mount volume
tar -xzf "$RESTORE_DIR"/local-files_backup_<timestamp>..tar.gz -C /home/n8n
# Restore external docker volumes
# Only restore volumes (n8n-data, letsencrypt)
for vol in n8n-data letsencrypt; do # omit postgres-data if using SQL dump
VOL_FILE=$(find "$RESTORE_DIR" -name "volume_${vol}_*.tar.gz" -print -quit)
if [[ -n "$VOL_FILE" ]]; then
docker volume rm -f $vol 2>/dev/null || true
docker volume create $vol
docker run --rm -v ${vol}:/data -v "$RESTORE_DIR:/backup" alpine \
sh -c "rm -rf /data/* && tar xzf /backup/$(basename $VOL_FILE) -C /data"
echo "[OK] Restored $vol"
fi
done
# Start Postgres first
docker compose up -d postgres
# Check if postgres in healthy status
docker inspect --format='{{.State.Health.Status}}' postgres
# Drop & recreate the database
DB_NAME=n8n
DB_USER=n8n
docker exec -i postgres psql -U $DB_USER -d postgres -v ON_ERROR_STOP=1 -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${DB_NAME}' AND pid <> pg_backend_pid();"
docker exec -i postgres psql -U $DB_USER -d postgres -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS ${DB_NAME};"
docker exec -i postgres psql -U $DB_USER -d postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};"
docker exec -i postgres psql -U $DB_USER -d $DB_NAME < $RESTORE_DIR/n8n_postgres_dump_*.sql
# Start the full stack
docker compose up -d
# Check health
docker ps --format "table {{.Names}}\t{{.Status}}"
# Example logs:
# NAMES STATUS
# traefik Up 37 seconds (healthy)
# n8n Up 37 seconds (healthy)
# postgres Up About a minute (healthy)
⚠️ Best Practices
- Automate backups (cron + script) for daily or weekly snapshots.
- Test restores periodically to verify data integrity.
- Keep multiple versions (30-day rolling window recommended).
- Store copies in secure, redundant locations.
Frequently Asked Questions (FAQs)
What’s the best way to install n8n on Ubuntu for production?
The recommended method is Docker Compose. This isolates n8n and its dependencies (like PostgreSQL) in containers, making upgrades, scaling, and backups much easier. With Docker Compose, you can also define environment variables, persistent storage, and restart policies—ensuring your n8n instance is robust and production-ready.
How do I secure my n8n instance on Ubuntu with HTTPS?
Use a reverse proxy (Traefik recommended, though Nginx or Caddy also work). Pair it with Let’s Encrypt for free SSL certificates. This ensures all traffic between users and your automation platform is encrypted. Also:
- Set strong credentials.
- Restrict access to the admin interface.
- Keep your containers updated to patch vulnerabilities.
Do I need a separate database for n8n on Ubuntu?
Yes—PostgreSQL is strongly recommended for production. Unlike SQLite (default), PostgreSQL ensures:
- Better persistence and reliability.
- Support for scaling and queue mode.
- Safer backups and easier migrations.
What common mistakes should I avoid when self-hosting n8n?
- Using SQLite in production
- Skipping reliable backup strategies
- Lacking error monitoring
- Over-relying on third-party AIs or automated advice. These errors can lead to data loss, downtime, or costly failures.
Why might webhooks fail in a self-hosted setup?
If n8n doesn’t know its public address (missing WEBHOOK_URL), it can generate incorrect local URLs like localhost:5678 that external services can’t reach. Make sure this variable matches your external domain.
How do I recover if the server goes down or I lose data?
You’ll need to rely on your backup strategy: PostgreSQL dumps, backups of volumes, SSL certs, .env, and Compose files. These let you restore quickly. Without backups, you risk permanent data loss and prolonged downtime.
Conclusion
Self-hosting n8n on Ubuntu with Docker, PostgreSQL, and Traefik gives you full control over your automation platform. You’ve learned how to:
- Install a production-ready stack with HTTPS.
- Safely upgrade n8n without losing workflows.
- Back up and restore your system to protect against failures.
- Troubleshoot common issues and follow best practices.
With this setup, your automation platform is secure, flexible, and future-proof.
But let’s be honest—managing servers, SSL, databases, and backups isn’t everyone’s favorite task. If you’d rather focus on building workflows instead of wrestling with infrastructure, I can help.
Professional n8n Deployment & Support
I offer a done-for-you n8n setup and ongoing support service, so you get the benefits of self-hosting without the headaches.
Here’s what I provide:
- ✅ Deploy and run n8n on your personal VPS.
- ✅ Set up PostgreSQL database with secure configuration.
- ✅ Add firewall rules to protect your server.
- ✅ Automated daily backups directly on your VPS.
- ✅ Auto-upload backups to Google Drive for safe offsite storage.
- ✅ Step-by-step guides on how to upgrade, back up, or restore on your own.
- ✅ Ongoing support if you hit issues after deployment.
💡 Think of it as having your own “n8n DevOps partner” — I handle the heavy lifting so you can focus on building powerful automations.
Contact me via this form: