← All posts

Sep 1, 20261,921 words9 min read

How to self-host n8n on Coolify (with PostgreSQL and webhooks that work)

Deploy n8n on Coolify with PostgreSQL, get webhooks working behind Traefik or a Cloudflare Tunnel, and keep it healthy. No execution limits.

n8n and Coolify thumbnail

Introduction

n8n is a workflow automation tool: you connect triggers (a webhook, a cron schedule, a new row in a database) to actions (send a message, call an API, run some JavaScript) by wiring nodes together in a visual editor. Think Zapier or Make, except you can run it on your own server.

Self-hosting it makes sense for a few reasons. The hosted plans charge per execution: n8n Cloud's Starter plan gives you 2,500 executions a month for USD 24 (20 if you pay yearly), and a single workflow that runs every five minutes burns through that in about nine days. The self-hosted version has no such limit and is free for your own use under its fair-code license. Your data and credentials stay on your machine. And it's a handful of Docker containers with a database next to them, which is exactly the kind of thing Coolify is good at.

In this article I will guide you through deploying n8n on Coolify with a PostgreSQL database, putting it behind your own domain, getting the environment variables right so webhooks work, and taking a first workflow for a spin. It builds on my post on setting up Coolify, and if you followed the one on Cloudflare Tunnels, there is a section for you too.

For this guide, I'll be assuming you already have a Coolify instance running on a Linux server and a domain, or a subdomain, you can point at it.

What you need

  • A server with Coolify installed. n8n itself is light, but keep in mind that every workflow execution is stored in the database, so give it some room: 2 GB of RAM is comfortable for personal use.
  • A domain for the editor, for example n8n.example.com. Webhooks are the main reason to have n8n online at all, and third-party services will only call a public HTTPS URL.
  • Ten minutes.

Creating the service

Coolify ships n8n as a one-click service, so there is no compose file to write. Go to Projects, open your project and environment, click + New and type n8n in the search box. You will see two flavors: plain n8n, which stores everything in a SQLite file, and n8n with PostgreSQL. Pick the PostgreSQL one.

SQLite works, but n8n writes a row for every execution and its data, and that file grows quickly and gets slow to vacuum. PostgreSQL handles it without complaint, and it's the database n8n recommends for anything beyond a quick test. Coolify creates the containers for you (n8n, a small task runner where your Code nodes execute, and PostgreSQL), wires them into the same private network and generates the database password.

Once the service is created you land on its page. The compose file that defines it is right there under the editor, and you can tweak it later; for now, take a look at the two things that matter:

  • The volume mounted at /home/node/.n8n. That's where n8n keeps its config, and the encryption key that protects every credential you save. It appears in the Persistent Storage tab, and it must survive redeploys.
  • The magic variables such as SERVICE_URL_N8N. Coolify fills these in from the domain you set on the service (the full URL in this case; SERVICE_FQDN_N8N is just the host), and the template uses them to tell n8n its own public URL.

Setting the domain

In the service page, find the n8n container and set its domain. How you write it depends on how your server is exposed.

With a public server

If your server has a public IP and Coolify handles TLS, as in my first post, set the domain to https://n8n.example.com and create the matching DNS record if it doesn't fall under your wildcard already. Traefik requests the Let's Encrypt certificate on the first deploy:

n8n.example.com    A    YOUR_SERVER_IP

Behind a Cloudflare Tunnel

If your apps go through a tunnel, the rule from the previous post applies: set the domain in Coolify to http://n8n.example.com (plain http, TLS ends at Cloudflare) and add a public hostname in Zero Trust pointing at localhost:80, or coolify-proxy:80 if your connector runs as a container.

Configuring the environment

Open the Environment Variables tab of the service. The template already sets the essentials, but there are a handful of variables worth adding or changing before the first deploy:

bash
# Public URL n8n advertises (needed behind a tunnel, harmless otherwise)
N8N_PROTOCOL=https
N8N_EDITOR_BASE_URL=https://n8n.example.com/
WEBHOOK_URL=https://n8n.example.com/

# How many proxies set X-Forwarded-* headers before n8n. Coolify's Traefik is one; see below for tunnels
N8N_PROXY_HOPS=1

# Your timezone, for schedules and date nodes
GENERIC_TIMEZONE=America/Argentina/Buenos_Aires
TZ=America/Argentina/Buenos_Aires

# Key used to encrypt stored credentials. Generate it once, keep it forever.
N8N_ENCRYPTION_KEY=<A_LONG_RANDOM_STRING>

# Prune old executions. The defaults keep 14 days or 10,000 rows; a week is plenty for personal use
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168

