Tutorial

How to Run n8n in Production

By Amith Kumar25 July 202615 min read
Self-hostingn8n
How to Run n8n in Production
Verified 25 Jul 2026 · Ubuntu 24.04 LTS · Docker 29.1.3, nginx 1.24.0, PostgreSQL 16.14, Redis 7

What running n8n in production means

Running n8n in production means the automation platform stops being one process on one box and becomes a stack that shares work and survives a restart. The single container from the self-host n8n on a VPS guide runs the editor, the triggers and the workflow executions all in the same node process, which is fine while n8n is a toy and a problem the day it is not.

This guide takes that container and turns it into the production shape: a main process that serves the editor and holds the triggers, a Redis queue in the middle, and a pool of dedicated workers that actually run the executions.

This is for the person whose n8n has quietly become business-critical. The workflows now move real orders, sync real customers, and page real people, so a run that dies because one container was busy, or an instance that comes back empty after a reboot, is no longer an annoyance. The fix is queue mode: executions go onto a Redis queue and workers pull them off, so a slow workflow no longer blocks the editor and you can add capacity by adding a worker rather than a bigger box.

Here is the destination. The stack below is the real one we stood up for this guide: the main n8n process, two dedicated workers, the Redis that carries the queue, and the Postgres that holds every workflow and execution.

sudo docker compose ps
docker compose ps on the finished production stack showing five containers up: the main n8n process on 127.0.0.1:5678, two dedicated n8n-worker containers, the Redis holding the queue, and the Postgres database, the whole automation platform in queue mode on the lab slice n8n.boxlab.space
The real n8n 2.31.6 editor canvas at n8n.boxlab.space over real HTTPS, the Site uptime monitor workflow with a Schedule Trigger, an HTTP Request node fetching servercake.in and an Edit Fields node, the production workflow whose scheduled runs go through the Redis queue to a worker

Five containers, one platform. Everything durable lives in named volumes, so the last thing we do is prove it: take the whole stack down, bring it back, and watch the workflows, the past executions and the encrypted credentials all return. Every screenshot in this guide is genuine, captured on that live box.

What this guide covers
  • Why a single container runs the editor, triggers and executions in one process, and why that caps you.
  • Queue mode: setting EXECUTIONS_MODE=queue, adding Redis, and running a dedicated worker pool.
  • Proving the queue works: a worker executes the job, Redis holds it, and the main process stays responsive.
  • A real .env for the encryption key, database and Redis secrets, kept out of the Compose file.
  • Surviving a restart with restart policies, then proving workflows and executions come back intact.

Before you begin

This guide starts where the install pillar ends, so it assumes a little rather than starting from a bare box.

What you need first
  • A working self-hosted n8n from the install guide: n8n in Docker Compose, backed by Postgres, behind nginx over real HTTPS. This guide changes how that stack runs, so a working single-container setup is the starting line.
  • SSH access to the slice as a user with sudo, and the working directory that holds your docker-compose.yml and .env.
  • A slice with room to grow. Queue mode runs more processes, so 2 vCPUs and 4 GB of RAM is a comfortable size once a worker pool is involved, rather than the 2 GB a single container is happy on.
  • Your N8N_ENCRYPTION_KEY from the install. The whole point of this exercise is a stack that restarts cleanly, and it can only do that if the key stays the same across every container.

Everything below runs as a normal sudo user over SSH, from the directory that holds the Compose file.

Why a single container is not production

The default n8n container does three jobs at once. It serves the editor UI, it holds the active triggers and schedules, and it runs every workflow execution, all inside one node process. While traffic is light this is invisible.

It stops being invisible the moment a single heavy workflow, a large data transform or a slow external API, ties up that process: the editor lags, other executions wait their turn, and a burst of webhooks can push the whole thing over. One process is also one point of failure, and it can only ever use one box worth of CPU.

The production shape splits those jobs. The main process keeps the editor and the triggers, but it no longer runs executions itself. Instead it puts each execution onto a queue, and a separate pool of worker processes pulls jobs off that queue and runs them. The queue lives in Redis.

This is what n8n calls queue mode, and it buys three things: the editor stays responsive because executions run elsewhere, you scale throughput by adding workers instead of resizing the box, and a worker crashing takes its one job with it rather than the whole instance. The trade is one more moving part, Redis, and a few more containers to run, which is exactly what the rest of this guide sets up.

Queue mode with Redis and workers

Queue mode is three changes to the stack you already have: tell n8n to use the queue with EXECUTIONS_MODE=queue, add a Redis service for the queue to live in, and add a worker service you can scale. The workers run the same n8n image as the main process, just started with the worker command, and they read their configuration from the same environment. Because both share one encryption key and one database, they behave as one instance split across processes.

