---
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](data:image/png;base64,[encoded data, 206676 characters])

# 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):**

![Gremlin with Badge](data:image/png;base64,[encoded data, 196868 characters])

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

![Gremlin Badge](data:image/png;base64,[encoded data, 206676 characters])

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.
