self-hosted

Self-Host n8n on Your Own Server with Docker

Step-by-step guide to self-hosting n8n on your own Ubuntu server using Docker Compose. Beginner-friendly, with copy-paste commands throughout.

Shashikant Dwivedi Shashikant Dwivedi
8 min read
Self-Host n8n on Your Own Server with Docker

Step-by-step guide to self-hosting n8n on a Linux server using Docker Compose — including the sandboxed AI Assistant stack, SearXNG web search, and task runners. Beginner-friendly, with copy-paste commands.

Hey there, if you've been using n8n on the cloud and thinking about moving to a self-hosted setup, you're in the right place. n8n is an open-source workflow automation tool — think of it as a self-hostable alternative to Zapier or Make, except you own the server, the data, and the workflow history.

🏳️
If you just want the commands and code, check the code section in bottom.

What makes this guide slightly different from a "just run one container" tutorial is that we're setting up the full stack — the one n8n's own one-line installer ships when you run curl -fsSL https://get.n8n.io | sh. That means, besides the n8n editor itself, you also get:

  • Task runners — isolated processes that execute your Code nodes instead of running them inside the main n8n process.
  • A sandbox service (sandbox-api + sandbox-runner) — a Docker-in-Docker sandbox that powers n8n's built-in AI Assistant when it needs to run code on your behalf.
  • SearXNG — a self-hosted, privacy-respecting metasearch engine that gives the AI Assistant web search capability, without sending your queries to a third party.

This is part of my Self Hosting series, where I document how I run my own infrastructure instead of renting it by the month.

Security note

A few of the containers in this stack (the sandbox runner especially) run in privileged mode because they need to spin up Docker-in-Docker for code execution. Never publish their ports to the internet. The only port that should ever be internet-facing here is n8n's own 5678, and even that should sit behind a reverse proxy with HTTPS in production, not exposed raw.

Prerequisite