Here is the full Compose file. It keeps the Postgres from the install, adds Redis, and defines the main n8n service plus an n8n-worker service. A YAML anchor holds the environment both n8n services share so there is one place to edit it.

# /opt/n8n-prod/docker-compose.yml
name: n8nprod

x-n8n-env: &n8n-env
  N8N_HOST: n8n.example.com
  N8N_PROTOCOL: https
  N8N_EDITOR_BASE_URL: https://n8n.example.com/
  WEBHOOK_URL: https://n8n.example.com/
  N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
  DB_TYPE: postgresdb
  DB_POSTGRESDB_HOST: db
  DB_POSTGRESDB_DATABASE: n8n
  DB_POSTGRESDB_USER: n8n
  DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
  EXECUTIONS_MODE: queue
  QUEUE_BULL_REDIS_HOST: redis
  QUEUE_BULL_REDIS_PORT: "6379"
  QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
  QUEUE_HEALTH_CHECK_ACTIVE: "true"
  OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true"
  N8N_PROXY_HOPS: "1"
  N8N_RUNNERS_ENABLED: "true"
  GENERIC_TIMEZONE: Asia/Kolkata
  EXECUTIONS_DATA_PRUNE: "true"
  EXECUTIONS_DATA_MAX_AGE: "336"
  EXECUTIONS_DATA_PRUNE_MAX_COUNT: "10000"

services:
  db:
    image: postgres:16-alpine
    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: 5s
      timeout: 5s
      retries: 12

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 5s
      retries: 12

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      <<: *n8n-env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  n8n-worker:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    command: worker
    environment:
      <<: *n8n-env
    depends_on:
      n8n:
        condition: service_started
      redis:
        condition: service_healthy

volumes:
  db_data:
  redis_data:

A few lines carry the whole design. EXECUTIONS_MODE: queue is the switch that turns off in-process execution and sends every run to Redis. The three QUEUE_BULL_REDIS_* values point n8n at the Redis service and authenticate to it, because the queue holds your execution data in flight and should not be open.

OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true" sends even the manual "Execute workflow" runs from the editor to the workers, so nothing executes in the main process. The main service still publishes to 127.0.0.1:5678 for nginx to proxy, while the workers publish nothing: they only talk to Redis and Postgres.

Now bring it up, with two workers, in one command. The --scale n8n-worker=2 flag runs two copies of the worker service so jobs can run in parallel.

sudo docker compose up -d --scale n8n-worker=2
docker compose up -d --scale n8n-worker=2 starting Postgres and Redis, waiting for both to report healthy, running the main n8n so migrations apply once, then starting two dedicated worker containers in parallel

Compose starts Postgres and Redis, waits for both to report healthy, starts the main n8n so it can run the database migrations once, and then starts the two workers. Give it a moment and check the stack. You should see five containers: the database, Redis, the main process published on loopback, and two workers.

sudo docker compose ps
curl -s http://127.0.0.1:5678/healthz
Checkpoint You should now see five containers up, with db and redis healthy, the n8n main published on 127.0.0.1:5678, and two n8n-worker containers with no published port. A {"status":"ok"} from the health endpoint means the main process is up and connected to Postgres and Redis. If a worker keeps restarting, check that it can reach Redis with the password from the shared environment.

Prove the queue works

Queue mode is only real if a worker actually runs the executions, so prove it rather than assume it. Build a small production-style workflow in the editor: a Schedule Trigger set to run every minute, an HTTP Request that fetches a URL, and an Edit Fields node that shapes the result into a tidy record.

Give it a name like "Site uptime monitor", then toggle it Active. An active workflow with a Schedule Trigger is a production execution, which in queue mode means the main process enqueues it and a worker runs it.

Watch a worker take a job. Follow the worker logs and wait for the next minute to tick over.

sudo docker compose logs --tail=40 n8n-worker
The worker log proving a WORKER ran the execution: a matched pair of lines showing the worker starting the queued job by its id and the same job finishing successfully, so the run happened in a worker process and not in the main one

Each run prints a matched pair in the worker log: a line that the worker started the job with its id, and a line that the same job finished successfully. That is the execution running in a worker process, not in the main one. The main process only handed the job to Redis and moved on, which is why the editor stays quick while workflows run.

Look at the queue itself. Redis is holding the Bull queue that carries the jobs, and you can list its keys straight from the Redis container.

sudo docker compose exec redis \
     redis-cli -a "$REDIS_PASSWORD" --no-auth-warning keys 'bull:*'
Listing the Bull queue keys in Redis with redis-cli to show Redis is holding n8n's execution queue, then curl returning ok from the main process readiness endpoint at the same time, the editor responsive on one side while the workers run jobs on the other

