Installing n8n with Docker and PostgreSQL
n8n is a workflow automation platform that you can run on your own server. A self-hosted instance keeps your workflows, credentials and execution data under your control and has no limits on the number of workflows or executions beyond what the server can handle.
This guide sets up n8n on a dedicated server with PostgreSQL, HTTPS and backups. It uses three containers managed by Docker Compose:
- n8n, the automation platform itself;
- PostgreSQL, the database for workflows, credentials and execution history;
- Caddy, a reverse proxy that gets and renews an HTTPS certificate automatically.
By default n8n stores its data in SQLite. This guide uses PostgreSQL instead: it can be backed up while n8n is running, and it is the database n8n recommends for self-hosting.
The steps were tested on Ubuntu 24.04 LTS with n8n 2.46.7, PostgreSQL 17, Docker 29 and Docker Compose v5.
Before you start
You will need:
- a server running Ubuntu 24.04 LTS with root access over SSH;
- a domain or subdomain for n8n, for example
n8n.example.com; - an A record for that name pointing to the server's IPv4 address.
If the domain also has an AAAA record, the server must be reachable over IPv6 at that address. Otherwise Let's Encrypt may try IPv6 first and fail to issue the certificate.
Check that the record resolves to your server before you continue:
dig +short n8n.example.com
The command should return the server's IP address. Certificate issuance in Caddy depends on this, so wait until DNS has propagated.
All commands below are run as root. If you work as a regular user with sudo rights, add sudo before each command.
Install Docker
Install Docker Engine and the Compose plugin from the official Docker repository. The docker.io package from the Ubuntu repository is older and does not include Compose v2.
apt update && apt install -y ca-certificates curl install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc
Add the repository:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
Install the packages:
apt update apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Check that both tools are available:
docker --version docker compose version
Create the project directory and secrets
All n8n files will live in /opt/n8n:
mkdir -p /opt/n8n && cd /opt/n8n
Create the .env file with your domain, time zone and generated secrets. Replace n8n.example.com and the time zone with your own values:
cat > .env <<EOF DOMAIN=n8n.example.com GENERIC_TIMEZONE=Europe/Amsterdam POSTGRES_USER=n8n POSTGRES_DB=n8n POSTGRES_PASSWORD=$(openssl rand -hex 24) N8N_ENCRYPTION_KEY=$(openssl rand -hex 32) EOF chmod 600 .env
GENERIC_TIMEZONE sets the time zone used by the Schedule Trigger and other time-based nodes. Without it, n8n uses America/New_York.
N8N_ENCRYPTION_KEY is the key n8n uses to encrypt the credentials you save in it: API tokens, passwords, OAuth secrets. If this key is lost, the credentials in a restored database cannot be decrypted and every one of them has to be entered again.
Copy the .env file to a safe place outside the server right away, for example a password manager. A database backup without N8N_ENCRYPTION_KEY restores your workflows, but not your credentials.
Create the Docker Compose file
Create /opt/n8n/compose.yaml:
services: postgres: image: postgres:17 restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 n8n: image: docker.n8n.io/n8nio/n8n restart: unless-stopped depends_on: postgres: condition: service_healthy environment: DB_TYPE: postgresdb DB_POSTGRESDB_HOST: postgres DB_POSTGRESDB_PORT: 5432 DB_POSTGRESDB_DATABASE: ${POSTGRES_DB} DB_POSTGRESDB_USER: ${POSTGRES_USER} DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD} N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} N8N_HOST: ${DOMAIN} N8N_PROTOCOL: https N8N_PROXY_HOPS: 1 N8N_WEBHOOK_URL: https://${DOMAIN}/ GENERIC_TIMEZONE: ${GENERIC_TIMEZONE} TZ: ${GENERIC_TIMEZONE} ports: - "127.0.0.1:5678:5678" volumes: - n8n_data:/home/node/.n8n caddy: image: caddy:2 restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config volumes: postgres_data: n8n_data: caddy_data: caddy_config:
A few details in this file matter for security and stability
PostgreSQL has no ports section. The database is reachable only by the other containers in the project, never from the internet.
The n8n port is published as 127.0.0.1:5678. Users reach n8n only through Caddy over HTTPS, while you can still check n8n locally on the server.
Do not shorten the n8n port line to "5678:5678". Docker adds its own firewall rules for published ports, and they bypass UFW. Without the 127.0.0.1 prefix, n8n becomes reachable over plain HTTP from anywhere, even if UFW blocks port 5678.
The depends_on condition with a healthcheck makes n8n wait until PostgreSQL is ready to accept connections. Without it, n8n can start first on a cold boot and fail to connect to the database.
N8N_PROXY_HOPS tells n8n that it runs behind one reverse proxy, so it reads the real client address from the proxy headers. N8N_WEBHOOK_URL sets the public address n8n shows for webhook URLs. In n8n versions before 2.30, this variable is called WEBHOOK_URL.
Configure HTTPS with Caddy
Create /opt/n8n/Caddyfile with your domain:
n8n.example.com { reverse_proxy n8n:5678 }
That is the whole configuration. On first start, Caddy requests a Let's Encrypt certificate for the domain, redirects HTTP to HTTPS and renews the certificate before it expires. Ports 80 and 443 must be open for this to work.
Configure the firewall
Allow SSH first, then HTTP and HTTPS, and enable UFW:
ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw --force enable ufw status
If UFW was already enabled with your own rules, run ufw status before making changes and keep the rules you need. Allowing OpenSSH before enabling UFW keeps your current SSH session from being cut off.
Start n8n
Start the containers in the background:
cd /opt/n8n docker compose up -d
The first start downloads the images and takes a minute or two. Check that all three containers are running and PostgreSQL is healthy:
docker compose ps
Look at the n8n log:
docker compose logs n8n --tail 50
The log should end with a line like:
Editor is now accessible via: https://n8n.example.com
Check that n8n answers locally and through Caddy:
curl -s http://127.0.0.1:5678/healthz curl -s https://n8n.example.com/healthz
Both commands should return {"status":"ok"}.
If the second command fails, check the Caddy log with docker compose logs caddy. The most common cause is a DNS record that does not point to this server yet, or ports 80 and 443 being closed.
Create the owner account
Open https://n8n.example.com in your browser. On the first visit, n8n asks you to create the owner account. This account has full rights over the instance and manages all other users.

