383 lines
608 KiB
Markdown
383 lines
608 KiB
Markdown
---
|
|
title: Gremlin — Build & Configuration
|
|
description: Complete build and configuration reference for the Gremlin AI stack on NetGrimoire
|
|
published: true
|
|
date: 2026-04-02T12:22:30.000Z
|
|
tags: gremlin, ai, docker, swarm, n8n, ollama
|
|
editor: markdown
|
|
dateCreated: 2026-04-02T12:22:30.000Z
|
|
---
|
|
|
|

|
|
|
|
# Gremlin — Build & Configuration
|
|
|
|
Gremlin is the self-hosted AI agent for NetGrimoire. This document covers the complete build, deployment, and configuration of the Gremlin stack as of the initial deployment checkpoint.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Gremlin runs entirely on local hardware via Ollama — no cloud APIs, no external dependencies. It provides an AI chat interface, automated infrastructure auditing, and alert triage through a four-service Docker Swarm stack.
|
|
|
|
| Component | Role |
|
|
|-----------|------|
|
|
| Ollama | Local LLM inference engine |
|
|
| Open WebUI | Chat interface and RAG pipeline |
|
|
| Qdrant | Vector database for document embeddings |
|
|
| n8n | Workflow automation and agent orchestration |
|
|
|
|
**Host:** docker4
|
|
**Network:** netgrimoire (external overlay)
|
|
**Swarm manager:** znas
|
|
**Repo path:** `services/swarm/stack/Gremlin/`
|
|
|
|
---
|
|
|
|
## Branding Assets
|
|
|
|
The Gremlin mascot — whiskey glass, cigar, mischievous grin — is the official NetGrimoire Gremlin character. Two versions:
|
|
|
|
**Full character (scene):**
|
|
|
|

|
|
|
|
**Badge / logo (green portal frame):**
|
|
|
|