Those bull:jobs:* keys are n8n's execution queue: the completed set, the id counter, and the metadata Bull uses to hand work to whichever worker is free. While the main process keeps serving the editor, confirm it is still answering its health endpoint, so you can see the split for what it is: the editor responsive on one side, the workers busy on the other.

curl -s http://127.0.0.1:5678/healthz/readiness

Every run is recorded. n8n stores its executions in Postgres, so you can confirm the scheduled workflow is landing rows by asking the database directly.

sudo docker compose exec db \
     psql -U n8n -d n8n -c \
     "select id, status, mode, finished from execution_entity order by id desc limit 5;"
Querying the Postgres execution_entity table directly: the newest rows show status success and mode trigger, proof the scheduled workflow fired, a worker ran it off the queue, and n8n saved every run
A successful execution of the Site uptime monitor on the n8n canvas at n8n.boxlab.space: the Schedule Trigger, HTTP Request and Edit Fields nodes each green with a checkmark, the run that was executed by a worker off the Redis queue

Rows with status = success and mode = trigger are your proof: the scheduled workflow fired, a worker ran it off the queue, and n8n saved the result. The Executions tab in the editor shows the same runs.

Environment and secrets done right

The Compose file above never contains a single secret. Every sensitive value is a ${...} reference that Compose fills in from a .env file sitting beside it, which means the Compose file is safe to commit and the secrets are not. This matters more in queue mode than it did with one container, because now three services, the database, Redis and n8n, each need a credential, and the encryption key has to be identical across the main process and every worker.

Create the .env with strong random values, and lock it down so only its owner can read it.

cd /opt/n8n-prod
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"  | sudo tee    /opt/n8n-prod/.env
echo "REDIS_PASSWORD=$(openssl rand -hex 24)"     | sudo tee -a /opt/n8n-prod/.env
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 24)" | sudo tee -a /opt/n8n-prod/.env
sudo chmod 600 /opt/n8n-prod/.env
Creating the .env beside the Compose file with strong random values for the Postgres password, the Redis password and the N8N_ENCRYPTION_KEY, all masked to placeholders, then locking the file to mode 600 so only its owner can read the secrets
Do not skip this The N8N_ENCRYPTION_KEY must be the same value it was on your single-container install, and the same across the main process and both workers. n8n encrypts every stored credential with it. Change it, or let one worker start without it, and those credentials become unreadable. Copy the existing key across rather than generating a fresh one on a stack that already holds credentials.

Least privilege applies here too. The Postgres user is scoped to the one n8n database, not a superuser; the Redis password keeps the queue private; and the whole .env is mode 600 so a second account on the box cannot read your secrets. None of these values appear in the Compose file, the process list, or any log line.

Survive a restart

This is the payoff, and the reason queue mode alone is not production: a real deployment has to come back after it stops. Two things make that happen. The restart: unless-stopped policy on every service means Docker brings each container back after a reboot or a crash, on its own. And the named volumes, db_data for Postgres and redis_data for Redis, mean the data outlives the containers. Confirm the restart policy is in force across the stack.

sudo docker inspect -f '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' \
     $(sudo docker compose ps -q)

Now the real test. Note how many executions the database holds, then take the entire stack down, containers and network gone, and bring it straight back up.

sudo docker compose down
sudo docker compose up -d --scale n8n-worker=2
Taking the whole stack down with docker compose down, which removes every container and the network but leaves the named volumes, then bringing it straight back up with two workers, the restart the stack has to survive

docker compose down stops and removes every container and the network, but it leaves the named volumes alone, so nothing durable is lost. When the stack comes back, the same encryption key from .env loads into the main process and both workers, the migrations are already applied, and Postgres still holds every row. Check that the executions from before the restart are still there.

sudo docker compose exec db \
     psql -U n8n -d n8n -c \
     "select count(*) from execution_entity;"
Counting the rows in execution_entity after the restart and getting the same number as before it, proof the past executions survived the stack going down and coming back because the data lives in a named volume and the encryption key stayed the same
The n8n Executions list at n8n.boxlab.space after the stack was taken down and brought back, showing the scheduled Site uptime monitor runs from before and after the restart all listed as Succeeded, proof the executions and the database survived
Checkpoint After the restart you should see the same execution count as before, the "Site uptime monitor" workflow still Active and adding new runs, and any stored credential still readable in the editor. If credentials read as broken after a restart, the encryption key changed between runs: pin the exact N8N_ENCRYPTION_KEY in .env and recreate the stack.

The editor tells the same story. Open the Executions list and the runs from before the restart are still listed alongside the new ones, each one green, because the database and the encryption key both survived the stack going down and coming back.

Operate it: pruning, updates and backups