N8N_PROXY_HOPS tells n8n how many reverse proxies add forwarding headers in front of it, so it can read the visitor's real IP from X-Forwarded-For (it uses it for things like rate limiting logins). On a public server that's Traefik alone. Behind a Cloudflare Tunnel there are two proxies, but 1 is still the right value: Coolify's Traefik doesn't trust forwarding headers from the connector by default, so it drops the ones Cloudflare sent and writes its own, and n8n only ever sees one hop. The price is that n8n records the connector's address instead of the visitor's. If you need the real IP, add the connector's address to --entrypoints.http.forwardedHeaders.trustedIPs in Coolify's proxy configuration and set N8N_PROXY_HOPS=2.

You may see N8N_RUNNERS_ENABLED in older guides. Since n8n 2.0 task runners are always on and the flag is deprecated; the template already runs them in their own container, which is where your Code nodes execute.

You can generate the encryption key on any machine with openssl rand -hex 32. Which brings us to the one thing you really don't want to lose:

Deploying

Hit Deploy and watch the logs. Coolify pulls the images, starts PostgreSQL first and then n8n and its task runner; n8n runs its database migrations on the first boot. When the containers report healthy, open your domain.

The first screen asks you to create the owner account: email, name and a password. There's no separate sign-up after this, so do it right away. Additional users can be invited later from Settings -> Users.

Testing it out

A webhook is the quickest way to prove that the whole chain works, from the internet to the container and back. Create a new workflow, add a Webhook trigger, leave the method on GET and copy the test URL it shows. Click Listen for test event and call it from your computer:

bash
curl -i https://n8n.example.com/webhook-test/YOUR_WEBHOOK_PATH

The node should light up in the editor with the request details, and the terminal should get a 200 back. Two things to check in that output:

  • The URL you copied starts with https://n8n.example.com. If it says http, WEBHOOK_URL is not being picked up.
  • The response is a 200, not a 301 or 308. A redirect means a caller sending a POST would lose its payload.

Now add a second node, for example Respond to Webhook with a fixed message, save it and hit Publish (n8n 2.0 replaced the old active toggle with an explicit publish step). The production URL drops the -test suffix:

bash
curl https://n8n.example.com/webhook/YOUR_WEBHOOK_PATH

If you get your message back, you have a working automation server. Anything that can make an HTTP request can now trigger a workflow.

Keeping it healthy

  • Updates: n8n releases often and its minor versions can include database migrations. In the compose editor, pin the image to a version instead of latest, and when you want to upgrade, read the release notes, change the tag and redeploy. Rolling back is then just changing the tag back.
  • Backups: the database is the state. Back up the PostgreSQL volume, or the database itself, on a schedule, and keep the encryption key with it. A cheap extra is exporting your workflows as JSON from inside the container with the command below and shipping the files anywhere you like.
  • Executions: if the database keeps growing, lower EXECUTIONS_DATA_MAX_AGE or set the workflow-level option to save only failed executions. Most of the time you don't need the payload of every successful run.
bash
docker exec -u node -it <N8N_CONTAINER> n8n export:workflow --all --output=/home/node/.n8n/workflows-backup.json

Troubleshooting

The questions I see most often, with the short answer. The details are in the sections above.

Why does my webhook URL start with http instead of https?

n8n builds it from N8N_EDITOR_BASE_URL and WEBHOOK_URL, and behind a tunnel Coolify fills those with the http:// domain you set on the service. Set both variables to your https:// address, set N8N_PROTOCOL=https, redeploy, and check with curl -i that the test URL answers 200 and not a redirect.

n8n complains about a secure cookie and won't log me in

The session cookie is only sent over HTTPS, so you are opening n8n over plain HTTP: through the server's IP, a port, or a domain Cloudflare isn't proxying. Fix the domain so it reaches n8n over HTTPS end to end. Setting N8N_SECURE_COOKIE=false makes the error go away, but your session travels in the clear, so keep it for local testing only.

Webhooks don't work behind a Cloudflare Tunnel

Three things to check: the domain in Coolify is http:// (Traefik never sees TLS), WEBHOOK_URL is https://, and Always Use HTTPS is on in Cloudflare. If the URL n8n shows is http, callers get redirected and a redirected POST loses its body. With curl -i you want a 200, not a 301 or 308.

My credentials are unreadable after a restore

The database was restored with a different N8N_ENCRYPTION_KEY than the one that encrypted it. Set the original key and restart; if it's gone, the credentials are gone too and you'll have to enter them again. Keep the key next to your backups.

That's it! You have n8n running on your own server, reachable over HTTPS, with its credentials encrypted with a key you control. From here you can connect it to whatever you already run on Coolify (a database, an API, this blog) and start automating the boring parts. When a single instance stops being enough, n8n's queue mode with Redis and worker containers is the next step, and it also fits nicely in a Coolify compose service.