|
|
|
|
Store these at `assets/gremlin/` in the Netgrimoire docs repo for use in future Wiki.js pages and artifacts.
|
|
|
|
---
|
|
|
|
## Repository Structure
|
|
|
|
```
|
|
services/swarm/stack/Gremlin/
|
|
├── .env # Environment variables (secrets)
|
|
├── deploy.sh # Deploy script — sources .env, runs docker stack config + deploy
|
|
└── gremlin-stack.yml # Swarm stack definition
|
|
```
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
All secrets and configuration live in `.env`. Fill in all values before deploying.
|
|
|
|
```bash
|
|
# Open WebUI
|
|
WEBUI_SECRET_KEY= # Random hex string — generate with: openssl rand -hex 32
|
|
|
|
# n8n
|
|
N8N_USER=admin
|
|
N8N_PASSWORD= # Strong password
|
|
|
|
# ntfy (self-hosted)
|
|
NTFY_URL=https://ntfy.netgrimoire.com
|
|
|
|
# Forgejo — read token (scope: repository:read)
|
|
FORGEJO_URL=https://git.netgrimoire.com
|
|
FORGEJO_TOKEN=
|
|
|
|
# Forgejo — docs repo write token (scope: repository:read, write, contents)
|
|
FORGEJO_DOCS_OWNER=traveler
|
|
FORGEJO_DOCS_REPO=Netgrimoire
|
|
FORGEJO_WRITE_TOKEN=
|
|
|
|
# Ollama models
|
|
OLLAMA_MODEL_GENERAL=llama3.2:3b
|
|
OLLAMA_MODEL_CODE=qwen2.5-coder:7b
|
|
```
|
|
|
|
---
|
|
|
|
## Stack File
|
|
|
|
```yaml
|
|
services:
|
|
ollama:
|
|
image: ollama/ollama:latest
|
|
ports:
|
|
- "11434:11434"
|
|
volumes:
|
|
- /DockerVol/ollama:/root/.ollama
|
|
environment:
|
|
- OLLAMA_ORIGINS=*
|
|
networks:
|
|
- netgrimoire
|
|
deploy:
|
|
labels:
|
|
- homepage.group=Gremlin
|
|
- homepage.name=Ollama
|
|
- homepage.icon=ollama.png
|
|
- homepage.href=http://ollama.netgrimoire.com:11434
|
|
- homepage.description=Local LLM Runtime
|
|
- kuma.ollama.http.name=Ollama API
|
|
- kuma.ollama.http.url=http://ollama:11434/api/tags
|
|
placement:
|
|
constraints:
|
|
- node.hostname == docker4
|
|
|
|
open-webui:
|
|
image: ghcr.io/open-webui/open-webui:main
|
|
ports:
|
|
- "3000:8080"
|
|
volumes:
|
|
- /DockerVol/open-webui:/app/backend/data
|
|
environment:
|
|
- OLLAMA_BASE_URL=http://ollama:11434
|
|
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
|
|
- ENABLE_RAG_WEB_SEARCH=true
|
|
- ENABLE_OLLAMA_API=true
|
|
- QDRANT_HOST=qdrant
|
|
- QDRANT_PORT=6333
|
|
networks:
|
|
- netgrimoire
|
|
deploy:
|
|
labels:
|
|
- homepage.group=Gremlin
|
|
- homepage.name=Open WebUI
|
|
- homepage.icon=openwebui.png
|
|
- homepage.href=https://ai.netgrimoire.com
|
|
- homepage.description=Gremlin Chat Interface
|
|
- kuma.openwebui.http.name=Open WebUI
|
|
- kuma.openwebui.http.url=http://open-webui:8080
|
|
- caddy=ai.netgrimoire.com
|
|
- caddy.reverse_proxy=open-webui:8080
|
|
- caddy_ingress_network=netgrimoire
|
|
placement:
|
|
constraints:
|
|
- node.hostname == docker4
|
|
|
|
qdrant:
|
|
image: qdrant/qdrant:latest
|
|
ports:
|
|
- "6333:6333"
|
|
- "6334:6334"
|
|
volumes:
|
|
- /DockerVol/qdrant:/qdrant/storage
|
|
environment:
|
|
- QDRANT__SERVICE__GRPC_PORT=6334
|
|
networks:
|
|
- netgrimoire
|
|
deploy:
|
|
labels:
|
|
- homepage.group=Gremlin
|
|
- homepage.name=Qdrant
|
|
- homepage.icon=qdrant.png
|
|
- homepage.href=http://qdrant.netgrimoire.com:6333/dashboard
|
|
- homepage.description=Vector Database
|
|
- kuma.qdrant.http.name=Qdrant
|
|
- kuma.qdrant.http.url=http://qdrant:6333
|
|
placement:
|
|
constraints:
|
|
- node.hostname == docker4
|
|
|
|
n8n:
|
|
image: n8nio/n8n:latest
|
|
ports:
|
|
- "5678:5678"
|
|
volumes:
|
|
- /DockerVol/n8n:/home/node/.n8n
|
|
- /DockerVol/n8n/workflows:/home/node/.n8n/workflows
|
|
environment:
|
|
- N8N_BASIC_AUTH_ACTIVE=true
|
|
- N8N_BASIC_AUTH_USER=${N8N_USER}
|
|
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
|
|
- WEBHOOK_URL=https://n8n.netgrimoire.com/
|
|
- GENERIC_TIMEZONE=America/Chicago
|
|
- N8N_EDITOR_BASE_URL=https://n8n.netgrimoire.com/
|
|
- OLLAMA_BASE_URL=http://ollama:11434
|
|
- NTFY_URL=${NTFY_URL}
|
|
- FORGEJO_URL=${FORGEJO_URL}
|
|
- FORGEJO_TOKEN=${FORGEJO_TOKEN}
|
|
- FORGEJO_DOCS_OWNER=${FORGEJO_DOCS_OWNER}
|
|
- FORGEJO_DOCS_REPO=${FORGEJO_DOCS_REPO}
|
|
- FORGEJO_WRITE_TOKEN=${FORGEJO_WRITE_TOKEN}
|
|
- N8N_BLOCK_ENV_ACCESS_IN_NODE=false
|
|
networks:
|
|
- netgrimoire
|
|
deploy:
|
|
labels:
|
|
- homepage.group=Gremlin
|
|
- homepage.name=n8n
|
|
- homepage.icon=n8n.png
|
|
- homepage.href=https://n8n.netgrimoire.com
|
|
- homepage.description=Workflow Automation
|
|
- kuma.n8n.http.name=n8n
|
|
- kuma.n8n.http.url=http://n8n:5678
|
|
- caddy=n8n.netgrimoire.com
|
|
- caddy.reverse_proxy=n8n:5678
|
|
- caddy_ingress_network=netgrimoire
|
|
placement:
|
|
constraints:
|
|
- node.hostname == docker4
|
|
|
|
networks:
|
|
netgrimoire:
|
|
external: true
|
|
```
|
|
|
|
---
|
|
|
|
## Deploy Script
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
STACK_NAME="gremlin"
|
|
COMPOSE_FILE="gremlin-stack.yml"
|
|
ENV_FILE=".env"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
echo "Loading environment..."
|
|
set -a && source "$ENV_FILE" && set +a
|
|
|
|
echo "Preprocessing stack file..."
|
|
docker stack config --compose-file "$COMPOSE_FILE" > resolved.yml
|
|
|
|
echo "Deploying $STACK_NAME..."
|
|
docker stack deploy --compose-file resolved.yml "$STACK_NAME"
|
|
rm -f resolved.yml
|
|
|
|
echo "Services:"
|
|
sleep 3
|
|
docker stack services "$STACK_NAME"
|
|
```
|
|
|
|
---
|
|
|
|
## Volume Directories
|
|
|
|
Create these on docker4 before first deploy:
|
|
|
|
```bash
|
|
mkdir -p /DockerVol/ollama
|
|
mkdir -p /DockerVol/open-webui
|
|
mkdir -p /DockerVol/qdrant
|
|
mkdir -p /DockerVol/n8n/workflows
|
|
chown -R 1000:1000 /DockerVol/n8n
|
|
```
|
|
|
|
The `chown` on n8n is required — n8n runs as uid 1000 and will fail to start without write access to its data directory.
|
|
|
|
---
|
|
|
|
## Model Setup
|
|
|
|
Run on docker4 after first deploy:
|
|
|
|
```bash
|
|
docker exec $(docker ps -qf name=gremlin_ollama) ollama pull llama3.2:3b
|
|
docker exec $(docker ps -qf name=gremlin_ollama) ollama pull qwen2.5-coder:7b
|
|
```
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
docker exec $(docker ps -qf name=gremlin_ollama) ollama list
|
|
```
|
|
|
|
| Model | Size | Use |
|
|
|-------|------|-----|
|
|
| llama3.2:3b | ~2 GB | General Q&A, alert triage, summarization |
|
|
| qwen2.5-coder:7b | ~4.7 GB | Code analysis, compose audits, YAML generation |
|
|
| qwen2.5:14b | ~9 GB | Already present — deep reasoning tasks |
|
|
|
|
---
|
|
|
|
## Known Issues & Fixes
|
|
|
|
### n8n volume permission denied
|
|
**Symptom:** `EACCES: permission denied, open '/home/node/.n8n/config'`
|
|
**Fix:** `chown -R 1000:1000 /DockerVol/n8n` on docker4, then `docker service update --force gremlin_n8n` from znas.
|
|
|
|
### Caddy label quoting
|
|
**Symptom:** `caddy.reverse_proxy` value has escaped quotes — `"n8n:5678"` instead of `n8n:5678`
|
|
**Cause:** `docker stack config` preprocessing adds quotes around `{{upstreams N}}` template values.
|
|
**Fix:** Use literal upstream address in labels — `caddy.reverse_proxy=n8n:5678` — instead of the template helper.
|
|
|
|
### Caddy wrong IP for upstream
|
|
**Symptom:** Caddy resolves service to wrong IP, 502 errors.
|
|
**Fix:** Add `caddy_ingress_network=netgrimoire` label to any service with Caddy labels. This pins Caddy to the correct network interface.
|
|
|
|
### n8n env vars blocked in workflows
|
|
**Symptom:** `access to env vars denied` error in n8n workflow nodes.
|
|
**Fix:** Add `N8N_BLOCK_ENV_ACCESS_IN_NODE=false` to n8n environment in stack file.
|
|
|
|
### Forgejo API 422 on file create (Forgejo 11+)
|
|
**Symptom:** `[SHA]: Required` when trying to create new files via API.
|
|
**Cause:** Forgejo 11 changed the contents API — `PUT` is for updates only, `POST` is required for new files.
|
|
**Fix:** Use `POST` to create, `GET` first to check for existing SHA, then `PUT` with SHA to update.
|
|
|
|
---
|
|
|
|
## n8n Workflows
|
|
|
|
### Forgejo Repo Audit
|
|
**File:** `gremlin-forgejo-audit.json`
|
|
**Trigger:** Schedule — Monday 06:00
|
|
**Function:** Fetches Caddyfile once, walks `swarm/` and `swarm/stack/*/` in the services repo, audits each YAML file against NetGrimoire standards using `qwen2.5-coder:7b`. Swarm files checked for homepage/kuma/caddy/placement labels. Compose files checked against Caddyfile for matching entries. FAILs trigger ntfy notification to `gremlin-audits` and commit a full report to `Netgrimoire/Audits/` in the docs repo.
|
|
|
|
**Architecture:** All Forgejo and Ollama API calls are made inside Code nodes using `this.helpers.httpRequest()` — not HTTP Request nodes — to avoid n8n body expression limitations.
|
|
|
|
### Uptime Kuma Alert Triage
|
|
**File:** `gremlin-kuma-triage.json`
|
|
**Trigger:** Webhook — `https://n8n.netgrimoire.com/webhook/gremlin-kuma-alert`
|
|
**Function:** Receives Uptime Kuma DOWN events, sends to `llama3.2:3b` for triage analysis (likely cause, immediate checks, severity), fires urgent ntfy notification to `gremlin-alerts`. RECOVERED events send a simple plain notification without AI analysis.
|
|
|
|
---
|
|
|
|
## ntfy Topics
|
|
|
|
| Topic | URL | Events |
|
|
|-------|-----|--------|
|
|
| gremlin-alerts | https://ntfy.netgrimoire.com/gremlin-alerts | Service DOWN triage, RECOVERED notices |
|
|
| gremlin-audits | https://ntfy.netgrimoire.com/gremlin-audits | Weekly audit FAILs, doc generation notices |
|
|
|
|
---
|
|
|
|
## Service URLs
|
|
|
|
| Service | URL | Auth |
|
|
|---------|-----|------|
|
|
| Open WebUI | https://ai.netgrimoire.com | Local account |
|
|
| n8n | https://n8n.netgrimoire.com | Basic auth (see .env) |
|
|
| Ollama API | http://ollama.netgrimoire.com:11434 | None |
|
|
| Qdrant Dashboard | http://qdrant.netgrimoire.com:6333/dashboard | None |
|
|
|
|
---
|
|
|
|
## Useful Commands
|
|
|
|
```bash
|
|
# Check all services (run on znas)
|
|
docker stack services gremlin
|
|
|
|
# View logs for a service
|
|
docker service logs gremlin_n8n --tail 50
|
|
docker service logs gremlin_ollama --tail 50
|
|
docker service logs gremlin_open-webui --tail 50
|
|
|
|
# Force restart a service
|
|
docker service update --force gremlin_n8n
|
|
|
|
# List loaded models (run on docker4)
|
|
docker exec $(docker ps -qf name=gremlin_ollama) ollama list
|
|
|
|
# Pull a new model
|
|
docker exec $(docker ps -qf name=gremlin_ollama) ollama pull <model>
|
|
```
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- All services pinned to docker4 via placement constraints. Deploy commands run from znas as Swarm manager.
|
|
- Do not use `endpoint_mode: dnsrr` — it breaks internal DNS resolution between services. Default VIP mode is required.
|
|
- Wiki.js syncs from the `traveler/Netgrimoire` Forgejo repo. Gremlin commits audit reports to `Netgrimoire/Audits/` using POST for new files (Forgejo 11+ requirement).
|
|
- n8n workflow imports: delete old workflow first, import JSON, click Publish. Each reimport resets node configurations — Code node approach avoids HTTP Request node body limitations.
|