A production stack needs a little upkeep, and queue mode does not change much of it. The first habit is pruning. Execution history grows every minute a scheduled workflow runs, and left alone it will fill the database. The EXECUTIONS_DATA_PRUNE and EXECUTIONS_DATA_MAX_AGE values in the shared environment tell n8n to delete executions older than a window, 336 hours here, which is fourteen days. Confirm the setting reached the container.

sudo docker compose exec n8n \
     printenv EXECUTIONS_DATA_PRUNE EXECUTIONS_DATA_MAX_AGE
Confirming the pruning window reached the container with printenv showing EXECUTIONS_DATA_PRUNE true and a fourteen-day max age, then docker compose pull fetching a newer image so an update is a pull and a recreate that leaves the volumes untouched

Updating is a pull and a recreate, and it applies to every n8n container at once because they share one image. Compose fetches the newer image, replaces the containers whose image changed, and leaves the volumes untouched, so your workflows and credentials ride through.

sudo docker compose pull
sudo docker compose up -d --scale n8n-worker=2

Backups are the same two things they were on the single container, because queue mode adds no new source of truth: the Postgres database holds your workflows and executions, and the N8N_ENCRYPTION_KEY decrypts the credentials inside it. Redis only carries jobs in flight, so it does not need backing up. Dump the database and keep the key beside it, then copy both off the slice.

sudo docker compose exec -T db pg_dump -U n8n n8n \
     | gzip > n8n-db-$(date +%F).sql.gz
sudo grep N8N_ENCRYPTION_KEY /opt/n8n-prod/.env > n8n-key.txt

That is a production n8n: a main process serving the editor, a Redis queue in the middle, a pool of workers running the executions, secrets in a locked .env, and a stack that comes back whole after it stops. From here the next step is securing it, tightening authentication, the firewall and how you expose webhooks, which the secure n8n work builds directly on top of the stack you now have.

Frequently asked questions

When do I actually need queue mode to run n8n in production? The moment executions start competing with each other or with the editor. A single container runs the editor, the triggers and every execution in one process, so a heavy workflow or a burst of webhooks makes the editor lag and other runs wait. Queue mode moves executions onto a Redis queue that workers pull from, so the editor stays responsive and you add throughput by adding a worker. If your n8n runs a handful of light workflows a day, one container is genuinely fine; switch when runs get heavy, frequent, or business-critical.
How many workers should I run, and how do I add more? Start with two and watch. Each worker runs several executions at once, so two workers already give you parallelism and a spare if one crashes. You add capacity with the same command, raising the number: docker compose up -d --scale n8n-worker=3 runs three. Size it by watching worker CPU and how long jobs wait in the queue rather than by guessing; add a worker when jobs queue up, and give the slice enough RAM, since every worker is a full node process.
Does Redis hold anything I need to back up? No. In n8n's queue mode Redis is the transport for jobs in flight, not the system of record. Your workflows, credentials and execution history all live in Postgres, and the encryption key in .env decrypts the credentials. So the two things to back up are the Postgres database and the encryption key, exactly as on a single container. If Redis is wiped it comes back empty and the queue simply refills; the appendonly file in the Redis volume only smooths a restart, it is not a backup you depend on.
My credentials read as broken after a restart. What happened? The encryption key changed between runs. n8n encrypts every stored credential with N8N_ENCRYPTION_KEY, and if the main process or a worker starts with a different key, or a fresh random one, it cannot decrypt what the old key wrote. Pin the exact key in .env so every container reads the same value, and make sure it matches the key your single-container install used. Restore the database with the wrong key and the workflows return but the credentials do not, which is why the key travels with every backup.

Running n8n in production means owning the whole platform: the editor, the queue, the workers, the data and the secrets, with no per-execution bill and no black box. To put it into service, take a ServerCake slice, a clean Ubuntu 24.04 box hosted in India and billed in rupees with GST, and run the queue-mode stack above on hardware that answers only to you.


Give your workflows room to scale

Run production n8n on a slice

Queue mode and a worker pool only pay off on a box with room to grow. Spin up a ServerCake slice, a clean Ubuntu 24.04 box in India billed in rupees with GST, and run n8n in queue mode with Redis and dedicated workers exactly as in this tutorial, from the Compose stack to a stack that survives a restart.

Get early access

This guide is provided for educational purposes and offered as is, without warranty of any kind. The commands change the configuration of a server you control, and some can lock you out if run out of order. Test on a non-production machine, keep a second session open, and take a snapshot or backup before you begin. You are responsible for changes made to your own systems; ServerCake accepts no liability for any loss, downtime, lockout, or damage arising from following this guide or running the accompanying script.

Spotted something out of date or have a question?

Built by the ServerCake team

Cloud that speaks India.

ServerCake is India's KYC-verified cloud: VMs and managed databases priced in ₹, billed with GST, on India-resident infrastructure. Reserve your spot for early access.

Reserve your spot