Roar AI
Get started
Resources

How to self-host n8n: a step-by-step guide with Docker, Postgres and HTTPS

A working setup for self-hosted n8n on your own server, with the settings that keep workflows, credentials and webhooks intact, and what we learned running it.

In short
  • A complete setup: n8n, a Postgres database and automatic HTTPS, in one Docker Compose file you can copy.
  • Four settings decide whether your n8n survives its first update: the database, the encryption key, the public address and never running two copies.
  • What we measured running n8n for customers: how long it takes to start, how long an update is offline, and the settings that fail without a word.

This is a walkthrough for running n8n on a server you control. By the end you will have n8n on your own domain with HTTPS, its data in Postgres, and a way to back it up and update it. It takes about half an hour if you have used a terminal before.

We host n8n for customers, so we have a view on this, and we say where at the end. Most of what follows applies wherever you run it.

The three ways to run n8n

  • n8n Cloud. Run by the makers of n8n. Nothing to set up, and plans are priced by how many workflow runs you make.
  • Your own server. Full control and no run limits. You do the setup, the updates and the backups. This guide is about this option.
  • Managed hosting. Someone else runs the server, and you still get your own instance and database. That is what we sell, at the end of the post.

What you need

  • A Linux server you can reach over SSH, with Docker and the Compose plugin installed. Leave a few gigabytes of disk free: the n8n image alone is about 1 GB.
  • A domain or subdomain you control, such as n8n.example.com, with an A record pointing at the server's IP address.
  • Ports 80 and 443 open to the internet, so HTTPS certificates can be issued.

Step 1: the settings file

Make a folder for n8n and create a file called .env in it. Everything secret or specific to you lives here, and nowhere else:

mkdir ~/n8n && cd ~/n8n
cat > .env <<EOF
N8N_HOST=n8n.example.com
TIMEZONE=Asia/Colombo
POSTGRES_PASSWORD=$(openssl rand -hex 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
EOF
chmod 600 .env

Change N8N_HOST to your domain and TIMEZONE to yours. The two random values are made for you.

Copy N8N_ENCRYPTION_KEY somewhere safe now, off the server. Every password and API key you save in n8n is locked with it. Lose the key and every saved credential has to be entered again by hand; there is no way to recover them.

Step 2: the Compose file

Create docker-compose.yml beside it:

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: n8n
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 10s
      retries: 5

  n8n:
    image: n8nio/n8n:2.43.3
    restart: unless-stopped
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_HOST: ${N8N_HOST}
      N8N_PROTOCOL: https
      N8N_PROXY_HOPS: 1
      WEBHOOK_URL: https://${N8N_HOST}/
      N8N_WEBHOOK_URL: https://${N8N_HOST}/
      N8N_EDITOR_BASE_URL: https://${N8N_HOST}/
      GENERIC_TIMEZONE: ${TIMEZONE}
      TZ: ${TIMEZONE}
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data

volumes:
  db_data:
  n8n_data:
  caddy_data:

What each part is doing:

  • postgres keeps your workflows, credentials and run history. Without it n8n writes everything into a small file inside its own container.
  • n8n pins an exact version (2.43.3 is the one we run at the time of writing). A floating tag like latest updates n8n whenever the image is pulled, which is not when you want an update to happen.
  • The DB_ settings point n8n at Postgres. n8n does not read the DATABASE_URL setting most apps use, so it needs these spelled out.
  • The three address settings tell n8n its public address, so the webhook links it hands out and the editor both work behind HTTPS.
  • caddy puts HTTPS in front of n8n and renews the certificate itself.

Step 3: HTTPS

Create a file called Caddyfile with your domain in it:

n8n.example.com {
	reverse_proxy n8n:5678
}

Then start everything:

docker compose up -d
docker compose logs -f n8n

When the log says the editor is accessible, open https://n8n.example.com. n8n asks you to create the owner account. That is the account that manages everything else, so give it a strong password.

Step 4: backups

Your n8n is now two things you cannot afford to lose: the database and the encryption key. The key is in .env, and you copied it in Step 1. For the database, a nightly dump is enough to start with:

docker compose exec -T postgres pg_dump -U n8n n8n | gzip > ~/n8n-backup-$(date +%F).sql.gz

Put that in a nightly cron job, and copy the files off the server. A backup that only lives on the machine it is protecting goes down with that machine.

Step 5: updating n8n

Read the release notes for the version you are moving to, take a backup, then change the tag in docker-compose.yml and run:

docker compose pull n8n
docker compose up -d n8n

Compose stops the old n8n and starts the new one, and n8n updates its database on the way up. Expect it to be offline for a short while; the numbers we measured are below.

Four mistakes that only show up later

Each of these works perfectly on day one and fails on a later restart or update.

1. No database, or the wrong settings for it. Without the DB_ settings, n8n writes to a file inside the container. Recreate the container, which every update does, and your workflows are gone. Setting DATABASE_URL looks right and is ignored.

2. An encryption key nobody wrote down. If you do not set N8N_ENCRYPTION_KEY, n8n makes one up and saves it in its own folder. That works as long as the folder survives. The day it does not, every saved credential stops opening. Set the key yourself and keep a copy off the server. The same key is also what lets credentials move with you to a new server.

3. A wrong public address. Behind a proxy, n8n can only guess its own address, and it often guesses http://localhost:5678. Then the webhook links it shows you point nowhere. n8n 2.x has also renamed the webhook setting from WEBHOOK_URL to N8N_WEBHOOK_URL and says so in its log, while 1.x reads only the old name. Setting both, as above, works on either.

4. Two copies at once. If two n8n processes ever share one database, both run every scheduled workflow, so every scheduled email goes out twice. This is easy to cause by accident: plenty of hosting platforms start the new version before stopping the old one, to avoid downtime. For n8n that default is wrong. Stop the old copy first, and accept a short gap.

What we measured running n8n

These come from running n8n 2.43.3 on our own servers in October 2026.

  • First start, against an empty database with its setup included, took 14 to 16 seconds.
  • The web address answers before the editor is ready. A health check on / passed about 35 seconds before n8n reported the editor as ready. If something checks whether n8n is up, point it at /healthz/readiness instead.
  • An update is offline for about half a minute: around 18 seconds with nothing answering, then around 10 seconds where the editor loaded but could not reach its own API yet. That gap is what stopping the old copy first costs, and it is worth it.
  • Pull from n8nio/n8n. The docker.n8n.io address in some n8n examples refused our downloads with rate-limit errors, while n8nio/n8n served the same image.
  • Python Code steps need extra setup. n8n 2.x runs Python code in a separate runner container that this guide does not include. JavaScript Code steps work as they are.

Point n8n's AI steps at more models

n8n's AI steps use an OpenAI credential, and that credential has a Base URL field. Any service that speaks the same API works there. Ours is https://api.roar-ai.com/v1: with a Roar AI key in the credential, the same AI steps can use any model we serve, and the spend shows up against that key. This works whether your n8n runs on your server or ours.

Or skip the server

If the steps above are more upkeep than you want, we run this setup for you. n8n hosting on Roar AI launches n8n from your console with its own database, an encryption key kept in your own settings, addresses that follow your domain, updates that stop the old copy first, and its AI steps already connected to every model we serve. You can run it in Colombo, Bangalore or Singapore, and an Australian team's n8n runs in Singapore for now (why).

It has limits worth knowing before you choose it. Python Code steps don't work. A webhook that waits for its workflow to finish must answer within about 2 minutes. Each update has the short gap described above.

n8n is a trademark of n8n GmbH. Roar AI hosts and supports n8n and is not affiliated with or endorsed by n8n GmbH.

Start building
in minutes

Get a key, call any model, ship your app. Pay in your local currency.