You just need a Linux server (I'm using Ubuntu) with SSH access and a public IP. A VPS with 2 vCPUs and 4 GB RAM is comfortable for this — the sandbox and runner containers add a bit of overhead compared to running n8n alone.

Steps

If you just want the commands with no explanation, scroll down to the "Show me the code" section at the bottom.

Step 1 — Log in to Your Server

ssh your-user@your-server-ip

Replace your-user and your-server-ip with your actual SSH username and server address.

Step 2 — Update the System and Install Docker

Keep this habit for every server you spin up — update first, install second, avoids half of the weird package conflicts later.

sudo apt-get update && sudo apt-get upgrade -y

I installed Docker via snap here, since it's a single command and I didn't want to fuss with the official repo on this box:

sudo snap install docker

If you'd rather use Docker's official APT repository (recommended for anything long-running or production-facing), follow Docker's own install guide instead — both give you docker and docker compose.

Step 3 — Create the Project Directory

mkdir n8n && cd n8n

Everything for this stack — the compose file, the .env, and SearXNG's config — lives inside this one folder.

Step 4 — Configure SearXNG

SearXNG is what gives the AI Assistant its "search the web" ability without routing your queries through someone else's API. It needs a tiny settings file before it'll start.

nano searxng-settings.yml

Paste this in:

use_default_settings: true
search:
  formats:
    - html
    - json

The json format is the important bit here — that's what n8n's sandbox service calls into when the AI Assistant needs search results back as structured data instead of an HTML page.

Step 5 — Write the Docker Compose File

This is the core of the setup. Six services work together here:

Service What it does
sandbox-certs Bootstraps short-lived mTLS certificates so the sandbox API and runner can talk to each other securely, then exits
sandbox-api The control plane for code execution requests coming from n8n's AI Assistant
sandbox-runner-1 The actual Docker-in-Docker worker that executes sandboxed code — runs privileged
n8n The n8n editor and workflow engine itself — the only service exposed on 5678
runners n8n's task runner — executes your workflow's Code nodes outside the main process
searxng Self-hosted search backend for the AI Assistant

None of the internal services (sandbox-api, sandbox-runner-1, runners, searxng) publish ports to the host — they only talk to each other over the default Compose network, by service name. That's intentional and you shouldn't change it on an internet-facing box.

nano compose.yml
volumes:
  n8n-data:
  sandbox-tls:

services:
  sandbox-certs:
    image: ghcr.io/n8n-io/n8n-sandbox-service-api:1.2.0
    user: '0:0'
    entrypoint: ['sh', '-c']
    command:
      - >
        bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api
        --control-san-prefix sandbox-runner --world-readable &&
        chown -R sandbox-api:sandbox-api /tls/api && chmod -R a+rX /tls
    environment:
      NUM_RUNNERS: '1'
    volumes:
      - sandbox-tls:/tls

  sandbox-api:
    image: ghcr.io/n8n-io/n8n-sandbox-service-api:1.2.0
    depends_on:
      sandbox-certs:
        condition: service_completed_successfully
    env_file: .env
    environment:
      SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt
      SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key
      SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key
      SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1
    volumes:
      - sandbox-tls:/tls:ro
    healthcheck:
      test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/healthz']
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    # Never publish 8080/9090 to the host on an internet-facing server.
    # n8n reaches this container by service name, over the default Compose network.

  sandbox-runner-1:
    image: ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:1.2.0
    privileged: true
    depends_on:
      sandbox-api:
        condition: service_healthy
    env_file: .env
    environment:
      SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090
      SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080
      SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ':9091'
      SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091
      SANDBOX_RUNNER_ID: runner-1
      SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest
      SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt
      SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt
      SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key
      SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api
      SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt
      SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key
      SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt
    volumes:
      - sandbox-tls:/tls:ro
    # Never expose this container's ports publicly — it runs privileged Docker-in-Docker.

  n8n:
    image: docker.io/n8nio/n8n:${N8N_VERSION}
    depends_on:
      sandbox-api:
        condition: service_healthy
    ports:
      - '5678:5678' # the only port that should be internet-facing
    env_file: .env
    volumes:
      - n8n-data:/home/node/.n8n

  runners:
    image: ghcr.io/n8n-io/runners:${N8N_VERSION}
    depends_on:
      - n8n
    environment:
      N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN}
      N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679
      # Idle runners exit and are relaunched on demand (per the task-runners docs)
      N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: '15'
    # Runs user code from Code nodes. Never publish this container's ports.

  searxng:
    image: ghcr.io/searxng/searxng:latest
    environment:
      SEARXNG_SECRET: ${SEARXNG_SECRET}
    volumes:
      - ./searxng-settings.yml:/etc/searxng/settings.yml:ro
    # Internal-only: n8n reaches it by service name. Never publish its port.