Enter your email, first and last name, and a password, then click Next. The password must be at least 8 characters long and contain at least one number and one capital letter. Use a strong one: the n8n editor is open to the internet, and anyone who gets into it can run commands on the server through your workflows.
After the first login, n8n shows a short survey, Customize n8n to you. The questions are optional. Click Get started to close it, with or without answering.

Next, n8n offers a free license key for several Community Edition features: advanced debugging, execution search and tagging, and folders for workflows. The key is sent to the email you enter. Click Send me a free license key to get it, or Skip if you do not need these features.

The overview page opens. The installation is complete and you can create your first workflow.

Back up n8n
An n8n backup consists of two parts: a PostgreSQL dump with your workflows, credentials, users and execution history, and the .env file with N8N_ENCRYPTION_KEY. Neither part is useful without the other.
In this setup, n8n keeps all of its state in PostgreSQL. The n8n_data volume does not need to be backed up: a full restore works from the database dump and the .env file alone.
Create a backup directory and dump the database:
mkdir -p /opt/n8n-backup cd /opt/n8n docker compose exec -T postgres pg_dump -U n8n -d n8n -Fc > /opt/n8n-backup/n8n-db.dump cp .env compose.yaml Caddyfile /opt/n8n-backup/
The dump is made while n8n is running, so no downtime is needed.
A backup that stays on the same server will not help if the server's disk fails. Copy /opt/n8n-backup to external storage on a regular schedule. You can use INTROSERV Cloud Storage for this, as described in the guide Connecting INTROSERV Cloud Storage to the server.
Restore from a backup
To restore n8n on a new server, install Docker as described above and copy the backup directory to /opt/n8n-backup on that server. Then put the configuration files in place:
mkdir -p /opt/n8n cp /opt/n8n-backup/.env /opt/n8n-backup/compose.yaml /opt/n8n-backup/Caddyfile /opt/n8n/ chmod 600 /opt/n8n/.env
If the new server has a different IP address, update the domain's A record first.
Start only the database:
cd /opt/n8n docker compose up -d postgres
Wait until docker compose ps shows PostgreSQL as healthy, then restore the dump:
docker compose exec -T postgres pg_restore -U n8n -d n8n --clean --if-exists < /opt/n8n-backup/n8n-db.dump
Start the rest of the stack:
docker compose up -d
Log in with the same owner account and check that your workflows and credentials are in place. Saved credentials are decrypted with N8N_ENCRYPTION_KEY from the restored .env file, so they work without being entered again.
Update n8n
n8n releases updates often. To update to the latest version, pull the new images and recreate the containers:
cd /opt/n8n docker compose pull docker compose up -d docker image prune -f
The last command removes the old images that are no longer used.
Check the new version:
docker compose exec n8n n8n --version
Make a backup before every update. On start, a new n8n version can migrate the database schema, so the reliable way back to the previous version is to restore a dump made before the update.
The PostgreSQL image is pinned to major version 17, so docker compose pull only installs minor updates of PostgreSQL. Moving to a new major version requires a dump and restore and is not done automatically.
What's next
Your n8n instance is ready for work. From here you can:
- invite other users to the instance;
- set up SMTP so n8n can send invitations and password reset emails;
- connect your services and build your first workflows.
If you plan to run AI workflows with local language models, see the guide Installing the n8n AI Starter Kit with Ollama.