Installing n8n takes an evening. Keeping it running for a year without losing a credential, missing a failed workflow or getting stuck on a bad update is the real job. I've set up self-hosted n8n for clients on Oracle Cloud's free tier and on Hostinger VPS plans. This is the checklist for everything after the install.
Every n8n command and setting here comes from n8n's docs, and the Docker, PostgreSQL and other tool commands come from their own docs. I link them where they come up and list them all at the end. Where I combine them into my own script or command, or make a call the docs don't, I say so.
Still need a server? Start with my Oracle Cloud free tier guide or the Hostinger VPS guide. If you set up n8n from either guide before this checklist existed, check three settings covered below: an exact image version instead of an unpinned one, port 5678 bound to 127.0.0.1, and N8N_WEBHOOK_URL in place of WEBHOOK_URL once you're on n8n 2.35.0 or later.
The self-hosted n8n production checklist
Six areas, in the same order as my production setup service. Each one links to the section that shows how.
- Server: a VPS you own, n8n in Docker Compose, the image pinned to an exact version. n8n's Compose guide asks for "At least 4 GB of RAM and 2 vCPUs" for its full stack, which includes a Docker-in-Docker sandbox for AI-generated code. My Hostinger guide covers picking a plan by RAM.
- Secured: HTTPS through a reverse proxy,
N8N_WEBHOOK_URLandN8N_PROXY_HOPSset, port 5678 on127.0.0.1, authenticated webhooks, an owner account with 2FA,n8n auditreviewed. - Database: Postgres, not SQLite, with queue mode once one instance can't keep up.
- Backed up: the encryption key in a password manager; a nightly script for the database dump, the
.n8nvolume, the compose file and.env; an encrypted off-site copy; a heartbeat; and a restore you've tested. - Updated: monthly, release notes read, backup first, rollback by restore, and your n8n 3.0 prep done before you upgrade.
- Monitored: an Error Workflow on every production workflow, an outside check on /healthz/readiness,
/metricskept private and execution pruning tuned.
Back up the n8n encryption key first
n8n encrypts credentials before saving them to the database. On first launch it generates a random key and stores it in a file called config inside the .n8n folder, which in Docker is /home/node/.n8n.
n8n's backup and restore page is blunt about it: "Without the encryption key from the config file, or a custom N8N_ENCRYPTION_KEY, a restored database or encrypted credential export can't be decrypted." A perfect database backup without the key leaves every API key and OAuth connection unreadable.
Get the key into your password manager today. First copy the file out of the container. This is Docker's compose cp command pointed at the path n8n documents:
cd /opt/n8n # the folder that holds your compose file
docker compose cp n8n:/home/node/.n8n/config ./n8n-config.json
The file is JSON with an encryptionKey field. Put that value in your password manager, away from the server, then delete the local copy with rm n8n-config.json.
Then pin it. Set N8N_ENCRYPTION_KEY to that exact value. Put it in .env if your n8n service has env_file: .env (n8n's current Compose file does). Otherwise, add it to the n8n service's environment: block. My quick check, which doesn't print the key: docker compose exec n8n sh -c 'test -n "$N8N_ENCRYPTION_KEY" && echo set' prints set when the variable reached the container.
On a brand-new instance, you can generate a key before the first start with openssl rand, for example openssl rand -base64 32.
Fix "Mismatching encryption keys"
Never invent a new key for an instance that already has credentials. n8n's source code compares the two keys at startup and refuses to start with an error that reads "Mismatching encryption keys. The encryption key in the settings file ... does not match the N8N_ENCRYPTION_KEY env var." The fix: set N8N_ENCRYPTION_KEY to the encryptionKey value in /home/node/.n8n/config, or remove the variable so n8n reads the key from that file.
What to back up in self-hosted n8n (SQLite vs Postgres)
n8n defines a complete backup as the .n8n folder plus your external database. I add the deployment config, because n8n's restore steps need "the environment variables for your database connection, a custom N8N_ENCRYPTION_KEY if you use one, and any external storage settings."
| Piece | Why | SQLite | Postgres |
|---|---|---|---|
.n8n folder (Docker volume) |
Encryption key, plus binary data in filesystem mode | Yes, it also holds database.sqlite |
Yes, for the key |
| Database | Workflows, credentials, users, settings, executions | Inside .n8n |
pg_dump |
Compose file and .env |
Database connection settings and your pinned key | Yes | Yes |
| Custom nodes, external storage (S3, Azure Blob) | Workflows depend on them | If used | If used |
SQLite: stop n8n before you copy. Copying the file while n8n writes to it "can produce an inconsistent backup," per the docs. Since n8n 2.0, the SQLite driver uses WAL mode, which keeps a separate -wal file beside the database, so a live copy of database.sqlite alone can miss data. Better still, move to Postgres. n8n's Docker Compose guide says "SQLite is fine for trying things out," but production instances running around the clock should use Postgres. Old data doesn't carry over by itself. The CLI's export:entities and import:entities commands handle the move.
Postgres: dump it live. The PostgreSQL manual says pg_dump "makes consistent exports even if the database is being used concurrently" without blocking readers or writers. It also says that except in simple cases, pg_dump "is generally not the right choice for taking regular backups of production databases." I treat one n8n database on one VPS as that simple case, and a nightly dump as enough for it. A large, busy database should look at continuous archiving instead.
Keep Postgres pinned to a major tag, like the postgres:18 in n8n's example, and don't bump it (16 to 18, say) as part of an n8n update. n8n's docs warn that bumping the tag on an existing setup fails with database files are incompatible with server.
Export workflows and credentials with the n8n CLI
The CLI gives you a second, readable copy. The documented commands:
n8n export:workflow --backup --output=backups/workflows/
n8n export:credentials --backup --output=backups/credentials/
--backup sets --all --pretty --separate: one JSON file per workflow and per credential. Use separate folders. n8n lists a known issue where import:credentials --separate fails if workflow files share the folder.
In Docker, n8n documents docker exec -u node -it <n8n-container-name> <n8n-cli-command>. With Compose, prefix commands with docker compose exec -u node n8n. The gotcha: "a backups/ directory inside the container isn't persisted." Bind-mount a host folder or copy the files out before the next update recreates the container.
What the CLI backup misses: users and roles, execution history, variables and instance settings, including the encryption key. In n8n's words, it is "enough to move workflows between instances, but not enough to recover a full instance on its own." The credential files stay encrypted with your key.
Backup templates are the same kind of copy. n8n's template library has workflows like Back up your n8n workflows to Github, which "uses the public api to export all of the workflow data," and one that runs the n8n CLI over SSH inside the Docker container. Neither saves the encryption key, users or execution history. They sit next to a full backup, not in place of it.
The --decrypted flag writes every credential in plain text, for migrating to an instance with a different key:
n8n export:credentials --all --decrypted --output=backups/decrypted.json
The CLI docs warn: "All sensitive information is visible in the files." Delete it once the restore is done. Never put it in a scheduled job or an off-site bucket.
Restore with n8n import:workflow --separate --input=backups/workflows/ and the matching credentials command. On a fresh instance, finish the owner setup first, because imports need an existing user. Imports overwrite items with the same IDs, and workflows arrive deactivated.
Automate n8n backups with a cron script
Here's a nightly script I put together for this guide from the documented commands above. It isn't an official n8n procedure, so run it by hand once and test a restore before you schedule it. It assumes Compose lives in /opt/n8n with services named n8n and postgres, POSTGRES_USER and POSTGRES_DB set as in n8n's Postgres example, and rclone installed with an encrypted remote (set up below).
#!/usr/bin/env bash
set -euo pipefail
umask 077 # new backup files readable by root only
PROJECT_DIR=/opt/n8n # the folder with your compose file
COMPOSE_FILE_NAME=compose.yml # or compose.yaml / docker-compose.yml
N8N_VOLUME=n8n_n8n-data # check yours with: docker volume ls
OFFSITE=n8n-crypt: # your rclone crypt remote
KEEP_DAYS=14 # local backups to keep
cd "$PROJECT_DIR"
STAMP=$(date +%F-%H%M)
DEST=/var/backups/n8n/$STAMP
mkdir -p "$DEST"
# 1. Database dump, taken while n8n keeps running
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"' > "$DEST/n8n-db.dump"
# 2. The .n8n folder: encryption key, plus filesystem binary data
docker volume inspect "$N8N_VOLUME" > /dev/null # stop if the volume name is wrong
docker run --rm -v "$N8N_VOLUME":/data:ro -v "$DEST":/backup ubuntu \
tar cf /backup/n8n-data.tar -C /data . || [ $? -eq 1 ]
tar tf "$DEST/n8n-data.tar" ./config > /dev/null # stop if the key file is missing
# 3. Deployment config for the restore (contains secrets)
cp "$COMPOSE_FILE_NAME" "$DEST/"
cp .env "$DEST/" # delete this line if you have no .env
# 4. Readable CLI exports, copied out of the container
docker compose exec -T -u node n8n n8n export:workflow --backup --output=/tmp/export/workflows/
docker compose exec -T -u node n8n n8n export:credentials --backup --output=/tmp/export/credentials/
docker compose cp n8n:/tmp/export "$DEST/export"
docker compose exec -T -u node n8n rm -rf /tmp/export
# 5. Encrypted off-site copy, then local retention
rclone copy "$DEST" "$OFFSITE$STAMP"
find /var/backups/n8n -mindepth 1 -maxdepth 1 -type d -mtime +"$KEEP_DAYS" -exec rm -rf {} +
# 6. Heartbeat, only reached if every step above succeeded
curl -fsS -m 10 --retry 5 -o /dev/null https://hc-ping.com/your-uuid-here
Save it as /opt/n8n/backup.sh, lock it down and run it once by hand:
sudo chmod 700 /opt/n8n/backup.sh
sudo /opt/n8n/backup.sh
If it finishes and your heartbeat check shows a ping, schedule it. The script needs Docker access, so it goes in root's crontab: open it with sudo crontab -e and add this line to run it at 03:15 every night (fields per the crontab format):
15 3 * * * /opt/n8n/backup.sh >> /var/log/n8n-backup.log 2>&1
What each guard in the script is for:
-Tdisables the pseudo-TTY thatdocker compose execallocates by default. Cron has no terminal.- The official Postgres image trusts socket connections made inside its own container, so
pg_dumpneeds no password there. - Compose prefixes volume names with the project name, which defaults to the folder name. n8n's current Compose file calls the volume
n8n-data, so a folder namedn8ngivesn8n_n8n-data. Older compose files usen8n_data. With a wrong name, Docker quietly creates an empty volume and the tar succeeds with nothing in it. Step 2 checks the name, then checks that the archive holdsconfig. - GNU tar exits with 1 when "some files were changed while being archived," which can happen on a live instance. The script accepts that and still stops on a fatal error (exit 2).
- n8n's export commands exit with an error when there's nothing to export. On a brand-new instance, the script stops at step 4 until you have at least one workflow and one credential.
set -estops at the first failure, so the heartbeat never fires and the Healthchecks.io check alerts you. Its free plan covers 20 jobs.umask 077makes the script create its backup folders readable by root only. Every folder holds your key.
Off-site, and encrypted. A backup on the same disk dies with the disk, so step 5 sends each night's folder away with rclone copy. Each folder holds the encryption key (in the tar and in .env) right next to the encrypted credentials, and together they open everything, so point step 5 at an rclone crypt remote: "Rclone crypt remotes encrypt and decrypt other remotes." Set up the storage remote, and the crypt remote on top of it, with rclone config as root (for example after sudo -i). Root runs the script, and rclone keeps its config per user. Keep the crypt password in your password manager next to the n8n key.
rclone copy "Doesn't delete files from the destination," so the remote grows until you prune it. rclone delete --min-age 30d n8n-crypt: removes files older than 30 days, and --rmdirs clears the empty folders. Locally, the find line in step 5 deletes backup folders older than KEEP_DAYS.
On SQLite, delete step 1 and move step 4 above step 2, because the exports need n8n running. Then wrap the step 2 commands in a stop and a start:
docker compose stop n8n
trap 'docker compose start n8n' EXIT # restarts n8n even if the copy fails
# ...step 2 here: the tar now includes database.sqlite
docker compose start n8n
trap - EXIT
Restore n8n from a backup (and test the restore)
A backup you haven't restored is a guess. n8n's documented restore has five steps: stop n8n, restore the .n8n folder (plus any custom-node directories), restore the Postgres database, restore the deployment configuration, start n8n. Below is my version of those steps for the script above. It isn't an official n8n procedure, so run it on a throwaway server before you rely on it.
In place, on the same server (this is also the rollback). Run it as root (for example after sudo -i), because the backup folders are root-only:
cd /opt/n8n
BACKUP=/var/backups/n8n/2026-09-19-0315 # the folder to restore
N8N_VOLUME=n8n_n8n-data
docker compose stop # n8n, workers and Postgres
cp "$BACKUP/compose.yml" "$BACKUP/.env" ./ # old settings, including N8N_VERSION
docker compose up -d --wait postgres # start only the database
# Drop the database and recreate it from the dump
docker compose exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d postgres --clean --if-exists --create' < "$BACKUP/n8n-db.dump"
# Empty the .n8n volume, then extract the backup into it
docker run --rm -v "$N8N_VOLUME":/data -v "$BACKUP":/backup ubuntu \
sh -c 'find /data -mindepth 1 -delete && tar xf /backup/n8n-data.tar -C /data'
docker compose up -d
Two details in there matter:
- Drop and recreate the database. After an update, n8n has already migrated the live database forward. A plain
pg_restoreinto it collides with existing objects and leaves behind tables the newer version added.--clean --createis PostgreSQL's documented way to reload a dump "into the same database it was dumped from, discarding the current contents of that database." pg_restore reads the dump from standard input when you don't name a file. - Empty the volume before extracting. Untarring on top of it leaves behind files the newer version created. n8n 3.0 renames
binaryDatatostorage, and per its notes, "If both directories exist, n8n doesn't start."
Use your compose file's real name in the cp line, and drop .env if you don't have one. On SQLite, skip the two Postgres steps: the database comes back with the volume. Copying the old .env back also puts the old N8N_VERSION back, which is what a rollback needs. --wait holds until Postgres reports healthy, which works because n8n's Postgres example defines a healthcheck (docker compose up).
On a fresh test server:
- Install Docker and create the same folder,
/opt/n8n, so Compose derives the same project and volume names. Copy in the compose file,.envand one backup folder. - In the test
.env, pointN8N_WEBHOOK_URL(orWEBHOOK_URL) at a test hostname. - Run
docker compose up --no-start. It creates the volumes and containers without starting anything. - Run the
up -d --wait postgres, database and volume commands from the block above. - Before n8n starts for the first time, unpublish every workflow:
docker compose run --rm n8n unpublish:workflow --all. - Run
docker compose up -d, log in with your production owner account and test a few credentials. If they connect, the backup is real.
Don't skip step 5. A database restore keeps every workflow's published state, so a copy started as-is runs every schedule and polling trigger against real systems with working credentials: duplicate emails, CRM writes, alerts in your real Slack. n8n's CLI docs say the publish and unpublish commands "operate on your n8n database," the image's entrypoint passes arguments straight to n8n, and docker compose run doesn't publish the service's ports. That one-off command is still my composition, so check the workflow list after the first start before you touch anything.
How to update self-hosted n8n in Docker Compose
Updating the n8n version on a self-hosted Community edition instance works the same on any VPS. n8n's update page says "Try to update at least once a month," because jumping many versions at once is riskier.
Pin an exact version. The n8n 2.0 notes say "Pin your n8n version to a specific version number". The official Compose file does this with image: docker.io/n8nio/n8n:${N8N_VERSION} and the number in .env:
N8N_VERSION=2.39.8
That was the stable release at the time of writing (September 2026). Get the current one from GitHub releases. If your image: line has no tag, like image: n8nio/n8n, change it to docker.io/n8nio/n8n:${N8N_VERSION} and add the N8N_VERSION line to .env, or write the version straight into the image line. In n8n's current Compose file, N8N_VERSION also pins the task runners image, and a separate N8N_SANDBOX_VERSION pins the sandbox services.
Avoid floating tags like stable or latest in production. Nothing in the docs promises they won't jump to a new major version once n8n promotes one. Skip auto-updaters like Watchtower too. Its README now says "This project is no longer maintained," and its maintainers write that they "do not recommend using Watchtower in a commercial or production environment." An auto-updater also pulls the new version before anyone has read the release notes.
If you installed with n8n's one-line setup, use its upgrade command instead: curl -fsSL https://get.n8n.io | sh -s -- --upgrade. It "edits two lines in .env: N8N_VERSION and N8N_SANDBOX_VERSION," and --version plus a number picks a specific release. Run it from the folder you installed from (n8n's everyday commands for that setup point at ./n8n/compose.yml), and take the backup first.
Read the release notes. n8n follows semantic versioning, so majors carry "incompatible changes which can require user action." The release notes flag breaking changes.
Test on a copy. n8n suggests Environments for this, but Environments isn't in the free Community edition. What I'd do instead: restore last night's backup on a throwaway server, following the fresh-server steps above, and run the new tag there first.
Then update:
cd /opt/n8n
sudo /opt/n8n/backup.sh # n8n recommends a full backup before updating
nano .env # set N8N_VERSION to the new number
docker compose pull
docker compose up -d
docker compose logs -f --tail=100 n8n
docker compose up recreates containers whose image changed, "preserving mounted volumes." n8n's older Docker page adds docker compose down between pull and up, which also works. Never run docker compose down -v: it deletes named volumes, key included. docker compose pull pulls every service's image, which is one more reason to keep Postgres on a major tag. The last line follows the logs. Afterwards, confirm /healthz/readiness returns 200 and run one real workflow.
Roll back a failed n8n update: restore, don't re-tag
n8n doesn't document downgrades as supported. Its 3.0 notes mention, while explaining its node development tooling, that "n8n only migrates a database forward." Key rotation is one-way as well: the rotation docs say "Older versions can't decrypt the new format." The rollback I recommend is the in-place restore with the backup you took right before the update: stop, put back the old compose file and .env, restore the dump and the .n8n volume, start.
Prepare for n8n 3.0 (scheduled for October 2026)
n8n has scheduled 3.0 for October 2026. Among the changes that hit self-hosters:
- Self-hosted n8n will require Docker.
npmandnpx n8ninstalls lose support. - On first start,
~/.n8n/binaryDatais renamed to~/.n8n/storage. If you mount a volume atbinaryData, remount it or setN8N_STORAGE_PATH. If both folders exist, n8n doesn't start. - Legacy nodes are removed, including Function, Function Item, Item Lists, Cron, Interval and the Read/Write Binary File nodes, along with version 1 of the AI Agent node. Older workflows that still use them need migrating first.
- Installing unverified community nodes from npm is off by default:
N8N_UNVERIFIED_PACKAGES_ENABLEDchanges fromtruetofalse. N8N_RUNNERS_TASK_TIMEOUTdefaults to 60 seconds instead of 300, so longer Code node tasks fail unless you set it.- Key rotation is listed as enabled by default, so I treat the 3.0 upgrade as one-way.
To prepare, read your 2.x startup logs (n8n logs a deprecation warning for each 3.0 configuration change that affects your instance) and open Settings > Migration Report. Then read the full 3.0 page for what those two don't cover, take a full backup and stay pinned until you've worked through it.
Keep the n8n database small with execution pruning
Without limits, "your n8n database can grow in size and run out of storage," say the execution data docs. Pruning is on by default: finished executions older than 336 hours (14 days) are deleted, and so are the oldest ones past 10,000. Annotated executions are never pruned. n8n's documented example for 7 days and 50,000 executions:
services:
n8n:
environment:
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000
If your environment: block uses KEY: value lines, as n8n's Postgres example does, write these the same way. Mixing the two styles breaks the file. Or put them in .env if the service loads it.
EXECUTIONS_DATA_SAVE_ON_SUCCESS=none stops saving successful runs. Keep EXECUTIONS_DATA_SAVE_ON_ERROR=all (the default), because the execution link in your error alerts only exists for saved executions. On SQLite, pruned space isn't returned to the disk. DB_SQLITE_VACUUM_ON_STARTUP=true reclaims it, at the cost of a "long running blocking operation" on every start.
Monitor n8n: /healthz, /healthz/readiness and /metrics
n8n has three status endpoints:
/healthzreturns 200 when the instance is reachable. "It doesn't indicate DB status."/healthz/readinessreturns 200 only when the database is connected and migrated./metricsserves Prometheus metrics, off by default.
Point uptime monitors at /healthz/readiness. /healthz can answer fine while Postgres is unreachable. On the server, n8n's install guide checks with curl -sf http://localhost:5678/healthz. Queue mode workers need QUEUE_HEALTH_CHECK_ACTIVE=true before they serve /healthz and /healthz/readiness.
For metrics, set N8N_METRICS=true. n8n's warning is plain: "Don't make it accessible on the public internet, as it can reveal sensitive operational data about your n8n instance." /metrics is served at your instance URL, right next to the editor. Block that path at the reverse proxy and let only Prometheus reach it.
Also watch the logs for "Allocation failed - JavaScript heap out of memory". n8n's memory docs list that message for self-hosted instances and cover the two ways out: give n8n more memory, or make it use less.
Get alerts when an n8n workflow fails
An automation that fails quietly is worse than none. n8n's answer is the Error Workflow:
- Create a workflow that starts with the Error Trigger node, named something like
Error Handler. - Add a Slack or email step with
workflow.name,execution.error.message,execution.lastNodeExecutedandexecution.url, so the alert links to the failed run. - Save it. It doesn't need to be published.
- In each production workflow, open Options > Settings, choose it under Error workflow and save. One error workflow can serve them all.
Per the Error Trigger docs, it only fires when an automatic run fails. To test it, build a small workflow with a Schedule Trigger that ends in a Stop And Error node, select your Error Handler in its settings and publish it, since a Schedule Trigger only runs on a published workflow. Wait for one run, then unpublish it. If the trigger node itself fails, the payload has no execution ID or URL.
The blind spot: the Error Workflow runs inside n8n. If the server, Docker or Postgres is down, nothing fires. So add an outside check on /healthz/readiness:
- UptimeRobot: the free plan covers 50 monitors at 5-minute intervals. The pricing page pitches it at hobby and non-profit projects, but the terms say it "is available for any use, including commercial and business use," subject to a fair use policy.
- Uptime Kuma: a self-hosted monitor with checks down to 20-second intervals. Run it on a different machine, or it goes down with n8n.
One gap is left: a scheduled workflow that simply stops running raises no error, and the outside check stays green. For the few workflows that must run, end them with an HTTP Request node that pings a Healthchecks.io check, the same heartbeat the backup script uses. When a run doesn't happen, the missed ping alerts you.
When to switch n8n to queue mode
Queue mode has the main instance handle timers and webhooks and pass executions through Redis to separate workers. It's in the free Community edition, and the docs call it the mode with "the best scalability." Two things are requirements: no filesystem binary data, and the same encryption key on every worker. n8n also advises against SQLite in queue mode and recommends a worker concurrency of 5 or more. I wouldn't start a small team on it. Add it when one instance can't keep up.
Secure a self-hosted n8n instance
- HTTPS through a reverse proxy. n8n's SSL page recommends "a reverse proxy like Traefik or a Network Load Balancer (NLB) in front of the n8n instance." My Oracle guide uses Caddy. My Hostinger guide follows Hostinger's NGINX setup.
- Tell n8n about the proxy. Set
N8N_WEBHOOK_URL=https://n8n.yourdomain.com/andN8N_PROXY_HOPS=1, and make sure the proxy forwardsX-Forwarded-For,X-Forwarded-HostandX-Forwarded-Proto, per the reverse proxy docs.WEBHOOK_URLis deprecated from 2.35.0. On 2.35.0 or later, rename it toN8N_WEBHOOK_URL. On older versions, keepWEBHOOK_URLuntil you update. - Bind the port to localhost when a proxy on the same server fronts n8n:
"127.0.0.1:5678:5678", as n8n's own Traefik example does. Docker's docs warn "Publishing container ports is insecure by default," and ufw won't catch it: published-port traffic "gets diverted before it goes through the ufw firewall settings," per Docker's firewall page. - Protect production webhooks. The Webhook node supports Basic auth, Header auth and JWT auth, plus an IP(s) Allowlist option.
n8n auditflags unprotected webhooks, and this is how you clear that finding. - Own the login. Basic auth was removed in n8n 1.0, and there's no supported way to turn the login screen off. The first person to finish the owner setup screen becomes the owner, so create that account the minute the instance is reachable. Turn on 2FA for each account. Enforcing it instance-wide needs a Business or Enterprise license.
- Run the audit.
docker compose exec -u node n8n n8n auditis the Docker form of n8n'sn8n auditcommand. It reports unprotected webhooks, missing security settings and an outdated version. Disable the public API if nothing uses it, including API-based backup workflows. - Keep the editor off the public internet if you can. My recommendation, not an n8n rule: put the editor behind a VPN or IP allowlist and leave these paths public:
/webhook/,/webhook-waiting/,/form/and/form-waiting/(defaults in n8n's source),/mcp/if you use the MCP Server Trigger, and/healthzplus/healthz/readinessfor your uptime monitor. n8n doesn't publish a complete list of paths that must stay open. After locking down, test every webhook, form and OAuth login, and confirm your uptime monitor still gets a 200.
An earlier client setup
My self-hosted AI stack case study shows a 2025 client setup on Oracle Cloud's free tier: n8n on Postgres behind Caddy, with a runbook for updates and database backups. This checklist goes further than that build. The Community edition is free, but this list is the ops work you sign up for. My n8n pricing guide weighs it against n8n Cloud, or you can run your own numbers in my n8n cost calculator.
Want it set up for you?
My Production n8n setup covers the same six areas on a server you own: server, security, database, backups, updates and monitoring, with a written update routine. How much of the list above goes in depends on your server and volume, so I scope it and quote it in writing within 24 hours of a call. Details are on my n8n consultant page, or book a 30-minute call.
Sources
- n8n docs: Back up and restore
- n8n docs: Set a custom encryption key
- n8n source: instance settings (encryption key check)
- n8n docs: Use the command line
- n8n docs: Install using Docker Compose
- n8n: official Compose file
- n8n: Docker image entrypoint
- n8n docs: One-line setup
- n8n docs: Update n8n
- n8n docs: Release notes
- n8n docs: breaking changes in 2.0 and 3.0
- n8n docs: Rotate encryption keys
- n8n docs: Community edition features
- n8n docs: Manage execution data
- n8n docs: Monitor n8n and Enable Prometheus metrics
- n8n docs: Memory-related errors
- n8n docs: Error handling, Error Trigger and Schedule Trigger
- n8n docs: Queue mode
- n8n docs: Set up SSL and webhook URLs behind a reverse proxy
- n8n docs: Webhook node and MCP Server Trigger
- n8n source: endpoint path defaults
- n8n docs: User management, 2FA, security audit and disable the public API
- n8n templates: 1534 and 16669
- n8n releases on GitHub
- Docker docs: compose up, compose exec, compose run, compose cp, compose logs, volumes, port publishing and packet filtering and firewalls
- Official Postgres image on Docker Hub
- PostgreSQL docs: pg_dump and pg_restore
- SQLite: write-ahead logging
- GNU tar: exit codes
- Ubuntu manpages: crontab(5) and find(1)
- OpenSSL: openssl-rand
- rclone docs: copy, crypt, config and delete
- Healthchecks.io: bash docs and pricing
- UptimeRobot: pricing and terms
- Uptime Kuma on GitHub
- Watchtower on GitHub
Versions and plan limits were checked in September 2026. n8n releases a new minor version most weeks. Check the current version and the 3.0 date before you act on them.
Frequently asked questions
What do I need to back up for self-hosted n8n?
Back up the .n8n folder, which holds the config file with the credential encryption key and, on SQLite, the whole database. If you run Postgres, add a pg_dump of the database. I also keep the compose file and .env, because a restore needs your database settings and any custom N8N_ENCRYPTION_KEY. Add custom node folders and external storage if you use them. CLI exports alone are not a full backup: they skip users, execution history and instance settings.
What happens if I lose the n8n encryption key?
Your stored credentials become unreadable. n8n encrypts credentials with that key before saving them, and its docs say a restored database or encrypted credential export can't be decrypted without it. You would have to re-enter every API key and OAuth connection by hand. Copy the key from /home/node/.n8n/config into a password manager and pin the same value as N8N_ENCRYPTION_KEY.
How do I update n8n in Docker Compose without losing data?
Pin the image to an exact version, such as n8nio/n8n:2.39.8 (in n8n's official Compose file, that's N8N_VERSION in .env). Read the release notes, take a full backup, change the version, then run docker compose pull and docker compose up -d. Compose recreates the n8n container and keeps mounted volumes, so your data stays. Never run docker compose down -v, which deletes named volumes. Check the logs and /healthz/readiness afterwards. n8n suggests updating at least once a month.
How do I roll back a failed n8n update?
Restore the backup you took right before updating. Stop the stack, put back the old compose file and .env (which also restores the old N8N_VERSION), recreate the database from the dump with pg_restore --clean --create, empty the .n8n volume and extract the backup into it, then start. Don't rely on changing the image tag alone: n8n doesn't document downgrades as supported, and once encryption key rotation is on, older versions can't decrypt the new format.
What changes in n8n 3.0 for self-hosted instances?
n8n has scheduled 3.0 for October 2026. For self-hosters, Docker becomes the only supported install, ~/.n8n/binaryData is renamed to ~/.n8n/storage on first start, legacy nodes such as Function, Cron and Interval are removed, installing unverified community nodes is off by default, the task runner timeout for Code node tasks drops from 300 to 60 seconds, and key rotation is listed as on by default. Check Settings > Migration Report and take a full backup before you upgrade.
How do I get alerts when an n8n workflow fails?
Create a workflow that starts with the Error Trigger node and sends a Slack or email message, then select it under Error workflow in each workflow's settings. It fires only when an automatic execution fails, not on manual test runs. Because it runs inside n8n, it can't tell you when n8n itself is down, so add an external uptime check on /healthz/readiness as well.