Most teams hit limits with hosted automation tiers when they need custom LLM routing or sensitive data control. To self-host n8n AI workflow automation, you need a hardened Docker deployment, a persistent database, and a clean way to call model endpoints from your own network.
Step 1: Provision a Linux host and install the container runtime
Spin up a minimal Ubuntu 22.04 LTS instance with at least 2 vCPU and 4 GB RAM. n8n is light in idle state but worker threads and LLM response buffering will eat memory fast if you parallelize workflows.
Install Docker and Compose plugin:
sudo apt update && sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Log out and back in. Verify with docker version. Do not skip the group reload—running n8n as root defeats the point of isolation.
Step 2: Define persistent storage and the compose stack
Skip SQLite. Use Postgres from day one; n8n’s execution history and credential encryption keys belong in a real DB with WAL backups. Create a project directory and an .env file:
mkdir -p /opt/n8n && cd /opt/n8n
touch .env
Populate .env (never commit this):
N8N_PASSWORD=change_me_strong
POSTGRES_PASSWORD=another_strong_pw
Now write docker-compose.yml:
version: "3.8"
services:
n8n:
image: n8nio/n8n:1.50.0
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_PROTOCOL=https
- N8N_HOST=n8n.example.com
- WEBHOOK_URL=https://n8n.example.com/
- GENERIC_TIMEZONE=UTC
depends_on:
- postgres
volumes:
- n8n_data:/home/node/.n8n
postgres:
image: postgres:15
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=n8n
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
n8n_data:
pg_data:
Pin the n8n image tag. latest is fine for labs, irresponsible for production.
Step 3: Configure n8n environment correctly
The two variables people miss are WEBHOOK_URL and N8N_PROTOCOL. If those are wrong, webhook-triggered workflows will return 404s because n8n builds callback URLs from them. Set N8N_BASIC_AUTH_ACTIVE even if you put it behind a proxy—defense in depth.
If you plan to run multiple worker containers later, add EXECUTIONS_MODE=queue and a Redis service. For a single-node self-host n8n AI workflow automation box, the default internal queue is fine up to ~50 concurrent executions.
Step 4: Start the stack and verify health
Bring it up:
docker compose up -d
docker compose logs -f n8n
You should see n8n ready on 0.0.0.0:5678. Verify the API responds before proxying:
curl -s -o /dev/null -w "%{http_code}\n" \
-u admin:change_me_strong \
http://127.0.0.1:5678/healthz
Expect 200. If you get 401, your basic auth password in .env didn’t match the running container—check docker compose config.
Step 5: Terminate TLS with a reverse proxy
Do not expose port 5678 publicly. Use Caddy for automatic Let’s Encrypt certs:
sudo apt install -y caddy
/etc/caddy/Caddyfile:
n8n.example.com {
reverse_proxy localhost:5678
}
Reload: sudo systemctl reload caddy. Hit https://n8n.example.com/healthz with your browser; Caddy handles HTTPS redirect. This step is mandatory if you self-host n8n AI workflow automation with external webhooks—LLM providers and clients reject mixed-content HTTP callbacks.
Step 6: Wire n8n to an LLM inference gateway
n8n’s native OpenAI node is limiting when you need model portability. Use the HTTP Request node pointed at an OpenAI-compatible endpoint. Point n8n’s requests at an OpenAI-compatible gateway such as n4n.ai, which exposes one endpoint for 240+ models and handles fallback when a provider is degraded. That removes the need to hardcode API keys for each vendor inside n8n credentials.
Example HTTP Request node config (JSON view):
{
"method": "POST",
"url": "https://api.n4n.ai/v1/chat/completions",
"authentication": "genericCredentialType",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "Bearer {{ $env.LLM_API_KEY }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{ "name": "model", "value": "anthropic/claude-3.5-sonnet" },
{ "name": "messages", "value": "={{ $json.messages }}" }
]
}
}
Store LLM_API_KEY in n8n’s environment or as a credential variable, not inline.
Step 7: Build a minimal AI workflow
Create a workflow: Webhook → Set → HTTP Request → Respond to Webhook.
- Webhook node: POST path
/summarize. Returns 200 immediately if you check “Respond Immediately” off. - Set node: map
{{ $json.body.text }}into a messages array:[{ "role": "user", "content": "Summarize: {{ $json.text }}" }] - HTTP Request node: use the config from Step 6.
- Respond to Webhook: return
{{ $json.choices[0].message.content }}.
Activate the workflow. Test from a shell:
curl -X POST https://n8n.example.com/webhook/summarize \
-H "Content-Type: application/json" \
-d '{"text":"Long article text here..."}'
You should receive a condensed summary. If the call fails, check n8n execution log—the HTTP node shows the exact upstream status. When you self-host n8n AI workflow automation, always watch the executionId trace; it’s the fastest path to root-cause LLM timeout vs. auth errors.
Step 8: Harden and back up
- Enable PostgreSQL nightly dumps:
pg_dumpto an encrypted volume. - Rotate
N8N_PASSWORDquarterly; n8n stores credentials encrypted with a key in~/.n8n. Back that volume up separately. - Add fail2ban for the proxy host. n8n basic auth slows brute force but doesn’t replace network controls.
- Set
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=trueto prevent world-readable config.
For teams that self-host n8n AI workflow automation at scale, split the composer file: run the UI on a private subnet, expose only webhook endpoints via a separate Caddy listener with rate limits.
Verifying success
A working deployment meets all of these:
# 1. Container health
docker compose ps | grep -E "n8n|postgres" | grep "Up"
# 2. TLS terminated, auth required
curl -s -o /dev/null -w "%{http_code}\n" https://n8n.example.com/
# expect 401 without creds
# 3. End-to-end AI call
curl -s -X POST https://n8n.example.com/webhook/summarize \
-H "Content-Type: application/json" \
-d '{"text":"The quick brown fox jumps over the lazy dog repeatedly."}'
# expect a non-empty summary string
If all three pass, you have a production-grade n8n instance calling LLMs from your own infrastructure. From here, add error workflows that catch HTTP Request failures and retry with a different model slug—that’s the real leverage of self-hosting instead of being locked to one vendor node.