Save and exit (Ctrl+O, Enter, Ctrl+X if you're in nano, same as always).

Step 6 — Generate Secrets and Environment Variables

Every one of these services needs its own key or token to authenticate with the others, and n8n also needs to know which of its own version to pull. Rather than typing 32-character random strings by hand, let openssl do it:

umask 077; api=$(openssl rand -hex 32); printf 'SANDBOX_API_KEYS=%s\nSANDBOX_API_RUNNER_REGISTRATION_TOKEN=%s\nSANDBOX_API_RUNNER_API_KEY=%s\nN8N_INSTANCE_AI_SANDBOX_API_KEY=%s\nSEARXNG_SECRET=%s\nN8N_INSTANCE_AI_SEARXNG_URL=http://searxng:8080\nN8N_RUNNERS_AUTH_TOKEN=%s\nN8N_VERSION=2.38.1\n' "$api" "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" "$api" "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" >> .env

A quick word on what's happening here, since it looks intimidating in one line:

  • umask 077 makes sure the .env file is only readable by you the moment it's created — reasonable, given it's about to hold six secrets.
  • api=$(openssl rand -hex 32) generates one shared key that's deliberately reused for both SANDBOX_API_KEYS and N8N_INSTANCE_AI_SANDBOX_API_KEY, since n8n's AI Assistant authenticates to the sandbox API using that same key.
  • Every other $(openssl rand -hex 32) generates its own independent 32-byte hex secret.
  • N8N_VERSION=2.38.1 pins the n8n and task-runner image versions so a stray docker compose pull doesn't silently bump you to a new major version. Check the n8n releases page for the current version before you run this and swap it in if you want something newer.

If you're running behind plain HTTP for now (no reverse proxy with a TLS certificate in front yet), also add:

printf 'N8N_SECURE_COOKIE=false\n' >> .env

n8n sets secure cookies by default, which browsers will refuse to send back over plain HTTP. Once you put n8n behind Nginx with a real SSL certificate — I've written about setting up Nginx as a reverse proxy and getting free SSL with Certbot if you haven't done that before — remove this line and let n8n use secure cookies again.

Step 7 — Launch the Stack

sudo docker compose up -d

This pulls six images and starts them in dependency order — sandbox-certs finishes and exits first, then sandbox-api waits for it, sandbox-runner-1 waits for sandbox-api to report healthy, and n8n waits for the same thing. Give it a minute or two on the first run.

Check that everything actually came up:

sudo docker compose ps

You want to see sandbox-api and n8n marked healthy, sandbox-certs as Exited (0) (that one's supposed to finish and stop), and the rest as running.

Step 8 — Verify the Sandbox Is Healthy

docker compose exec n8n wget -qO- http://sandbox-api:8080/healthz

If this returns a healthy response, n8n can reach the sandbox service internally, which means the AI Assistant's code-execution and web-search features are wired up correctly.

Step 9 — Open n8n and Create Your Owner Account

Point your browser at:

http://your-server-ip:5678

n8n hands you an owner-account setup screen on first load — email, name, and a password — and from there you're on the main dashboard. If you've already put Nginx and a domain in front of it, use your domain instead of the raw IP and port.

And that's it — you're running n8n with its own AI Assistant, sandboxed code execution, and self-hosted web search, all on infrastructure you control end to end.


Show me the code

# Log in
ssh your-user@your-server-ip

# Update system and install Docker
sudo apt-get update && sudo apt-get upgrade -y
sudo snap install docker

# Project directory
mkdir n8n && cd n8n

# SearXNG config
cat > searxng-settings.yml << 'EOF'
use_default_settings: true
search:
  formats:
    - html
    - json
EOF

# Create compose.yml (paste the full file from Step 5 above)
nano compose.yml

# Generate secrets and env vars
umask 077; api=$(openssl rand -hex 32); printf 'SANDBOX_API_KEYS=%s\nSANDBOX_API_RUNNER_REGISTRATION_TOKEN=%s\nSANDBOX_API_RUNNER_API_KEY=%s\nN8N_INSTANCE_AI_SANDBOX_API_KEY=%s\nSEARXNG_SECRET=%s\nN8N_INSTANCE_AI_SEARXNG_URL=http://searxng:8080\nN8N_RUNNERS_AUTH_TOKEN=%s\nN8N_VERSION=2.38.1\n' "$api" "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" "$api" "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" >> .env

# Only if you're not behind HTTPS yet
printf 'N8N_SECURE_COOKIE=false\n' >> .env

# Launch
sudo docker compose up -d
sudo docker compose ps

# Sanity check
docker compose exec n8n wget -qO- http://sandbox-api:8080/healthz

A couple of things worth doing next, if you're taking this to production: put Nginx in front of port 5678 with a real certificate instead of hitting the raw IP, and consider swapping n8n's default SQLite database for Postgres if you expect meaningful workflow volume — SQLite is fine to start, but it'll show its limits under concurrent executions.

I hope this saves you the trial-and-error I went through getting all six containers to actually talk to each other. If you get stuck on any step, drop a comment and I'll try to help.

References

Tagged self-hosted docker n8n automation ubuntu devops

Tagged self-hosted
Shashikant Dwivedi

Written by Shashikant Dwivedi

Engineer, occasional writer, full-time noticer. Based in Prayagraj, India. New essays land roughly twice a month.