“Zero-downtime deployment” for self-hosted n8n is two different problems wearing one name, and conflating them is how people end up with a half-applied database schema at 2 a.m. The first problem is keeping the data plane available: webhook ingress and queued execution. That one is solvable. The second is upgrading the shared database and the control plane, and that one is not truly zero-downtime on the community edition. There is a single main process, every instance has to run the same version, and schema migrations run on startup and are not reversed automatically when you downgrade.
So the honest goal is not “zero downtime.” It is “no dropped webhooks, no lost executions, and a control-plane blip short enough to be harmless.” The procedure that gets you there forks on a single question, asked before you touch anything: does this release ship database migrations? This guide gives you both procedures, the topology they assume, and the failure modes that turn a clean upgrade into an outage.
This targets n8n 2.x in queue mode, with notes for 1.x where behavior differs. It assumes working knowledge of queue mode, PostgreSQL, Redis, containers, and a reverse proxy or load balancer.
What zero-downtime means for n8n
Before any procedure, set expectations on which components can stay up and which cannot. Three facts make a naive rolling or blue-green swap of the whole stack unsafe.
First, all main and worker processes must run the same n8n version. n8n’s own multi-main prerequisites list this explicitly, and the reason generalizes to every queue-mode topology: job payloads and the database schema are tied to the running version, so a worker on a different version than the main can mishandle queued jobs or write against a schema it does not expect.
Second, the database is shared and single. Workflows, encrypted credentials, and execution data all live in one PostgreSQL instance that every process reads and writes.
Third, schema migrations run on startup and are not reversed on their own. When you start a new version against an older schema, n8n applies the pending migrations before the instance becomes ready. There is an n8n db:revert command to undo them, but it rolls back a single migration per run and you have to run it on the current version before you install the older one, per the downgrade instructions; across a release that ships several migrations that is slow and easy to get wrong. So in practice the reliable rollback is “restore the pre-upgrade backup and redeploy the previous image,” not “downgrade the binary and walk the migrations back by hand.”
Put those together and the thing you cannot safely do becomes obvious: run old code and new code against one migrated schema at the same time. Old code does not know about the new columns; new code assumes they exist. That is the boundary. Everything below splits into “keep the data plane alive” and “cut over the schema and control plane,” because those are the two halves the single name hides.
One 2.x-specific wrinkle affects capacity planning during a deploy. As of n8n 2.0, task runners are enabled by default, so Code node executions run in isolated processes rather than inside the main. This is a security hardening change, not a deployment feature, but it means your worker and main containers carry more process overhead than they did on 1.x. If you run task runners in external mode, note that the main n8nio/n8n image no longer bundles the runner and you use the separate n8nio/runners image, per the 2.0 breaking changes.
The deployment unit: queue-mode topology and per-process blast radius
You can only design a good deploy if you know which process holds state and which is disposable. Queue mode splits n8n into four roles plus two backing stores.
The main process serves the editor UI and the public API, and it owns scheduling: it fires Schedule and other trigger and poller nodes, and in a single-main setup it is the only instance doing so. It can also receive production webhooks, though in a scaled setup you move those off it. Restarting the main interrupts the editor and API and pauses timer-driven triggers for the duration of the restart. It does not, by itself, destroy queued or running executions.
Workers are stateless. They pull jobs from Redis, execute the workflow, write results to PostgreSQL, and report back. Because they hold no durable state, they are safely drainable: you can stop a worker after it finishes its in-flight jobs and lose nothing.
Webhook processors are also stateless. They expose the public webhook endpoints, accept incoming HTTP requests, and enqueue the resulting executions to Redis for workers to run. They listen on the same port as the main (default 5678) and scale horizontally behind a load balancer. The queue-mode docs are explicit that you should not add the main to that load-balancer pool, because routing webhook traffic to it degrades editor and UI performance.
Redis is the BullMQ broker that holds the queue of pending and active executions. PostgreSQL is the single source of truth for workflows, encrypted credentials, and execution history.
The blast radius, summarized: restart a worker and you lose nothing if you drain it first; restart a webhook processor and ingress continues as long as another processor is in the pool; restart the main and you pause scheduling and the UI but keep durable executions intact. The database and Redis are the two things you never want to interrupt mid-write.
The hard constraint: shared database and on-startup migrations
This is the load-bearing limitation behind everything else. On startup, a new n8n version applies any pending schema migrations before it finishes coming up, and on a large execution table this can take real time. The instance is not ready until they finish, so the operational rule during an update is to watch the logs and wait for the migrations to complete, rather than assume the container is healthy the moment it starts.
Two consequences follow.
Interrupting a migration is dangerous. If you kill the migrating process partway through, you can be left with a half-applied schema that neither the old nor the new version expects. So the migrating instance must be allowed to finish.
Rollback is restore, not downgrade. You can walk migrations back by hand with n8n db:revert, one migration at a time, but keeping a freshly migrated schema under the previous image is not something to gamble a production instance on. The dependable rollback is: restore the database from the pre-upgrade backup and redeploy the previous image tag. This is why the backup is not optional and why you record the encryption key before you start (more on that below).
There is also a coordination concern when more than one instance starts at once. If several instances come up simultaneously against an unmigrated schema, you are relying on them not to race each other through the migration. The safe operational rule, which sidesteps the question entirely, is to let exactly one instance run the migrations to completion first, confirm it is up, and only then start the rest. The procedures below are built around that rule rather than around any assumption about internal locking.
Decide first: does this upgrade ship migrations?
The entire procedure forks here, so run this check before anything else.
A migration-free patch release can be upgraded with a near-zero-downtime rolling restart, Pattern A. A release that changes the schema needs a drain-and-cutover, Pattern B. Three ways to find out which one you are dealing with, in order of convenience:
Use the built-in Migration Report. Navigate to Settings then Migration Report. It surfaces workflow-level and instance-level issues you must address before upgrading to 2.0, and it is documented in the v2.0 migration tool docs. It is available from version 1.121.0 and visible to global admins only.
Read the release notes and the version-specific breaking-changes page. The release notes flag schema and behavior changes per version, and major versions get a dedicated breaking-changes page like the 2.0 one.
When in doubt, diff the migrations directory between your current image tag and the target tag. A new migration file is the unambiguous signal that you are on Pattern B.
If the release is a major version, assume Pattern B and assume there is preparation work beyond the deploy mechanics. For 2.0 specifically, MySQL and MariaDB are no longer supported, so if you are on either you must migrate to PostgreSQL before upgrading.
Pattern A: near-zero-downtime upgrade for migration-free releases
With no schema change, old and new code can coexist briefly against the same schema, so you can roll the disposable processes one at a time.
The order is: drain and replace workers, roll webhook processors through the load balancer, then restart the main last.
The mechanism that makes draining safe is graceful shutdown. When a worker receives SIGTERM, n8n stops accepting new jobs and lets in-flight executions finish, bounded by N8N_GRACEFUL_SHUTDOWN_TIMEOUT, which defaults to 30 seconds. This variable replaces the deprecated QUEUE_WORKER_TIMEOUT; both are described in the queue-mode environment variables reference. The catch is that your orchestrator has to deliver SIGTERM to the n8n process, and the grace period has to be long enough for your longest execution, or work gets cut off. Size the timeout to reality, not to the default.
For webhook processors, put readiness probes and load-balancer connection draining in front of them so traffic stops being routed to a processor before it exits. As long as at least one processor stays in the pool, ingress never fully drops.
Here is a queue-mode stack as a starting point. It pins n8n 2.23.2, the current stable at the time of writing in June 2026; substitute the version you are targeting and pin the same tag on every n8n service. It uses the current docker compose plugin and the compose.yaml filename, omits the obsolete top-level version: field, and pins postgres:18 and redis:8, the current stable major lines for each.
x-n8n-env: &n8n-env EXECUTIONS_MODE: queue DB_TYPE: postgresdb DB_POSTGRESDB_HOST: postgres DB_POSTGRESDB_PORT: 5432 DB_POSTGRESDB_DATABASE: n8n DB_POSTGRESDB_USER: n8n DB_POSTGRESDB_PASSWORD: REPLACE_WITH_STRONG_DB_PASSWORD QUEUE_BULL_REDIS_HOST: redis QUEUE_BULL_REDIS_PORT: 6379 N8N_ENCRYPTION_KEY: REPLACE_WITH_GENERATED_ENCRYPTION_KEY N8N_RUNNERS_ENABLED: "true" WEBHOOK_URL: https://n8n.example.com/
services: postgres: image: postgres:18-alpine restart: unless-stopped environment: POSTGRES_DB: n8n POSTGRES_USER: n8n POSTGRES_PASSWORD: REPLACE_WITH_STRONG_DB_PASSWORD volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"] interval: 10s timeout: 5s retries: 5
redis: image: redis:8-alpine restart: unless-stopped volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5
n8n-main: image: docker.n8n.io/n8nio/n8n:2.23.2 restart: unless-stopped ports: - "5678:5678" environment: <<: *n8n-env N8N_GRACEFUL_SHUTDOWN_TIMEOUT: "300" depends_on: postgres: condition: service_healthy redis: condition: service_healthy
n8n-worker: image: docker.n8n.io/n8nio/n8n:2.23.2 restart: unless-stopped command: ["worker", "--concurrency", "10"] environment: <<: *n8n-env N8N_GRACEFUL_SHUTDOWN_TIMEOUT: "300" depends_on: postgres: condition: service_healthy redis: condition: service_healthy
n8n-webhook: image: docker.n8n.io/n8nio/n8n:2.23.2 restart: unless-stopped command: ["webhook"] environment: <<: *n8n-env depends_on: postgres: condition: service_healthy redis: condition: service_healthy
volumes: postgres-data: redis-data:A few notes on the values, since the config carries no comments. The shared environment anchor guarantees every n8n service runs with the same database, Redis, and encryption settings, which is exactly the consistency the same-version rule depends on. N8N_GRACEFUL_SHUTDOWN_TIMEOUT is set to 300 seconds here rather than the 30-second default, on the assumption that some executions run for minutes; tune it to your longest job. The worker subcommand is worker and the webhook subcommand is webhook, because the image entrypoint is the n8n binary and you pass the subcommand to it. This is the form n8n moved to in the 1.0 migration, where the entrypoint changed so you write worker --concurrency=5 rather than prefixing it with n8n. The worker exposes optional health endpoints when you enable QUEUE_HEALTH_CHECK_ACTIVE: /healthz reports liveness (the process is up), and /healthz/readiness reports whether its database and Redis connections are ready, which is the one your orchestrator’s readiness probe should hit, per the queue-mode docs.
In Compose terms, Pattern A is: docker compose pull, then recreate n8n-worker and n8n-webhook (scaling them so a replacement is healthy before the old one drains), then recreate n8n-main. On Kubernetes this is a standard rolling update with terminationGracePeriodSeconds set at or above your shutdown timeout and a readiness probe on each pod.
Pattern B: minimal-downtime cutover for migration releases
When the schema changes, two versions cannot serve at once, so this is minimal-downtime, not zero. The goal is to make the unavoidable migration window short and non-destructive.
The sequence:
- Take a full backup of the PostgreSQL database and record the value of
N8N_ENCRYPTION_KEY. The database holds your encrypted credentials; the key decrypts them. A restore without the matching key leaves every credential unreadable, which is the most common way a “successful” rollback still ruins your day. - Run the Migration Report and fix every flagged workflow and instance issue before you go further. For a major version this is where the real work is.
- Quiesce scheduling and let workers drain. Stop the trigger and timer source by taking the main offline (or disabling production triggers on it), and send
SIGTERMto workers so in-flight executions finish under the graceful-shutdown timeout. - Keep webhook processors accepting and queuing incoming requests for as long as your buffer allows, so external callers are not bounced during the window. This is the part that protects ingress while the control plane is mid-cutover.
- Upgrade the image and let a single main start and run the migrations to completion. Watch the logs and confirm the editor becomes reachable before proceeding. Do not start a second main or the workers until this one reports it is up.
- Bring workers and webhook processors back on the new version, pinned to the same tag.
The irreducible point: between step 3 and the end of step 5 there is a window during which no instance is executing new work on a consistent, migrated schema. You are minimizing that window and making sure nothing is lost during it, not eliminating it. Rollback, if step 5 goes wrong, is to restore the backup from step 1 and redeploy the previous tag.
How destructive the window feels depends entirely on the safety net in the next two sections: buffered ingress and durable, idempotent executions are what let a short cutover pass without anyone downstream noticing.
How far Enterprise multi-main gets you, and where it still stops
The single main is the one true single point of failure in the community topology. The only way to remove it is multi-main, which requires an Enterprise license.
Multi-main runs several main instances with Redis-based leader election, so timers, pollers, and pruning fire exactly once across the cluster while the UI, API, and webhook handling spread across instances. You enable it with N8N_MULTI_MAIN_SETUP_ENABLED set to true (license required), behind a load balancer with sticky sessions. Two related variables tune the leader election: N8N_MULTI_MAIN_SETUP_KEY_TTL, the time-to-live in seconds for the leader key, which defaults to 10, and N8N_MULTI_MAIN_SETUP_CHECK_INTERVAL, the leader-check interval in seconds, which defaults to 3. These names matter, because incorrect variants circulate; the correct spellings are the ones in the queue-mode environment variables reference.
What multi-main buys you: migration-free upgrades get close to zero-downtime because you can roll mains one at a time while another keeps the control plane up, and the cutover for migration releases is smoother because the UI and webhook handling never go fully dark.
What it does not buy you: it does not escape the schema-migration cutover or the same-version rule. A migration release still cannot have two versions serving at once, so you still coordinate a cutover, and you still avoid running mixed versions across the fleet. The practical deploy detail is to keep exactly one leader-eligible main running through the schema cutover so scheduling does not split-brain across versions.
Protecting webhooks and executions during any deploy
This is the practical core of the promise in the title, and it is what makes the residual control-plane blip harmless.
Put dedicated webhook processors behind a load balancer and keep the main out of the pool. Ingress then lives on stateless processors you can scale and roll independently. You can also stop the main from handling production webhooks entirely by setting endpoints.disableProductionWebhooksOnMainProcess so that every production webhook is served by the processor pool, as described in the queue-mode docs. For routing, send /webhook/* and /webhook-waiting/* to the processor pool and keep /webhook-test/* and everything else, including the editor and REST API, pointed at the main.
A clarification on what survives a main restart, because this is where expectations and reality often diverge. Webhook processors receive a request and enqueue the execution; they depend on the database (which holds the active-workflow registrations) and on Redis, not on the main being up at that instant. So for workflows that were already active before the deploy, incoming webhooks can continue to be received and queued while the main restarts briefly, provided the processors, the database, and Redis stay up. The operations that require the main are activating or deactivating workflows and firing time-based triggers. The practical guardrails that follow from this: do not change workflow activation state during a deploy, keep the database and Redis untouched, and after the main returns, confirm your webhooks are still registered before you call the deploy done. There is a long-standing failure mode where webhooks stopped responding after a restart when persistence was missing or a workflow needed re-activation, captured in issue #1122; persistent storage and leaving activation alone are what prevent it.
Make workflows idempotent. Design them so a re-delivered webhook or a re-run job does not double its effects: use idempotency keys, upserts, and bounded retries with backoff. This is what lets you treat a queued execution as safe to retry.
Lean on durability. Queued executions live in Redis and the database, so they survive a restart. n8n’s concurrency control makes this explicit: on instance startup, n8n resumes queued executions up to the concurrency limit and re-enqueues the rest. Concurrency applies to production executions, those started by a webhook or trigger, and you set the limit with N8N_CONCURRENCY_PRODUCTION_LIMIT. The combined effect is that a short main restart, with buffered ingress and durable executions behind it, loses nothing.
Operational guardrails and failure modes
The mistakes that turn a zero-downtime attempt into an outage, each with its preventive measure.
Version mismatch across the fleet. A worker on a different version than the main can break job formats and run into schema mismatches. Pin one image tag on every n8n service and bump them together.
A lost or rotated encryption key. Change or lose N8N_ENCRYPTION_KEY and every stored credential becomes unreadable, because the key is what decrypts them in the database. Record it before any upgrade, share the same value across main, workers, and webhook processors, and never rotate it without a planned re-encryption. Note that an instance with no key set generates a fresh one on first start, so always set it explicitly.
SIGTERM that never reaches the process. If your container runs n8n as a child of a shell that does not forward signals, graceful shutdown never runs and in-flight executions are killed. Make sure the n8n process receives SIGTERM directly, so the graceful-shutdown path can run.
An interrupted migration. Killing a main mid-migration can leave a half-applied schema. Never stop a migrating instance, and always have the pre-upgrade backup so the worst case is a restore rather than a manual schema repair.
A grace timeout shorter than your longest execution. If N8N_GRACEFUL_SHUTDOWN_TIMEOUT expires before a long job finishes, that job is cut off. Size it to your real maximum execution length.
SQLite under multiple processes. SQLite locks the whole file on write and cannot handle the concurrent access that workers require, so queue mode needs PostgreSQL. Running queue mode on SQLite is unsupported and risks corruption.
Which pattern, and what to expect
| Situation | Recommended pattern | Realistic downtime | Rollback |
|---|---|---|---|
| Patch release, no schema migration, queue mode | Pattern A: roll workers and webhook processors, main last | Near zero on ingress; brief main blip | Redeploy previous tag |
| Minor or major release with migrations, community edition | Pattern B: backup, drain, single-main migration, cutover | Minimal; bounded by the migration window | Restore backup plus previous tag |
| Migration release, Enterprise multi-main | Pattern B with leader-aware ordering | Minimal; smoother than community | Restore backup plus previous tag |
| Single instance, regular mode, any upgrade | No true zero-downtime path; schedule a maintenance window | Full restart plus migration time | Restore backup plus previous tag |
The shape of the whole thing: decide whether the release migrates, keep ingress and executions safe with dedicated webhook processors and idempotent workflows, and accept that the schema cutover is a window to minimize rather than a thing to eliminate. Do that and you can upgrade self-hosted n8n in production without dropping a webhook or losing an execution, which is the promise worth keeping, as opposed to the one the marketing phrase implies.