New Customers: 50% OFF Your First Month on All VPS Servers & Web Hosting Plans!

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.

Rhys CallowayLinux VPS, servers, security and the command line 11 min read Updated 23 Sep 2026 AlmaLinux 9, Ubuntu 24.04

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: Do not use deprecated packages or third-party builds.
  • Choose PostgreSQL for production. SQLite is built in but stores its database at ~/.n8n/database.sqlite and 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/.n8n inside 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_KEY before 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_URL and N8N_WEBHOOK_URL to your public https://n8n.example.com. When using a proxy, forward X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto and set N8N_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_dump or pg_upgrade as 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/.n8n mounted. It contains the encryption key and other files n8n needs to decrypt credentials. Do not change N8N_ENCRYPTION_KEY after 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:5678 or 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.

open a support ticket

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_URL is deprecated and logs a warning. Keep using N8N_WEBHOOK_URL for 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-Host and X-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_TEST and N8N_ENDPOINT_WEBHOOK_WAIT if you need custom paths.
  • Payload size defaults to 16 MB. You can raise it with N8N_PAYLOAD_SIZE_MAX if 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=true enables pruning.
  • EXECUTIONS_DATA_MAX_AGE sets how many hours to retain.
  • EXECUTIONS_DATA_PRUNE_MAX_COUNT limits rows processed per prune pass.
  • For SQLite only: DB_SQLITE_VACUUM_ON_STARTUP=true reclaims 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_dump or pg_upgrade according 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 -d

    Ubuntu 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.