Self-hosting n8n for automation
Self-host n8n to run automations on your own infrastructure. This guide walks through installing n8n with Docker, securing credentials with encryption keys, configuring webhooks and DNS, and backing up your workflows and data.
Self-hosting n8n for automation
You self-host n8n by running the official Docker image with persistent storage and a database, then placing it behind a reverse proxy with TLS. Configure a long encryption key before first run, set your public editor and webhook URLs, point DNS at your server, and back up workflows and credentials with the n8n CLI. This guide covers a production-friendly Compose stack with PostgreSQL and the settings that keep your secrets safe.
Before you start
This picks up from a server you can already reach over SSH. We assume you have a Hostworld VPS running AlmaLinux 9 or Ubuntu 24.04. You manage power, rebuilds and console access in Virtualizor from the Hostworld client area. If you need a server, see our Linux VPS in London or New York.
- Plan a subdomain such as n8n.example.com. You will create a DNS A record pointing it at your VPS public IP. A subdomain is preferred over a path prefix.
- Install Docker Engine and the Docker Compose plugin from Docker’s official repositories:
- Ubuntu 24.04: follow Docker’s Ubuntu install guide.
- AlmaLinux 9: follow Docker’s RHEL guide for Engine and the Compose plugin install.
- Choose PostgreSQL for production. SQLite is built in but stores its database at
~/.n8n/database.sqliteand is not recommended at scale. MySQL and MariaDB are not supported in n8n 2.x. - Decide where to persist data. You must mount a volume to
/home/node/.n8ninside the n8n container. This path holds the encryption key and other critical state. If you start n8n without the original files in this path, stored credentials will become unreadable. - Set a long random
N8N_ENCRYPTION_KEYbefore the first run and keep it safe. n8n will auto-generate a key on first startup and store it under~/.n8n. Changing or losing the key later breaks decryption of all saved credentials. In multi-worker mode the same key must be used by every worker. - Plan TLS. Put n8n behind a reverse proxy and terminate HTTPS there. You will set
N8N_EDITOR_BASE_URLandN8N_WEBHOOK_URLto your publichttps://n8n.example.com. When using a proxy, forwardX-Forwarded-For,X-Forwarded-HostandX-Forwarded-Protoand setN8N_PROXY_HOPS=1. - Decide on email. n8n has built-in login and roles. SMTP is optional, but without it users cannot reset passwords. Plan user invites and note the limitation if you skip SMTP.
- Retention. Executions can bloat the database. You can enable pruning via environment variables. For SQLite, a VACUUM is needed to reclaim disk space. We will show the key options.
- Upgrades. Do not change your PostgreSQL image tag across major versions without a proper database upgrade. Postgres will refuse to open older data directories. Use
pg_dumporpg_upgradeas per Postgres docs. The n8n install guide also calls this out.
Step 1: Create a project directory with secure permissions
This creates a directory for your Compose project and limits access to your user.
AlmaLinux 9:
mkdir -p ~/n8n && chmod 700 ~/n8n
Ubuntu 24.04:
mkdir -p ~/n8n && chmod 700 ~/n8n
Step 2: Write a Docker Compose file for n8n with PostgreSQL
This creates a production-friendly Compose stack that runs PostgreSQL for storage and persists both the database and n8n’s internal state at /home/node/.n8n. It also sets the public URLs and encryption key environment variables used by n8n.
Replace n8n.example.com and the placeholder password and key with your values. Use a long random string for N8N_ENCRYPTION_KEY. Keep the PGDATA line to pin Postgres’s data directory across upgrades.
AlmaLinux 9:
cat > ~/n8n/docker-compose.yml <<'YAML'
services:
postgres:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: change-me-strong-db-password
POSTGRES_DB: n8n
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- db_storage:/var/lib/postgresql/data
networks:
- n8n_net
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
depends_on:
- postgres
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: change-me-strong-db-password
DB_POSTGRESDB_SCHEMA: public
# Public URLs
N8N_EDITOR_BASE_URL: https://n8n.example.com
N8N_WEBHOOK_URL: https://n8n.example.com
N8N_PROXY_HOPS: 1
# Security and limits
N8N_ENCRYPTION_KEY: change-me-long-random-key
N8N_PAYLOAD_SIZE_MAX: 16mb
# Execution retention (tune to your needs)
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "720" # hours, example: 30 days
EXECUTIONS_DATA_PRUNE_MAX_COUNT: "1000" # prune in batches
volumes:
- n8n_storage:/home/node/.n8n
ports:
- "5678:5678" # behind a reverse proxy you can remove this public bind
networks:
- n8n_net
volumes:
db_storage:
n8n_storage:
networks:
n8n_net:
driver: bridge
YAML
Ubuntu 24.04:
cat > ~/n8n/docker-compose.yml <<'YAML'
services:
postgres:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: change-me-strong-db-password
POSTGRES_DB: n8n
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- db_storage:/var/lib/postgresql/data
networks:
- n8n_net
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
depends_on:
- postgres
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: change-me-strong-db-password
DB_POSTGRESDB_SCHEMA: public
# Public URLs
N8N_EDITOR_BASE_URL: https://n8n.example.com
N8N_WEBHOOK_URL: https://n8n.example.com
N8N_PROXY_HOPS: 1
# Security and limits
N8N_ENCRYPTION_KEY: change-me-long-random-key
N8N_PAYLOAD_SIZE_MAX: 16mb
# Execution retention (tune to your needs)
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "720"
EXECUTIONS_DATA_PRUNE_MAX_COUNT: "1000"
volumes:
- n8n_storage:/home/node/.n8n
ports:
- "5678:5678"
networks:
- n8n_net
volumes:
db_storage:
n8n_storage:
networks:
n8n_net:
driver: bridge
YAML
Notes:
- Keep
/home/node/.n8nmounted. It contains the encryption key and other files n8n needs to decrypt credentials. Do not changeN8N_ENCRYPTION_KEYafter first run. - The example exposes port 5678. In production place n8n behind a reverse proxy on 80 and 443 and limit direct exposure.
- If you copy scripts from the n8n-hosting repository on a Windows machine, save files with LF line endings. CRLF endings prevent Postgres init scripts from running.
Step 3: Start n8n and PostgreSQL
This pulls the container images, creates named volumes for persistence, and starts the services in the background.
AlmaLinux 9:
cd ~/n8n
docker compose pull
docker compose up -d
Ubuntu 24.04:
cd ~/n8n
docker compose pull
docker compose up -d
Check logs if you need to troubleshoot:
AlmaLinux 9:
docker compose logs -f n8n
Ubuntu 24.04:
docker compose logs -f n8n
Step 4: Terminate TLS with a reverse proxy and forward headers
This places n8n behind a reverse proxy and forwards the headers n8n needs to build correct webhook URLs. n8n’s docs recommend enabling SSL at the proxy and using a subdomain such as n8n.example.com. Set N8N_WEBHOOK_URL rather than the deprecated WEBHOOK_URL. When behind a proxy set N8N_PROXY_HOPS=1 and forward X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto.
If you use Nginx, your server block should:
- Listen on 443 with your certificate.
- Proxy to
http://127.0.0.1:5678or the container’s internal address. - Set the three
X-Forwarded-*headers.
If you prefer another proxy or a load balancer, apply the same header forwarding and TLS approach. Avoid path-based deployments. Use a subdomain for stable navigation. Update your Compose file and restart if you change N8N_EDITOR_BASE_URL or N8N_WEBHOOK_URL.
Step 5: Point DNS at your VPS
This creates an A record for your subdomain so browsers and webhook senders reach your instance. Create the record at your domain’s DNS provider.
- Name: n8n
- Type: A
- Value: your VPS public IPv4
Allow time for DNS to update. Once your reverse proxy is listening on 443 with a valid certificate, https://n8n.example.com should load the editor. If you need a server in the UK or US for lower latency, see our Linux VPS.
Step 6: Log in and understand authentication
This uses n8n’s built-in login and roles. Basic auth and JWT auth were removed in n8n 1.0. SMTP is optional, but without it users cannot reset passwords. Plan your admin user, invites and recovery before going live. If you need help deciding how to structure users, open a support ticket and we will look at your setup.
Step 7: Configure webhook URLs and test versus production behaviour
This ensures webhook nodes produce correct public URLs and behave as you expect during development and after publishing.
- Use N8N_WEBHOOK_URL. From n8n 2.35.0,
WEBHOOK_URLis deprecated and logs a warning. Keep usingN8N_WEBHOOK_URLfor your HTTPS base. - Set N8N_EDITOR_BASE_URL to the same public HTTPS URL. This is used in public links and emails.
- Behind a proxy, set N8N_PROXY_HOPS=1 and forward
X-Forwarded-For,X-Forwarded-HostandX-Forwarded-Proto. - Webhook nodes expose both a Test URL and a Production URL. The Test URL registers when you click “Listen for Test Event” or manually execute the node in the editor. The Production URL registers when you publish the workflow.
- Default webhook paths can be adjusted via
N8N_ENDPOINT_WEBHOOK,N8N_ENDPOINT_WEBHOOK_TESTandN8N_ENDPOINT_WEBHOOK_WAITif you need custom paths. - Payload size defaults to 16 MB. You can raise it with
N8N_PAYLOAD_SIZE_MAXif an integration needs larger posts.
After changing environment variables in your Compose file, restart to apply them:
AlmaLinux 9:
cd ~/n8n
docker compose up -d
Ubuntu 24.04:
cd ~/n8n
docker compose up -d
Step 8: Back up workflows and credentials with the n8n CLI
This uses the n8n Server CLI inside the container to export your workflows and credentials to files you can copy off the server. Keep backups and your N8N_ENCRYPTION_KEY together. Without the original key, credential backups cannot be decrypted.
These commands create a backups/latest directory under /home/node/.n8n in the container and export data there. The paths are then available on the mounted volume on the host.
AlmaLinux 9:
# Export workflows
docker compose exec -T n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/latest/
# Export credentials
docker compose exec -T n8n n8n export:credentials --backup --output=/home/node/.n8n/backups/latest/
Ubuntu 24.04:
# Export workflows
docker compose exec -T n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/latest/
# Export credentials
docker compose exec -T n8n n8n export:credentials --backup --output=/home/node/.n8n/backups/latest/
To migrate data between databases or instances, use entity export and import. This exports all entities for migration. Imports can overwrite by ID, and you can control active state or truncate tables first.
AlmaLinux 9:
# Export all entities for migration
docker compose exec -T n8n n8n export:entities --output=/home/node/.n8n/backups/latest/
# Import entities (review flags before running in production)
docker compose exec -T n8n n8n import:entities --input=/home/node/.n8n/backups/latest/ --truncateTables
Ubuntu 24.04:
# Export all entities for migration
docker compose exec -T n8n n8n export:entities --output=/home/node/.n8n/backups/latest/
# Import entities (review flags before running in production)
docker compose exec -T n8n n8n import:entities --input=/home/node/.n8n/backups/latest/ --truncateTables
Always test a restore on a non-production instance. Keep database-level backups too, especially before enabling features like encryption key rotation.
Step 9: Keep execution data under control
This configures pruning so execution logs do not grow without bound. The example Compose file already sets pruning variables. Adjust them to match your compliance and troubleshooting needs. If you use SQLite, enable a VACUUM to reclaim disk space on startup.
EXECUTIONS_DATA_PRUNE=trueenables pruning.EXECUTIONS_DATA_MAX_AGEsets how many hours to retain.EXECUTIONS_DATA_PRUNE_MAX_COUNTlimits rows processed per prune pass.- For SQLite only:
DB_SQLITE_VACUUM_ON_STARTUP=truereclaims freed space after pruning.
Step 10: Plan safe upgrades and key rotation
This avoids the common footguns that take an instance offline.
- Do not change or lose
N8N_ENCRYPTION_KEY. All stored credentials depend on it, even when using PostgreSQL. Keep it safe and backed up. - Persist
/home/node/.n8n. Starting n8n without this volume or with an empty one will generate a new key and make existing credentials unreadable. - PostgreSQL major upgrades: do not bump the container tag across major versions and expect it to work. You will see “database files are incompatible”. Use
pg_dumporpg_upgradeaccording to Postgres documentation, then change image tags. - Encryption key rotation is available in n8n. It is a one-way migration. Do not disable rotation after enabling, and do not downgrade n8n. If you do, new-format secrets become unreadable. The only recovery is restoring a database backup from before enabling rotation.
- To update n8n to a newer image:
AlmaLinux 9:
cd ~/n8n docker compose pull n8n docker compose up -dUbuntu 24.04:
cd ~/n8n docker compose pull n8n docker compose up -d
Step 11: Run a security audit
This checks your instance for unprotected webhooks, missing security settings and risky nodes. n8n provides a built-in audit you can run via CLI, API or an audit node in a workflow. Review the findings and address any gaps before moving sensitive automations into production.
See the vendor’s documentation: n8n security audit.
What next
You now have n8n running with a production database, persistent secrets, correct public URLs and a backup plan. If you prefer a click-by-click walkthrough tailored to our platform, ask us to produce a how-to. In the meantime, you can explore more topics in our VPS guides or pick the right size of Linux VPS for your workload.
If you are stuck at any point, please open a support ticket and we will help you diagnose it.
Common questions
Can I run n8n without PostgreSQL?
Yes, n8n defaults to SQLite and stores the database at ~/.n8n/database.sqlite. For production, PostgreSQL is recommended. Either way, you must persist /home/node/.n8n to retain the encryption key and other state.
Why do my webhooks break behind a proxy?
Set N8N_WEBHOOK_URL to your public HTTPS base, not the deprecated WEBHOOK_URL. Ensure your proxy forwards X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto, and set N8N_PROXY_HOPS=1. Prefer a subdomain over a path prefix.
How do I back up and restore workflows and credentials?
Use the n8n Server CLI inside the container. Export workflows and credentials to a directory on the mounted volume, copy them off-server, and keep your N8N_ENCRYPTION_KEY safe. To migrate between instances or databases, use export:entities and import:entities, taking care with flags that overwrite by ID.
Why did my credentials become unreadable after a restart?
Starting n8n without the original /home/node/.n8n volume or with a different N8N_ENCRYPTION_KEY generates a new key. Existing encrypted credentials cannot be decrypted. Restore the original volume and key from backup.
Can I update PostgreSQL by changing the image tag?
Not across major versions. Postgres will refuse to open the old data directory. Use the official Postgres migration paths, then change the image tag. The n8n docs also call this out for recent releases.