Automating workflows has always been a priority for me, especially for repetitive and error-prone manual processes. Recently, integrating AI capabilities into these automations offers a great opportunity for those, like me, who seek practical solutions. However, this integration often requires coding or complex API integrations. This is where “low-code/no-code” tools like n8n come into play. I used n8n to set up AI-powered agent flows on my own servers, without compromising data privacy and control. In this post, I will share my experience and explain how to do it step-by-step.
Why n8n and Self-Hosted AI Automation?
A few years ago, I experimented with different automation tools, especially for simple data transfers and notification flows. But when AI capabilities became involved, I either found them too expensive or avoided cloud-based solutions due to data security concerns. Especially in a client project where we needed to set up an automation handling sensitive financial data, a self-hosted solution became inevitable. n8n offers a wide range of integrations and, thanks to Docker support, I can easily host it on my own server.
For me, self-hosting is not just about cost advantage; it also means having complete control over my data. Especially when using AI agents, the question of how much of the data you send to LLMs is logged or used for training is always a concern. With an n8n setup under my control, I minimized these worries. Moreover, n8n’s flexible structure gives me the freedom to add as many custom integrations or LLM providers as I want. This is a significant advantage for someone like me who enjoys testing different LLMs.
Setup Steps: Quick Start with Docker Compose
The easiest way to run n8n on your own server is by using Docker Compose. I usually write the docker-compose.yml file myself for simple setups. This gives me flexibility and makes debugging easier in case of potential issues. The example below will be sufficient for a basic n8n setup. Here, I’m using PostgreSQL as the database because n8n working with SQLite can lead to issues like WAL bloat, especially under high load or unexpected shutdowns. I also added Redis for caching and queueing, which improves performance.
First, create a directory on your server and place the docker-compose.yml file inside it:
mkdir n8n-ai-automation
cd n8n-ai-automation
nano docker-compose.yml
The docker-compose.yml content might look like this:
version: '3.8'
services:
n8n:
image: n8nio/n8n
restart: always
ports:
- "5678:5678"
environment:
- N8N_HOST=${N8N_HOST:-localhost}
- N8N_PORT=5678
- N8N_PROTOCOL=${N8N_PROTOCOL:-http}
- WEBHOOK_URL=${WEBHOOK_URL:-http://localhost:5678/}
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
- DB_POSTGRESDB_USER=${POSTGRES_USER:-n8n}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD:-n8n}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=${N8N_USER:-admin}
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD:-admin_password_change_me}
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- N8N_METRICS_ENABLED=true # For monitoring metrics with tools like Prometheus
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
- redis
# Resource limits are important to protect against OOM killer
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 1G
postgres:
image: postgres:15
restart: always
environment:
- POSTGRES_DB=${POSTGRES_DB:-n8n}
- POSTGRES_USER=${POSTGRES_USER:-n8n}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-n8n}
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
redis:
image: redis:7
restart: always
volumes:
- redis_data:/data
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
volumes:
n8n_data:
postgres_data:
redis_data:
This file defines the n8n, PostgreSQL, and Redis services. I configure n8n’s database connection information and basic authentication settings using environment variables. Don’t forget to use N8N_BASIC_AUTH_ACTIVE and the user/password variables for security. Additionally, I specified the amount of memory to be allocated to each service with limits and reservations under deploy.resources. This is critical, especially in VPS environments, to prevent excessive resource consumption and OOM-killed errors. Last month, I saw a build container for one of my side products get OOM-killed due to a sleep 360 command in its CI/CD pipeline, so defining resource limits from the start has become a habit.
After saving the file, you can start the services with the docker compose up -d command:
docker compose up -d
Within a few minutes, you can access the n8n interface at http://localhost:5678. If you are using a reverse proxy (like Nginx), you will need to adjust the N8N_HOST and N8N_PROTOCOL variables accordingly. I usually set up SSL termination with Nginx and publish n8n under an address like https://automations.mysite.com.
Creating Your First AI Agent Flow
n8n’s power comes from its ability to create complex workflows through a visual interface without writing code. We can set up our first AI agent flow using a scenario where an incoming email is summarized and a notification is sent to Slack. This was a simple but effective method I used in a client project to quickly triage support emails.
-
Trigger: To start the workflow, we can use an “Email” trigger. I usually set up a “Webhook” trigger and route emails from another service (e.g., Mailgun or a simple email parser I wrote) to this webhook. However, n8n also has its own “IMAP Email” or “Gmail” nodes. For simplicity, let’s start with a “Manual Trigger” and then connect it to a real trigger later.
-
LLM Node: Under the “AI” category, you will find many LLM (Large Language Model) nodes. I usually use “OpenAI” or “Generic LLM” nodes. The “Generic LLM” node provides access to different LLMs via OpenRouter or your own custom APIs. Drag and drop this node onto the canvas.
-
Prompt Engineering: Inside the LLM node, we will enter a
System PromptandUser Promptto instruct it to summarize the email. Prompt engineering is critical here. While using AI for production planning in a manufacturing company’s ERP, I repeatedly tested how detailed and guiding the prompts needed to be.System Promptexample:You are an AI assistant that summarizes incoming customer support emails. Focus on the main issue, the customer's name (if available), and any urgent requests. Keep the summary concise, maximum 3 sentences.User Promptexample:Summarize the following email: --- Subject: About the Defective Product From: Ayşe Yılmaz <[email protected]> Date: 2026-06-15 Body: Hello, the X brand product I purchased with order number 12345 turned out to be defective. I have been using it for a week, and it suddenly stopped working yesterday. I urgently request a replacement. Please contact me as soon as possible. Thank you, Ayşe Yılmaz. ---You can dynamically link the content of the incoming email to this example using expressions like
{{ $json.body.text }}. -
Slack Node: Add a “Slack” node to send the summary from the LLM to a Slack channel. When configuring the Slack node, you will need to provide a Webhook URL or Bot Token. You can link the output from the LLM node to the message part using
{{ $node["LLM Node"].json["text"] }}.
After creating this flow, you can check if it works by clicking the “Test Workflow” button or by triggering a test email. As you can see, we have set up an AI-powered automation with a few drag-and-drop operations, without writing any code.
Real-World Agent Flows: RAG and Multi-LLM Integration
In more complex scenarios, our AI agents need access not only to general knowledge but also to our specific datasets. This is where RAG (Retrieval-Augmented Generation) comes in. In a client project, while building a knowledge base for a bank’s internal platform, I used a RAG architecture to ensure LLMs had access to accurate and up-to-date information. We can do this with n8n without writing any code.
RAG Integration
The basic idea for RAG is to retrieve the most relevant information from our own data sources before asking the LLM a question, and then adding this information to the prompt.
-
Data Source: Your internal documents (PDFs, text files, database records) should be embedded in a vector database (e.g., Pinecone, Weaviate, Qdrant, or even PostgreSQL’s
pg_vectorextension). n8n has direct integration nodes for these vector databases. On my end, I usually prefer PostgreSQL withpg_vectorbecause using a technology I already have eliminates additional costs. -
Generating Embeddings: Before uploading your documents to the vector database, you need to create their “embeddings” (numerical vector representations). You can use nodes like “OpenAI Embeddings” or “Cohere Embeddings” for this.
-
Vector Search: When a question comes in (e.g., from a Webhook trigger), you search for the most relevant document chunks related to the question using the appropriate vector database node from the “Vector Store” category.
-
Enriching the Prompt: You enrich the prompt by adding the information from the search results (usually text snippets) to the LLM node’s
User Prompt. For example:Here is some relevant information from our internal knowledge base: --- {{ $node["Vector Search"].json["results"] }} --- Based on the information above, answer the following question: {{ $json.query }}
This way, the LLM can generate more accurate and contextual answers by being fed not only general information but also your corporate knowledge.
Multi-LLM Provider Integration and Fallback
Relying on a single LLM provider can be risky in terms of both cost and performance. Different providers offer different models, and the costs, speeds, and performances of these models vary. With n8n, I can integrate multiple LLM providers and set up fallback mechanisms to switch to another provider based on the situation or if one provider fails. I typically use providers like Groq, Gemini Flash, and OpenRouter together. Groq stands out for its speed, Gemini Flash might be more affordable, and OpenRouter offers various models through a single API.
Example of a multi-LLM flow:
- Main LLM Attempt: First, I use my primary preferred LLM provider (e.g., Groq).
- Error Checking: If an error occurs from the Groq node (e.g., API timeout or rate limit), I check this condition with an “IF” node.
- Fallback LLM: In case of an error, I switch to another LLM node (e.g., Gemini Flash via OpenRouter) from the “true” branch of the “IF” node.
- Result Merging: I can then merge the results from both LLMs with a “Merge” node and present them as a single output to the rest of the flow.
This architecture is vital for ensuring continuity, especially in critical workflows. I use this fallback mechanism in the AI-based predictions for financial calculators in one of my side products. This way, if there’s an issue with one provider, the user experience isn’t interrupted.
Operational Details: Security, Monitoring, and Maintenance
Setting up a self-hosted system brings with it operational responsibilities. I take some basic precautions to ensure n8n runs smoothly and securely.
Security Measures
-
Nginx Reverse Proxy and SSL: Instead of exposing n8n directly to the internet, I place an Nginx reverse proxy in front of it. With Nginx, I perform SSL termination (free with Let’s Encrypt) and add basic security layers. For example, I use
rate limitingto create the first line of defense against DDoS attacks. I also set HTTP security headers likeX-Content-Type-OptionsandX-Frame-Options.Example Nginx configuration:
server { listen 80; server_name automations.mysite.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name automations.mysite.com; ssl_certificate /etc/letsencrypt/live/automations.mysite.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/automations.mysite.com/privkey.pem; # Security headers add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; add_header X-XSS-Protection "1; mode=block"; location / { proxy_pass http://localhost:5678; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Rate limiting example # limit_req zone=one burst=5 nodelay; } } -
Fail2ban: I use
fail2banto prevent brute-force attacks on my server via SSH. I can also track failed login attempts to n8n’s web interface from logs and block them withfail2ban. For this, I centralize n8n logs withjournaldand write a custom jail rule forfail2ban.Example
fail2banjail rule (for n8n):[n8n-auth] enabled = true port = http,https filter = n8n-auth logpath = /var/log/syslog # Or where logs from journald are stored maxretry = 5 bantime = 3600And
filter.d/n8n-auth.conffile:[Definition] failregex = .*n8n.*authentication failed for user.*<HOST>.* ignoreregex =This rule detects failed login attempts by searching for the “authentication failed for user” pattern in n8n logs and blocks the IP address for one hour.
-
Kernel Module Blacklist: To protect against security vulnerabilities, I blacklist kernel modules I don’t use. Taking such precautions is especially important when modules like
algif_aeadhave old CVEs (like CVE-2026-31431). This improves overall system security.
Monitoring and Maintenance
- Log Management: I centralize all Docker container logs with
journald. This is crucial for troubleshooting and security auditing. In case of an error, I monitor live logs withjournalctl -u docker.service -for check past logs withjournalctl -u docker.service --since "1 hour ago". - Resource Monitoring: I keep memory and CPU usage under control by specifying
cgrouplimits in the Docker Compose file. Additionally, I monitor n8n’s own metrics (enabled withN8N_METRICS_ENABLED=true) and the server’s overall resource usage with tools like Prometheus and Grafana. I particularly track database and cache server metrics to detect issues likePostgreSQL WAL bloatorRedis OOM eviction policyproactively. - Backup: I take regular backups for the PostgreSQL database. I automate this process with the
pg_dumpcommand or a backup tool. n8n workflows can also be exported as a JSON file, which helps in recovery scenarios.
Conclusion and Next Steps
Setting up my own AI automations self-hosted with n8n has provided me with both flexibility and cost advantages. The ability to create complex agent flows without writing code, use my own data with RAG, and integrate different LLM providers is a huge plus. I use these types of AI automations in many different scenarios, such as production planning in a manufacturing ERP, prioritizing customer support emails on an e-commerce site, or making financial predictions in my own side product.
This setup was a project that brought together my experience in many areas, not just AI integration, but also system administration, network security (Nginx, fail2ban), and database optimization (PostgreSQL). Although I initially encountered insidious issues like MTU/MSS mismatches or DNS negative caching, I debugged and resolved them step-by-step.
If you also want to build your own AI automations but don’t want to write code and data control is a priority for you, a self-hosted n8n setup is definitely worth trying. As a next step, I’m exploring how to use “agent patterns” (planning, tool use, memory) more effectively within n8n to make these AI agents even smarter. Perhaps I’ll cover this topic in my next post.