Replace Clay with n8n, Ollama, SearXNG and browserless; add Telegram assistant

Remove the Clay role (Claude Code web UI) and its Anthropic API key.

New roles:
- ollama: local CPU LLM (qwen3:4b-instruct), internal network only
- browserless: headless Chromium for JavaScript-heavy pages
- searxng: metasearch backend for the chat web_search tool
- n8n at agent.ladkau.de: web chat (/chat), scheduled workflows,
  Telegram bot; workflows and credentials are imported from the repo

Telegram bot: Gemini 3.8 -> Gemini 3.5 -> local Qwen fallback chain,
/qwen to force the local model, /stocks and a weekday schedule
(07:30, 13:00, 17:40) for the MTE.DE / PTX.F stock report.

Also: dashboard and check-services entries for the chat, vault checks
for the new secrets (n8n, SearXNG, Gemini, Telegram, SMTP), updated
CLAUDE.md, runbooks and README.
This commit is contained in:
ml
2026-10-03 17:10:33 +02:00
parent b3cf4e250a
commit 974486bc53
37 changed files with 2805 additions and 290 deletions
@@ -0,0 +1,9 @@
{
"id": "ollama0000000001",
"name": "Ollama (local)",
"type": "ollamaApi",
"data": {
"baseUrl": "http://ollama:11434",
"apiKey": ""
}
}
+222
View File
@@ -0,0 +1,222 @@
{
"id": "chat00000000001",
"name": "Chat",
"active": true,
"nodes": [
{
"id": "c1",
"name": "Chat",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.4,
"position": [
0,
0
],
"webhookId": "3f2b6c1e-8a4d-4e57-9b0a-6d1c2e7f5a10",
"parameters": {
"public": true,
"mode": "hostedChat",
"authentication": "none",
"initialMessages": "Hi! Ask me anything or paste a link and I will read the page.",
"options": {
"loadPreviousSession": "notSupported",
"responseMode": "streaming",
"title": "Local AI chat"
}
}
},
{
"id": "c2",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
300,
0
],
"parameters": {
"promptType": "auto",
"text": "={{ $json.chatInput }}",
"options": {
"systemMessage": "You are a concise assistant. Answer in the language of the user. For questions about current events, weather, news, prices or anything you cannot know for sure, call the web_search tool first with a short search query, then answer from the results and name the source URLs. When the user gives a web address, or a search result needs more detail, call the fetch_page tool with the full URL and base your answer only on what it returns. If a tool fails, say so. Do not invent facts or page content."
}
}
},
{
"id": "c3",
"name": "Ollama Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
200,
260
],
"parameters": {
"model": "__OLLAMA_MODEL__",
"options": {
"numCtx": 16384,
"temperature": 0.3,
"numThread": 4
}
},
"credentials": {
"ollamaApi": {
"id": "ollama0000000001",
"name": "Ollama (local)"
}
}
},
{
"id": "c4",
"name": "Window Buffer Memory",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.3,
"position": [
380,
260
],
"parameters": {
"sessionIdType": "fromInput",
"contextWindowLength": 3
}
},
{
"id": "c5",
"name": "fetch_page",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
560,
260
],
"parameters": {
"name": "fetch_page",
"description": "Opens a web page in a real browser (JavaScript is executed) and returns its title and readable text, truncated to about 3000 characters. Input: the full URL.",
"workflowId": {
"__rl": true,
"value": "fetchpage0000001",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"url": "={{ $fromAI('url', 'The full URL of the page to open, including https://', 'string') }}"
},
"matchingColumns": [],
"schema": [
{
"id": "url",
"displayName": "url",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
}
},
{
"id": "c6",
"name": "web_search",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
740,
260
],
"parameters": {
"name": "web_search",
"description": "Searches the web and returns the top results (title, URL, short snippet). Input: a short search query.",
"workflowId": {
"__rl": true,
"value": "websearch000001",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"query": "={{ $fromAI('query', 'A short web search query', 'string') }}"
},
"matchingColumns": [],
"schema": [
{
"id": "query",
"displayName": "query",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
}
}
],
"connections": {
"Chat": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Ollama Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Window Buffer Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"fetch_page": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"web_search": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,234 @@
{
"id": "digesttg00000001",
"name": "Weekly page digest to Telegram (example)",
"active": false,
"nodes": [
{
"id": "g1",
"name": "Every Monday 06:00",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 6 * * 1"
}
]
}
}
},
{
"id": "g2",
"name": "Sites",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
],
"parameters": {
"jsCode": "// One item per page to summarise. Edit url and prompt.\nreturn [\n { json: { url: 'https://example.com', prompt: 'Summarise this page in five bullet points.' } },\n];\n"
}
},
{
"id": "g3",
"name": "Fetch page text",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
480,
0
],
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"value": "fetchpage0000001",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"url": "={{ $json.url }}"
},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"waitForSubWorkflow": true
}
}
},
{
"id": "g4",
"name": "Summarise",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.7,
"position": [
720,
0
],
"parameters": {
"promptType": "define",
"text": "={{ $('Sites').item.json.prompt + '\\n\\n---\\n' + $json.response }}"
}
},
{
"id": "g5",
"name": "Ollama Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
720,
240
],
"parameters": {
"model": "__OLLAMA_MODEL__",
"options": {
"numCtx": 16384,
"temperature": 0.2,
"numThread": 4
}
},
"credentials": {
"ollamaApi": {
"id": "ollama0000000001",
"name": "Ollama (local)"
}
}
},
{
"id": "g6",
"name": "Format message",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1000,
0
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "f1",
"name": "message",
"type": "string",
"value": "={{ ('Digest: ' + $('Sites').item.json.url + '\\n\\n' + $json.text).slice(0, 4000) }}"
}
]
},
"options": {}
}
},
{
"id": "g7",
"name": "Post to Telegram chat",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1240,
0
],
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"value": "telegramchat0001",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"message": "={{ $json.message }}"
},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"waitForSubWorkflow": true
}
}
}
],
"connections": {
"Every Monday 06:00": {
"main": [
[
{
"node": "Sites",
"type": "main",
"index": 0
}
]
]
},
"Sites": {
"main": [
[
{
"node": "Fetch page text",
"type": "main",
"index": 0
}
]
]
},
"Fetch page text": {
"main": [
[
{
"node": "Summarise",
"type": "main",
"index": 0
}
]
]
},
"Ollama Chat Model": {
"ai_languageModel": [
[
{
"node": "Summarise",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Summarise": {
"main": [
[
{
"node": "Format message",
"type": "main",
"index": 0
}
]
]
},
"Format message": {
"main": [
[
{
"node": "Post to Telegram chat",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,193 @@
{
"id": "digest0000000001",
"name": "Weekly page digest (example)",
"active": false,
"nodes": [
{
"id": "d1",
"name": "Every Monday 06:00",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 6 * * 1"
}
]
}
}
},
{
"id": "d2",
"name": "Sites",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
],
"parameters": {
"jsCode": "// One item per page to summarise. Edit url and prompt.\nreturn [\n { json: { url: 'https://example.com', prompt: 'Summarise this page in five bullet points.', to: 'you@example.com' } },\n];\n"
}
},
{
"id": "d3",
"name": "Fetch page text",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
480,
0
],
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"value": "fetchpage0000001",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"url": "={{ $json.url }}"
},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"waitForSubWorkflow": true
}
}
},
{
"id": "d4",
"name": "Summarise",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.7,
"position": [
720,
0
],
"parameters": {
"promptType": "define",
"text": "={{ $('Sites').item.json.prompt + '\\n\\n---\\n' + $json.response }}"
}
},
{
"id": "d5",
"name": "Ollama Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
720,
240
],
"parameters": {
"model": "__OLLAMA_MODEL__",
"options": {
"numCtx": 16384,
"temperature": 0.2,
"numThread": 4
}
},
"credentials": {
"ollamaApi": {
"id": "ollama0000000001",
"name": "Ollama (local)"
}
}
},
{
"id": "d6",
"name": "Send mail",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1020,
0
],
"parameters": {
"fromEmail": "={{ $credentials.user }}",
"toEmail": "={{ $('Sites').item.json.to }}",
"subject": "={{ 'Digest: ' + $('Sites').item.json.url }}",
"emailFormat": "text",
"text": "={{ $json.text }}",
"options": {}
},
"credentials": {
"smtp": {
"id": "smtp00000000001",
"name": "Strato SMTP"
}
}
}
],
"connections": {
"Every Monday 06:00": {
"main": [
[
{
"node": "Sites",
"type": "main",
"index": 0
}
]
]
},
"Sites": {
"main": [
[
{
"node": "Fetch page text",
"type": "main",
"index": 0
}
]
]
},
"Fetch page text": {
"main": [
[
{
"node": "Summarise",
"type": "main",
"index": 0
}
]
]
},
"Ollama Chat Model": {
"ai_languageModel": [
[
{
"node": "Summarise",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Summarise": {
"main": [
[
{
"node": "Send mail",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,98 @@
{
"id": "fetchpage0000001",
"name": "Fetch page text",
"active": true,
"nodes": [
{
"id": "n1",
"name": "When called by another workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
0
],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "url",
"type": "string"
}
]
}
}
},
{
"id": "n2",
"name": "Render page in browserless",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
260,
0
],
"parameters": {
"method": "POST",
"url": "http://browserless:3000/function",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ code: \"export default async function ({ page, context }) {\\n await page.goto(context.url, { waitUntil: 'networkidle2', timeout: 40000 });\\n const title = await page.title();\\n const text = await page.evaluate(() => {\\n document.querySelectorAll('script,style,noscript,nav,footer,header,iframe,svg').forEach(e => e.remove());\\n return (document.body.innerText || '').replace(/\\\\n{3,}/g, '\\\\n\\\\n').trim();\\n });\\n return { data: { title, text: text.slice(0, context.maxChars || 3000) }, type: 'application/json' };\\n}\\n\", context: { url: $json.url, maxChars: 3000 } }) }}",
"options": {
"timeout": 90000
}
}
},
{
"id": "n3",
"name": "Format result",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
520,
0
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "a1",
"name": "response",
"type": "string",
"value": "={{ 'Title: ' + $json.data.title + '\\n\\n' + $json.data.text }}"
}
]
},
"options": {}
}
}
],
"connections": {
"When called by another workflow": {
"main": [
[
{
"node": "Render page in browserless",
"type": "main",
"index": 0
}
]
]
},
"Render page in browserless": {
"main": [
[
{
"node": "Format result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
{
"id": "websearch000001",
"name": "Web search",
"active": true,
"nodes": [
{
"id": "s1",
"name": "When called by another workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
0
],
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "query",
"type": "string"
}
]
}
}
},
{
"id": "s2",
"name": "Query SearXNG",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
260,
0
],
"parameters": {
"method": "GET",
"url": "http://searxng:8080/search",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $json.query }}"
},
{
"name": "format",
"value": "json"
}
]
},
"options": {
"timeout": 30000
}
}
},
{
"id": "s3",
"name": "Format results",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
520,
0
],
"parameters": {
"jsCode": "const results = ($input.first().json.results || []).slice(0, 6);\nif (!results.length) return [{ json: { response: 'No search results found.' } }];\nconst text = results.map((r, i) => (i + 1) + '. ' + r.title + '\\n ' + r.url + '\\n ' + (r.content || '').slice(0, 300)).join('\\n\\n');\nreturn [{ json: { response: text.slice(0, 3500) } }];\n"
}
}
],
"connections": {
"When called by another workflow": {
"main": [
[
{
"node": "Query SearXNG",
"type": "main",
"index": 0
}
]
]
},
"Query SearXNG": {
"main": [
[
{
"node": "Format results",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
+42
View File
@@ -0,0 +1,42 @@
---
- name: Restart n8n
community.docker.docker_compose_v2:
project_src: "{{ n8n_data_dir }}"
state: present
recreate: always
# "Import n8n content" is a topic, not a single handler: the tasks below all listen
# on it and run in the order they are defined (handlers cannot be blocks).
# Credentials first (workflows reference them by id), then workflows.
# Imported workflows start inactive; the ones marked "active": true are published.
# n8n only registers triggers/webhooks at startup, so restart afterwards.
- name: Wait for n8n to be ready (database migrations finished)
ansible.builtin.command: docker exec n8n wget -qO- http://localhost:5678/healthz/readiness
listen: Import n8n content
register: n8n_ready
changed_when: false
retries: 40
delay: 3
until: n8n_ready.rc == 0
- name: Import n8n credentials
ansible.builtin.command: docker exec n8n n8n import:credentials --separate --input=/import/credentials
listen: Import n8n content
no_log: true
changed_when: true
- name: Import n8n workflows
ansible.builtin.command: docker exec n8n n8n import:workflow --separate --input=/import/workflows
listen: Import n8n content
changed_when: true
- name: Publish active n8n workflows
ansible.builtin.command: docker exec n8n n8n publish:workflow --id={{ item }}
listen: Import n8n content
loop: "{{ n8n_publish_ids | from_json }}"
changed_when: true
- name: Restart n8n to register triggers
ansible.builtin.command: docker restart n8n
listen: Import n8n content
changed_when: true
+106
View File
@@ -0,0 +1,106 @@
---
- name: Create n8n directories
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.owner }}"
mode: "{{ item.mode }}"
loop:
- { path: "{{ n8n_data_dir }}", owner: root, mode: "0755" }
# n8n runs as uid 1000 (node) inside the container
- { path: "{{ n8n_data_dir }}/data", owner: "1000", mode: "0700" }
- { path: "{{ n8n_data_dir }}/import", owner: "1000", mode: "0700" }
- { path: "{{ n8n_data_dir }}/import/workflows", owner: "1000", mode: "0700" }
- { path: "{{ n8n_data_dir }}/import/credentials", owner: "1000", mode: "0700" }
tags: n8n
- name: Deploy Docker Compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ n8n_data_dir }}/docker-compose.yml"
owner: root
group: root
mode: "0644"
notify: Restart n8n
tags: n8n
# __TELEGRAM_USER_ID__ and __OLLAMA_MODEL__ in the workflow files are replaced by telegram_user_id and ollama_model
- name: Deploy workflows
ansible.builtin.copy:
content: "{{ lookup('ansible.builtin.file', item) | replace('__TELEGRAM_USER_ID__', telegram_user_id | string) | replace('__OLLAMA_MODEL__', ollama_model) }}"
dest: "{{ n8n_data_dir }}/import/workflows/{{ item | basename }}"
owner: "1000"
group: "1000"
mode: "0600"
with_fileglob:
- "workflows/*.json"
notify: Import n8n content
tags: n8n
- name: Deploy Ollama credential
ansible.builtin.copy:
src: credentials/ollama.json
dest: "{{ n8n_data_dir }}/import/credentials/ollama.json"
owner: "1000"
group: "1000"
mode: "0600"
notify: Import n8n content
tags: n8n
- name: Deploy SMTP credential
ansible.builtin.template:
src: smtp-credential.json.j2
dest: "{{ n8n_data_dir }}/import/credentials/smtp.json"
owner: "1000"
group: "1000"
mode: "0600"
no_log: true
notify: Import n8n content
tags: n8n
# Workflows with "active": true in their JSON are published after import
- name: Collect workflow ids to publish
ansible.builtin.set_fact:
n8n_publish_ids: >-
{%- set ns = namespace(ids=[]) -%}
{%- for f in query('fileglob', role_path ~ '/files/workflows/*.json') -%}
{%- set wf = lookup('file', f) | from_json -%}
{%- if wf.active | default(false) -%}{%- set ns.ids = ns.ids + [wf.id] -%}{%- endif -%}
{%- endfor -%}
{{ ns.ids | to_json }}
tags: n8n
- name: Deploy Telegram credential
ansible.builtin.template:
src: telegram-credential.json.j2
dest: "{{ n8n_data_dir }}/import/credentials/telegram.json"
owner: "1000"
group: "1000"
mode: "0600"
no_log: true
notify: Import n8n content
tags: n8n
- name: Deploy Gemini credential
ansible.builtin.template:
src: gemini-credential.json.j2
dest: "{{ n8n_data_dir }}/import/credentials/gemini.json"
owner: "1000"
group: "1000"
mode: "0600"
no_log: true
notify: Import n8n content
tags: n8n
- name: Start n8n
community.docker.docker_compose_v2:
project_src: "{{ n8n_data_dir }}"
state: present
pull: missing
tags: n8n
# Handlers run at the end of the play; flush so the import happens before the role finishes
- name: Apply pending handlers
ansible.builtin.meta: flush_handlers
tags: n8n
@@ -0,0 +1,77 @@
# Managed by Ansible — do not edit manually
services:
n8n:
image: n8nio/n8n:{{ n8n_version }}
container_name: n8n
restart: unless-stopped
environment:
N8N_HOST: "{{ domain_n8n }}"
N8N_PROTOCOL: "https"
N8N_PORT: "5678"
# Public URL used to build webhook / chat links behind Traefik
WEBHOOK_URL: "https://{{ domain_n8n }}/"
N8N_PROXY_HOPS: "1"
N8N_ENCRYPTION_KEY: "{{ n8n_encryption_key }}"
GENERIC_TIMEZONE: "{{ n8n_timezone }}"
TZ: "{{ n8n_timezone }}"
N8N_DIAGNOSTICS_ENABLED: "false"
N8N_VERSION_NOTIFICATIONS_ENABLED: "false"
N8N_TEMPLATES_ENABLED: "false"
# Keep the SQLite database small: drop execution history after 7 days
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "168"
volumes:
- {{ n8n_data_dir }}/data:/home/node/.n8n
# Workflows and credentials imported by Ansible (see roles/n8n/tasks/main.yml)
- {{ n8n_data_dir }}/import:/import:ro
mem_limit: {{ n8n_mem_limit }}
networks:
- traefik_public
- ollama_internal
- browserless_internal
- searxng_internal
labels:
- "traefik.enable=true"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
# Editor, webhooks and chat page — all behind basic auth
- "traefik.http.routers.n8n.rule=Host(`{{ domain_n8n }}`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
- "traefik.http.routers.n8n.service=n8n"
- "traefik.http.routers.n8n.middlewares=n8n-auth"
# Short link: /chat redirects to the hosted chat page of the chat workflow
- "traefik.http.routers.n8n-chat.rule=Host(`{{ domain_n8n }}`) && Path(`/chat`)"
- "traefik.http.routers.n8n-chat.priority=100"
- "traefik.http.routers.n8n-chat.entrypoints=websecure"
- "traefik.http.routers.n8n-chat.tls.certresolver=letsencrypt"
- "traefik.http.routers.n8n-chat.service=n8n"
- "traefik.http.routers.n8n-chat.middlewares=n8n-auth,n8n-chat-redirect"
- "traefik.http.middlewares.n8n-chat-redirect.redirectregex.regex=^https://{{ domain_n8n | replace('.', '\\\\.') }}/chat/?$$"
- "traefik.http.middlewares.n8n-chat-redirect.redirectregex.replacement=https://{{ domain_n8n }}/webhook/{{ n8n_chat_webhook_id }}/chat"
# Telegram bot webhook: called by Telegram's servers, so no basic auth. Restricted to Telegram's
# address ranges; n8n additionally checks Telegram's secret token header and the workflow
# only answers the user id in telegram_user_id.
- "traefik.http.routers.n8n-telegram.rule=Host(`{{ domain_n8n }}`) && Path(`/webhook/{{ n8n_telegram_webhook_id }}/webhook`) && Method(`POST`)"
- "traefik.http.routers.n8n-telegram.priority=200"
- "traefik.http.routers.n8n-telegram.entrypoints=websecure"
- "traefik.http.routers.n8n-telegram.tls.certresolver=letsencrypt"
- "traefik.http.routers.n8n-telegram.service=n8n"
{% if telegram_webhook_source_ranges | length > 0 %}
- "traefik.http.routers.n8n-telegram.middlewares=n8n-telegram-ips"
- "traefik.http.middlewares.n8n-telegram-ips.ipallowlist.sourcerange={{ telegram_webhook_source_ranges | join(',') }}"
{% endif %}
{% set ns = namespace(entries=[]) %}
{% for user in n8n_users %}
{% set ns.entries = ns.entries + [user.username + ':' + (user.password | password_hash('bcrypt', (user.username | hash('md5'))[:22]) | replace('$', '$$'))] %}
{% endfor %}
- "traefik.http.middlewares.n8n-auth.basicauth.users={{ ns.entries | join(',') }}"
networks:
traefik_public:
external: true
ollama_internal:
external: true
browserless_internal:
external: true
searxng_internal:
external: true
@@ -0,0 +1,9 @@
{
"id": "gemini00000001",
"name": "Google Gemini",
"type": "googlePalmApi",
"data": {
"host": "https://generativelanguage.googleapis.com",
"apiKey": {{ gemini_api_key | to_json }}
}
}
@@ -0,0 +1,14 @@
{
"id": "smtp00000000001",
"name": "Strato SMTP",
"type": "smtp",
"data": {
"user": {{ smtp_user | to_json }},
"password": {{ smtp_password | to_json }},
"host": {{ smtp_host | to_json }},
"port": {{ smtp_port }},
"secure": {{ smtp_secure | to_json }},
"disableStartTls": false,
"hostName": ""
}
}
@@ -0,0 +1,9 @@
{
"id": "telegram0000001",
"name": "Telegram bot",
"type": "telegramApi",
"data": {
"accessToken": {{ telegram_bot_token | to_json }},
"baseUrl": "https://api.telegram.org"
}
}