This article contains an affiliate link to Hostinger. Going through it gets you a discount and earns this site a commission. The price is the same for you, and nothing in this guide depends on that provider.

In short

  • What it fixes: the default n8n install runs on SQLite in a single process, with no Python and no ffmpeg. Fine for trying it out, not for holding up.
  • What goes in instead: Postgres, Redis, queue mode, one worker, and a custom image that can read a PDF or drive a browser.
  • Time: half an hour hardening the server, ten minutes deploying.
  • Cost: from roughly 7 to 10 euros a month for an 8 GB VPS.
  • The trap almost nobody shows: your firewall does not block the ports Docker publishes.

See Hostinger VPS plans →

Why the default install does not hold

The official n8n install command gives you a working service in thirty seconds. That is its strength and its trap, because two defaults turn against you the moment usage gets serious.

The first is the database. n8n starts on SQLite, a file sitting in a volume. SQLite is excellent, but it serialises writes. When several executions try to write their log at the same moment, they wait. You see no error, you see executions dragging.

The second is the execution mode. By default n8n runs workflows in the process that receives the request. A slow webhook therefore blocks the ones behind it. On a test server you never notice. The day a client triggers a three minute job, everything arriving after it queues up.

And what the official image does not ship

The official image contains no Python, no ffmpeg, no PDF library and no browser. The Code node loads no external module. In practice, the moment a workflow has to pull the text out of an invoice, convert an audio file, or read a page that renders nothing without JavaScript, you either leave the container or build your own.

It is that second option that makes self hosting genuinely worth it. A custom image adds Python and its libraries, ffmpeg, poppler for PDFs, and a driven Chromium. From there, the Code node does what a script on your own machine would do.

The architecture that holds

request ──► n8n (main) accepts, answers straight away
│
├──► Redis the job queue
│ │
│ └──► n8n-worker executes, 6 in parallel
│
└──► Postgres workflows, credentials, executions

The main instance computes nothing any more. It takes the request and hands back control immediately. The job goes into a Redis queue and a worker picks it up. A slow webhook blocks nobody, and a load spike is handled by adding a worker instead of renting a bigger server.

Postgres replaces SQLite for the same reason you do not run accounting out of a shared spreadsheet: several processes write at once.

The server, before n8n

A well configured n8n on an open server is still an open server. The order matters, and every step gets tested before the next one.

  • A user that is not root, with the SSH key copied over, and the login tested in a second terminal before anything else.
  • SSH hardening, passwords off, root by key only.
  • The firewall, 22, 80 and 443, nothing else.
  • fail2ban, banning an address after three failures.
  • Unattended upgrades and 4 GB of swap, without which the image build gets killed halfway through.
  • The SSH file that does nothing

    On Ubuntu 24.04, /etc/ssh/sshd_config opens with an Include line, and in OpenSSH the first value obtained wins. Cloud-init often drops a file into sshd_config.d that re-enables passwords. You then edit the main file, restart the service, and nothing changes. Without a single error.

    The only check that counts is on the effective configuration, not on a file:

    sshd -T | grep -E « ^(passwordauthentication|permitrootlogin) »

    The firewall does not protect what Docker publishes

    This is the most important point in this article, and the one most guides leave out.

    UFW filters the INPUT chain, meaning traffic destined for the machine. Docker publishes its ports by writing into FORWARD through NAT, and those rules are evaluated first. The result: a container publishing a port is reachable from the internet while ufw status shows a spotless table.

    The demonstration takes a minute:

    docker run -d –name test-expose -p 9999:80 nginx:alpine
    ufw status | grep 9999 # no rule allows that port

    From another machine, curl http://SERVER_IP:9999 serves the nginx page.

    The fix goes through the DOCKER-USER chain, which is evaluated before Docker’s own publishing rules. You let return traffic and ports 80 and 443 through, and drop the rest. The exact commands are in the public repository linked below.

    Second safety net, and it is worth as much as the first: your provider’s firewall. It filters before the machine, so Docker cannot bypass it, because it does not run there.

    The three variables that break everything

    N8N_ENCRYPTION_KEYLeft empty, n8n generates one and writes it into the volume. The day the volume is recreated, every connection has to be redone by hand.
    The same key on main and workerThe most common mistake in queue mode. The worker starts, fails to decrypt, and never says why.
    WEBHOOK_URLWrong or missing, n8n builds localhost addresses. No error at startup: you find out when a third party service never calls back.

    Back the encryption key up somewhere other than the server. A database restored without it hands you unreadable credentials.

    Self hosted or managed: how to decide

    Monthly costthe price of the VPS, roughly 7 to 10 € for 8 GBusage based billing, which rises with volume
    Executionsbounded by the machine, not by a quotametered
    Arbitrary codeyes, Python and Node modules includedrestricted
    Dataon your server, in the country you choosewith the vendor
    What it really costshalf an hour of setup, then keeping up with securitynothing, that is the point

    Self hosting wins as soon as volume goes up or a workflow has to touch a file. The managed offering stays the right call if nobody on your side wants to read a system log at 11pm.

    What machine you need

    Plan on 8 GB of memory and 2 vCPU for the full stack: main, worker, Postgres and Redis. It runs on 4 GB, but the image build installs Chromium and that is exactly the moment memory runs out. Swap is not a luxury, it is what stops the process being killed halfway.

    Any provider works: the commands are the same everywhere, apt, ufw and sshd have no idea where they are running.

    Compare VPS plans →

    The code

    The compose file, the Dockerfile and every hardening command are public, in English and in French:

    Frequently asked questions

    How much does a self hosted n8n cost per month?

    The price of the VPS and nothing else. An 8 GB server sits between 7 and 10 euros a month. Add the domain name, around 10 euros a year, and the certificate is free.

    Do you need to know Linux administration?

    You need to be able to copy commands and read what they return. The real prerequisite is not technical, it is regularity: an exposed server means applying security updates.

    What happens if I lose the encryption key?

    Every stored credential becomes unreadable, and each integration has to be reconnected by hand. It is irreversible. Back it up off the server, from the first start.

    Is one worker enough?

    One worker at six concurrent executions covers individual use or a small team comfortably. You add another when the Redis queue stops returning to zero between peaks.

    Is opening the Code node wide dangerous?

    Yes, if several people have access to the editor. Allowing every Node module gives anyone who writes a workflow the ability to read the container file system. On a single user instance it is a convenience. With several people, list the allowed modules explicitly.

    Can you migrate from a managed offering to your own server?

    Workflows export to JSON and import back. Credentials do not: they are encrypted on the vendor side and get reconnected one by one. That is the long part of the migration, plan for it.

    KR
    Koati R., Fondateur de TheStackPicks

    Ingénieur full-stack & IA. Je relève chaque tarif à la source et je date le relevé.