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
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.
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 inprivilegedmode 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 own5678, 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 077makes sure the.envfile 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 bothSANDBOX_API_KEYSandN8N_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.1pins the n8n and task-runner image versions so a straydocker compose pulldoesn'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
- n8n Docker installation docs
- n8n one-line installer, get.n8n.io
- n8n task runners — environment variables
- n8n GitHub repository
- SearXNG documentation
- Docker Engine install guide for Ubuntu
Tagged self-hosted docker n8n automation ubuntu devops