From 135225823003aec162814ae3fca1fef025723b7e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 17:20:08 +0000
Subject: [PATCH 1/4] Initial plan
From 935d0ab22bd69c5a96c455dd890cbfd024170526 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Mar 2026 17:32:28 +0000
Subject: [PATCH 2/4] Add container stack, nginx config, NAS templates, and
monitoring agent
Co-authored-by: DocwatZ <227472400+DocwatZ@users.noreply.github.com>
---
.env.example | 90 ++++++
agent/Dockerfile | 44 +++
agent/agent.mjs | 300 ++++++++++++++++++
agent/healthcheck.mjs | 14 +
agent/package.json | 11 +
docker-compose.yml | 201 ++++++++++++
.../reverse-proxy-examples/caddy.Caddyfile | 49 +++
.../nginx-proxy-manager.md | 87 +++++
homelab/reverse-proxy-examples/traefik.yml | 65 ++++
homelab/truenas/Chart.yaml | 25 ++
homelab/truenas/templates/deployment.yaml | 96 ++++++
homelab/truenas/templates/secrets.yaml | 12 +
homelab/truenas/templates/services.yaml | 63 ++++
homelab/truenas/values.yaml | 118 +++++++
homelab/unraid-template.xml | 46 +++
nginx/default.conf | 77 +++++
16 files changed, 1298 insertions(+)
create mode 100644 .env.example
create mode 100644 agent/Dockerfile
create mode 100644 agent/agent.mjs
create mode 100644 agent/healthcheck.mjs
create mode 100644 agent/package.json
create mode 100644 docker-compose.yml
create mode 100644 homelab/reverse-proxy-examples/caddy.Caddyfile
create mode 100644 homelab/reverse-proxy-examples/nginx-proxy-manager.md
create mode 100644 homelab/reverse-proxy-examples/traefik.yml
create mode 100644 homelab/truenas/Chart.yaml
create mode 100644 homelab/truenas/templates/deployment.yaml
create mode 100644 homelab/truenas/templates/secrets.yaml
create mode 100644 homelab/truenas/templates/services.yaml
create mode 100644 homelab/truenas/values.yaml
create mode 100644 homelab/unraid-template.xml
create mode 100644 nginx/default.conf
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..18d25d0
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,90 @@
+# =============================================================================
+# PROJECT N.O.M.A.D. — Homelab Edition
+# Network Operations Monitoring and Automation Dashboard
+# =============================================================================
+# Copy this file to .env and configure for your environment.
+# All values marked "replaceme" MUST be changed before starting.
+# =============================================================================
+
+# -----------------------------------------------------------------------------
+# Application Settings
+# -----------------------------------------------------------------------------
+NODE_ENV=production
+PORT=8080
+HOST=0.0.0.0
+LOG_LEVEL=info
+
+# Generate a random key: openssl rand -hex 32
+APP_KEY=replaceme
+
+# The external URL where Nomad will be accessed (used for links, redirects)
+# Examples: http://192.168.1.100:8080, https://nomad.home.local
+URL=http://localhost:8080
+
+# Optional: Base URL path if running behind a reverse proxy with a subpath
+# BASE_URL=/
+
+# -----------------------------------------------------------------------------
+# Database (MySQL 8.0)
+# -----------------------------------------------------------------------------
+DB_HOST=nomad-database
+DB_PORT=3306
+DB_DATABASE=nomad
+DB_USER=nomad
+DB_PASSWORD=replaceme
+DB_SSL=false
+
+# MySQL root password (used for initial database creation)
+MYSQL_ROOT_PASSWORD=replaceme
+
+# -----------------------------------------------------------------------------
+# Redis Cache / Queue
+# -----------------------------------------------------------------------------
+REDIS_HOST=nomad-cache
+REDIS_PORT=6379
+
+# Optional: Redis password (uncomment to enable)
+# REDIS_PASSWORD=
+
+# -----------------------------------------------------------------------------
+# Storage Paths (inside container — mapped via Docker volumes)
+# -----------------------------------------------------------------------------
+# Path where Nomad stores content (ZIM files, maps, uploads)
+NOMAD_STORAGE_PATH=/app/storage
+
+# -----------------------------------------------------------------------------
+# NAS / Host Storage Mapping
+# -----------------------------------------------------------------------------
+# Set these to your NAS storage paths on the HOST system.
+# These are used in docker-compose.yml volume mappings.
+#
+# Unraid example: /mnt/user/appdata/project-nomad
+# TrueNAS example: /mnt/pool/apps/project-nomad
+# Linux example: /opt/project-nomad
+NOMAD_DATA_DIR=/opt/project-nomad
+
+# -----------------------------------------------------------------------------
+# Reverse Proxy Settings
+# -----------------------------------------------------------------------------
+# Set to 'true' if running behind a TLS-terminating reverse proxy
+# TRUSTED_PROXY=false
+
+# Trusted proxy IP ranges (comma-separated CIDR)
+# TRUSTED_PROXY_IPS=172.16.0.0/12,192.168.0.0/16,10.0.0.0/8
+
+# -----------------------------------------------------------------------------
+# Monitoring Agent Settings (optional)
+# -----------------------------------------------------------------------------
+# AGENT_SECRET=replaceme
+# AGENT_PORT=9100
+# AGENT_COLLECT_INTERVAL=30
+
+# -----------------------------------------------------------------------------
+# Optional: External API URL for Nomad benchmark leaderboard
+# -----------------------------------------------------------------------------
+# NOMAD_API_URL=https://api.projectnomad.io
+
+# -----------------------------------------------------------------------------
+# Optional: Internet connectivity test URL
+# -----------------------------------------------------------------------------
+# INTERNET_STATUS_TEST_URL=https://connectivity-check.ubuntu.com
diff --git a/agent/Dockerfile b/agent/Dockerfile
new file mode 100644
index 0000000..26138eb
--- /dev/null
+++ b/agent/Dockerfile
@@ -0,0 +1,44 @@
+# =============================================================================
+# PROJECT N.O.M.A.D. — Homelab Edition
+# Lightweight Monitoring Agent
+# =============================================================================
+# A minimal monitoring agent that collects system metrics and reports
+# them to the Nomad server. Designed for low resource usage.
+#
+# Build: docker build -t nomad-agent ./agent
+# Run: docker run -d --name nomad-agent \
+# -e NOMAD_SERVER_URL=http://your-nomad-server:8080 \
+# -e AGENT_SECRET=your-secret \
+# -v /var/run/docker.sock:/var/run/docker.sock:ro \
+# -v /proc:/host/proc:ro \
+# -v /sys:/host/sys:ro \
+# nomad-agent
+# =============================================================================
+
+FROM node:22-alpine
+
+RUN apk add --no-cache curl
+
+WORKDIR /agent
+
+COPY package.json ./
+RUN npm ci --omit=dev 2>/dev/null || npm init -y
+
+COPY agent.mjs ./
+COPY healthcheck.mjs ./
+
+# Run as non-root
+RUN addgroup -g 1001 -S nomad && \
+ adduser -S nomad -u 1001 -G nomad
+USER nomad
+
+ENV NODE_ENV=production
+ENV AGENT_PORT=9100
+ENV COLLECT_INTERVAL=30
+
+EXPOSE 9100
+
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
+ CMD node healthcheck.mjs || exit 1
+
+CMD ["node", "agent.mjs"]
diff --git a/agent/agent.mjs b/agent/agent.mjs
new file mode 100644
index 0000000..a64f820
--- /dev/null
+++ b/agent/agent.mjs
@@ -0,0 +1,300 @@
+#!/usr/bin/env node
+// =============================================================================
+// PROJECT N.O.M.A.D. — Homelab Edition
+// Lightweight Monitoring Agent
+// =============================================================================
+// Collects system metrics and reports them to the Nomad server.
+// Designed for minimal CPU and RAM usage on homelab nodes.
+// =============================================================================
+
+import { createServer } from 'node:http'
+import { readFileSync, readdirSync, existsSync } from 'node:fs'
+import { hostname, cpus, totalmem, freemem, uptime, networkInterfaces, platform, arch, release } from 'node:os'
+
+// ---------------------------------------------------------------------------
+// Configuration
+// ---------------------------------------------------------------------------
+const config = {
+ port: parseInt(process.env.AGENT_PORT || '9100', 10),
+ collectInterval: parseInt(process.env.COLLECT_INTERVAL || '30', 10) * 1000,
+ serverUrl: process.env.NOMAD_SERVER_URL || '',
+ agentSecret: process.env.AGENT_SECRET || '',
+ nodeName: process.env.NODE_NAME || hostname(),
+ hostProcPath: process.env.HOST_PROC || '/host/proc',
+ hostSysPath: process.env.HOST_SYS || '/host/sys',
+}
+
+// ---------------------------------------------------------------------------
+// Metrics Collection
+// ---------------------------------------------------------------------------
+
+/** Collect CPU usage from /proc/stat or os module */
+function collectCpuMetrics() {
+ const cpuList = cpus()
+ const cpuCount = cpuList.length
+ let totalIdle = 0
+ let totalTick = 0
+
+ for (const cpu of cpuList) {
+ const { user, nice, sys, idle, irq } = cpu.times
+ totalTick += user + nice + sys + idle + irq
+ totalIdle += idle
+ }
+
+ return {
+ count: cpuCount,
+ model: cpuList[0]?.model || 'unknown',
+ usagePercent: cpuCount > 0 ? Math.round((1 - totalIdle / totalTick) * 100 * 100) / 100 : 0,
+ }
+}
+
+/** Collect memory metrics */
+function collectMemoryMetrics() {
+ const total = totalmem()
+ const free = freemem()
+ const used = total - free
+
+ return {
+ totalBytes: total,
+ usedBytes: used,
+ freeBytes: free,
+ usagePercent: Math.round((used / total) * 100 * 100) / 100,
+ }
+}
+
+/** Collect disk usage from /proc/mounts and /proc/diskstats */
+function collectDiskMetrics() {
+ const disks = []
+
+ try {
+ const mountsPath = `${config.hostProcPath}/mounts`
+ if (existsSync(mountsPath)) {
+ const mounts = readFileSync(mountsPath, 'utf-8')
+ const lines = mounts.split('\n').filter((l) => l.startsWith('/dev/'))
+
+ for (const line of lines) {
+ const parts = line.split(/\s+/)
+ if (parts.length >= 4) {
+ disks.push({
+ device: parts[0],
+ mountPoint: parts[1],
+ fsType: parts[2],
+ })
+ }
+ }
+ }
+ } catch {
+ // Disk metrics may not be available in all environments
+ }
+
+ return disks
+}
+
+/** Collect network interface information */
+function collectNetworkMetrics() {
+ const interfaces = networkInterfaces()
+ const nets = []
+
+ for (const [name, addrs] of Object.entries(interfaces)) {
+ if (!addrs) continue
+ for (const addr of addrs) {
+ if (addr.family === 'IPv4' && !addr.internal) {
+ nets.push({
+ interface: name,
+ address: addr.address,
+ mac: addr.mac,
+ })
+ }
+ }
+ }
+
+ return nets
+}
+
+/** Collect Docker container status via Docker socket */
+async function collectDockerMetrics() {
+ const containers = []
+
+ try {
+ const socketPath = '/var/run/docker.sock'
+ if (!existsSync(socketPath)) return containers
+
+ const response = await fetch('http://localhost/containers/json?all=true', {
+ socketPath,
+ }).catch(() => null)
+
+ if (response && response.ok) {
+ const data = await response.json()
+ for (const container of data) {
+ containers.push({
+ id: container.Id?.substring(0, 12),
+ name: container.Names?.[0]?.replace(/^\//, ''),
+ image: container.Image,
+ state: container.State,
+ status: container.Status,
+ })
+ }
+ }
+ } catch {
+ // Docker metrics may not be available
+ }
+
+ return containers
+}
+
+/** Collect all metrics */
+async function collectAllMetrics() {
+ const [dockerContainers] = await Promise.all([collectDockerMetrics()])
+
+ return {
+ timestamp: new Date().toISOString(),
+ node: {
+ name: config.nodeName,
+ platform: platform(),
+ arch: arch(),
+ release: release(),
+ uptime: Math.floor(uptime()),
+ },
+ cpu: collectCpuMetrics(),
+ memory: collectMemoryMetrics(),
+ disks: collectDiskMetrics(),
+ network: collectNetworkMetrics(),
+ docker: {
+ containers: dockerContainers,
+ containerCount: dockerContainers.length,
+ },
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Metrics Reporting
+// ---------------------------------------------------------------------------
+
+/** Send metrics to Nomad server */
+async function reportMetrics(metrics) {
+ if (!config.serverUrl) return
+
+ try {
+ const url = `${config.serverUrl}/api/agent/report`
+ await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${config.agentSecret}`,
+ },
+ body: JSON.stringify(metrics),
+ signal: AbortSignal.timeout(10000),
+ })
+ } catch {
+ // Silently retry on next interval
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Prometheus Metrics Endpoint
+// ---------------------------------------------------------------------------
+
+/** Format metrics as Prometheus exposition format */
+function formatPrometheusMetrics(metrics) {
+ const lines = []
+
+ // CPU metrics
+ lines.push('# HELP nomad_agent_cpu_usage_percent CPU usage percentage')
+ lines.push('# TYPE nomad_agent_cpu_usage_percent gauge')
+ lines.push(`nomad_agent_cpu_usage_percent{node="${metrics.node.name}"} ${metrics.cpu.usagePercent}`)
+
+ lines.push('# HELP nomad_agent_cpu_count Number of CPU cores')
+ lines.push('# TYPE nomad_agent_cpu_count gauge')
+ lines.push(`nomad_agent_cpu_count{node="${metrics.node.name}"} ${metrics.cpu.count}`)
+
+ // Memory metrics
+ lines.push('# HELP nomad_agent_memory_total_bytes Total memory in bytes')
+ lines.push('# TYPE nomad_agent_memory_total_bytes gauge')
+ lines.push(`nomad_agent_memory_total_bytes{node="${metrics.node.name}"} ${metrics.memory.totalBytes}`)
+
+ lines.push('# HELP nomad_agent_memory_used_bytes Used memory in bytes')
+ lines.push('# TYPE nomad_agent_memory_used_bytes gauge')
+ lines.push(`nomad_agent_memory_used_bytes{node="${metrics.node.name}"} ${metrics.memory.usedBytes}`)
+
+ lines.push('# HELP nomad_agent_memory_usage_percent Memory usage percentage')
+ lines.push('# TYPE nomad_agent_memory_usage_percent gauge')
+ lines.push(`nomad_agent_memory_usage_percent{node="${metrics.node.name}"} ${metrics.memory.usagePercent}`)
+
+ // Uptime
+ lines.push('# HELP nomad_agent_uptime_seconds System uptime in seconds')
+ lines.push('# TYPE nomad_agent_uptime_seconds gauge')
+ lines.push(`nomad_agent_uptime_seconds{node="${metrics.node.name}"} ${metrics.node.uptime}`)
+
+ // Docker containers
+ lines.push('# HELP nomad_agent_docker_containers Number of Docker containers')
+ lines.push('# TYPE nomad_agent_docker_containers gauge')
+ lines.push(`nomad_agent_docker_containers{node="${metrics.node.name}"} ${metrics.docker.containerCount}`)
+
+ for (const container of metrics.docker.containers) {
+ const running = container.state === 'running' ? 1 : 0
+ lines.push(`nomad_agent_docker_container_running{node="${metrics.node.name}",name="${container.name}",image="${container.image}"} ${running}`)
+ }
+
+ return lines.join('\n') + '\n'
+}
+
+// ---------------------------------------------------------------------------
+// HTTP Server
+// ---------------------------------------------------------------------------
+
+let latestMetrics = null
+
+const server = createServer(async (req, res) => {
+ if (req.url === '/health' || req.url === '/healthz') {
+ res.writeHead(200, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ status: 'healthy', node: config.nodeName }))
+ return
+ }
+
+ if (req.url === '/metrics') {
+ const metrics = latestMetrics || (await collectAllMetrics())
+ res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8' })
+ res.end(formatPrometheusMetrics(metrics))
+ return
+ }
+
+ if (req.url === '/api/metrics') {
+ const metrics = latestMetrics || (await collectAllMetrics())
+ res.writeHead(200, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify(metrics))
+ return
+ }
+
+ res.writeHead(404, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Not found' }))
+})
+
+// ---------------------------------------------------------------------------
+// Main Loop
+// ---------------------------------------------------------------------------
+
+async function collectAndReport() {
+ try {
+ latestMetrics = await collectAllMetrics()
+ await reportMetrics(latestMetrics)
+ } catch (err) {
+ console.error('Collection error:', err.message)
+ }
+}
+
+server.listen(config.port, '0.0.0.0', () => {
+ console.log(`[nomad-agent] Node: ${config.nodeName}`)
+ console.log(`[nomad-agent] Metrics server listening on port ${config.port}`)
+ console.log(`[nomad-agent] Collection interval: ${config.collectInterval / 1000}s`)
+ if (config.serverUrl) {
+ console.log(`[nomad-agent] Reporting to: ${config.serverUrl}`)
+ } else {
+ console.log('[nomad-agent] No NOMAD_SERVER_URL set — metrics available via /metrics endpoint only')
+ }
+
+ // Initial collection
+ collectAndReport()
+
+ // Periodic collection
+ setInterval(collectAndReport, config.collectInterval)
+})
diff --git a/agent/healthcheck.mjs b/agent/healthcheck.mjs
new file mode 100644
index 0000000..481a4d9
--- /dev/null
+++ b/agent/healthcheck.mjs
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+// Simple healthcheck for the Nomad agent
+
+const port = process.env.AGENT_PORT || 9100
+
+try {
+ const res = await fetch(`http://localhost:${port}/health`)
+ if (res.ok) {
+ process.exit(0)
+ }
+ process.exit(1)
+} catch {
+ process.exit(1)
+}
diff --git a/agent/package.json b/agent/package.json
new file mode 100644
index 0000000..9f88183
--- /dev/null
+++ b/agent/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "nomad-agent",
+ "version": "1.0.0",
+ "description": "Project N.O.M.A.D. lightweight monitoring agent",
+ "type": "module",
+ "main": "agent.mjs",
+ "scripts": {
+ "start": "node agent.mjs"
+ },
+ "license": "Apache-2.0"
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d25eff1
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,201 @@
+# =============================================================================
+# PROJECT N.O.M.A.D. — Homelab Edition
+# Network Operations Monitoring and Automation Dashboard
+# Docker Compose Stack for NAS and Homelab Environments
+# =============================================================================
+#
+# Usage:
+# 1. Copy .env.example to .env and configure
+# 2. docker compose up -d
+#
+# Compatible with: Unraid, TrueNAS SCALE, standard Docker hosts
+# =============================================================================
+
+name: project-nomad
+
+services:
+ # ---------------------------------------------------------------------------
+ # Nomad Application (Web UI + API)
+ # ---------------------------------------------------------------------------
+ nomad-app:
+ image: ghcr.io/crosstalk-solutions/project-nomad:latest
+ container_name: nomad-app
+ restart: unless-stopped
+ ports:
+ - "${PORT:-8080}:8080"
+ volumes:
+ - ${NOMAD_DATA_DIR:-./data}/storage:/app/storage
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ./install/entrypoint.sh:/usr/local/bin/entrypoint.sh:ro
+ environment:
+ - NODE_ENV=${NODE_ENV:-production}
+ - PORT=8080
+ - HOST=0.0.0.0
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - APP_KEY=${APP_KEY:?Set APP_KEY in .env}
+ - URL=${URL:-http://localhost:8080}
+ - DB_HOST=nomad-database
+ - DB_PORT=3306
+ - DB_DATABASE=${DB_DATABASE:-nomad}
+ - DB_USER=${DB_USER:-nomad}
+ - DB_PASSWORD=${DB_PASSWORD:?Set DB_PASSWORD in .env}
+ - DB_SSL=false
+ - REDIS_HOST=nomad-cache
+ - REDIS_PORT=6379
+ - NOMAD_STORAGE_PATH=/app/storage
+ - NOMAD_API_URL=${NOMAD_API_URL:-}
+ - INTERNET_STATUS_TEST_URL=${INTERNET_STATUS_TEST_URL:-}
+ entrypoint: ["/usr/local/bin/entrypoint.sh"]
+ depends_on:
+ nomad-database:
+ condition: service_healthy
+ nomad-cache:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 5
+ start_period: 60s
+ networks:
+ - nomad-internal
+ labels:
+ - "com.project-nomad.service=app"
+ - "com.project-nomad.description=Nomad Web Application"
+
+ # ---------------------------------------------------------------------------
+ # Nomad Worker (Background Job Processing)
+ # ---------------------------------------------------------------------------
+ nomad-worker:
+ image: ghcr.io/crosstalk-solutions/project-nomad:latest
+ container_name: nomad-worker
+ restart: unless-stopped
+ volumes:
+ - ${NOMAD_DATA_DIR:-./data}/storage:/app/storage
+ - /var/run/docker.sock:/var/run/docker.sock
+ environment:
+ - NODE_ENV=${NODE_ENV:-production}
+ - PORT=8081
+ - HOST=0.0.0.0
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - APP_KEY=${APP_KEY}
+ - URL=${URL:-http://localhost:8080}
+ - DB_HOST=nomad-database
+ - DB_PORT=3306
+ - DB_DATABASE=${DB_DATABASE:-nomad}
+ - DB_USER=${DB_USER:-nomad}
+ - DB_PASSWORD=${DB_PASSWORD}
+ - DB_SSL=false
+ - REDIS_HOST=nomad-cache
+ - REDIS_PORT=6379
+ - NOMAD_STORAGE_PATH=/app/storage
+ command: ["node", "ace", "queue:work", "--all"]
+ depends_on:
+ nomad-database:
+ condition: service_healthy
+ nomad-cache:
+ condition: service_healthy
+ networks:
+ - nomad-internal
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ labels:
+ - "com.project-nomad.service=worker"
+ - "com.project-nomad.description=Nomad Background Worker"
+
+ # ---------------------------------------------------------------------------
+ # Database (MySQL 8.0)
+ # ---------------------------------------------------------------------------
+ nomad-database:
+ image: mysql:8.0
+ container_name: nomad-database
+ restart: unless-stopped
+ environment:
+ - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}
+ - MYSQL_DATABASE=${DB_DATABASE:-nomad}
+ - MYSQL_USER=${DB_USER:-nomad}
+ - MYSQL_PASSWORD=${DB_PASSWORD}
+ volumes:
+ - nomad-db-data:/var/lib/mysql
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
+ networks:
+ - nomad-internal
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+ labels:
+ - "com.project-nomad.service=database"
+ - "com.project-nomad.description=Nomad MySQL Database"
+
+ # ---------------------------------------------------------------------------
+ # Cache / Queue (Redis 7)
+ # ---------------------------------------------------------------------------
+ nomad-cache:
+ image: redis:7-alpine
+ container_name: nomad-cache
+ restart: unless-stopped
+ command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
+ volumes:
+ - ${NOMAD_DATA_DIR:-./data}/redis:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 15s
+ timeout: 5s
+ retries: 5
+ networks:
+ - nomad-internal
+ deploy:
+ resources:
+ limits:
+ memory: 512M
+ labels:
+ - "com.project-nomad.service=cache"
+ - "com.project-nomad.description=Nomad Redis Cache"
+
+ # ---------------------------------------------------------------------------
+ # Nginx Reverse Proxy
+ # ---------------------------------------------------------------------------
+ nomad-nginx:
+ image: nginx:alpine
+ container_name: nomad-nginx
+ restart: unless-stopped
+ ports:
+ - "${NGINX_HTTP_PORT:-80}:80"
+ - "${NGINX_HTTPS_PORT:-443}:443"
+ volumes:
+ - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
+ - ${NOMAD_DATA_DIR:-./data}/logs/nginx:/var/log/nginx
+ depends_on:
+ nomad-app:
+ condition: service_healthy
+ networks:
+ - nomad-internal
+ labels:
+ - "com.project-nomad.service=nginx"
+ - "com.project-nomad.description=Nomad Reverse Proxy"
+
+# =============================================================================
+# Networks
+# =============================================================================
+networks:
+ nomad-internal:
+ driver: bridge
+ name: nomad-internal
+
+# =============================================================================
+# Volumes
+# =============================================================================
+# Database uses a named Docker volume for optimal I/O performance.
+# This avoids NFS/SMB latency issues common on NAS systems.
+# All other data uses bind mounts configured via NOMAD_DATA_DIR.
+volumes:
+ nomad-db-data:
+ driver: local
diff --git a/homelab/reverse-proxy-examples/caddy.Caddyfile b/homelab/reverse-proxy-examples/caddy.Caddyfile
new file mode 100644
index 0000000..81dbc6b
--- /dev/null
+++ b/homelab/reverse-proxy-examples/caddy.Caddyfile
@@ -0,0 +1,49 @@
+# =============================================================================
+# PROJECT N.O.M.A.D. — Homelab Edition
+# Caddy Reverse Proxy Configuration
+# =============================================================================
+#
+# Usage:
+# 1. Replace "nomad.home.local" with your actual domain/hostname
+# 2. Place this file in your Caddy configuration directory
+# 3. Reload Caddy: caddy reload
+#
+# Caddy automatically provisions TLS certificates via Let's Encrypt
+# when using a public domain. For local domains, it generates
+# self-signed certificates automatically.
+# =============================================================================
+
+nomad.home.local {
+ # Reverse proxy to Nomad application
+ reverse_proxy nomad-app:8080 {
+ # Health checks
+ health_uri /api/health
+ health_interval 30s
+ health_timeout 5s
+
+ # WebSocket support
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Proto {scheme}
+ header_up X-Forwarded-Host {host}
+ }
+
+ # Large file upload support (10GB)
+ request_body {
+ max_size 10GB
+ }
+
+ # Access logging
+ log {
+ output file /var/log/caddy/nomad-access.log
+ format json
+ }
+
+ # Security headers
+ header {
+ X-Frame-Options "SAMEORIGIN"
+ X-Content-Type-Options "nosniff"
+ X-XSS-Protection "1; mode=block"
+ Referrer-Policy "strict-origin-when-cross-origin"
+ }
+}
diff --git a/homelab/reverse-proxy-examples/nginx-proxy-manager.md b/homelab/reverse-proxy-examples/nginx-proxy-manager.md
new file mode 100644
index 0000000..16fb853
--- /dev/null
+++ b/homelab/reverse-proxy-examples/nginx-proxy-manager.md
@@ -0,0 +1,87 @@
+# Nginx Proxy Manager Configuration for Project N.O.M.A.D.
+
+## Prerequisites
+
+- Nginx Proxy Manager installed and running
+- Project N.O.M.A.D. stack running (via `docker compose up -d`)
+- Both NPM and Nomad on the same Docker network, or using the host IP
+
+## Setup Steps
+
+### 1. Add Proxy Host
+
+1. Open Nginx Proxy Manager web UI (typically `http://your-server:81`)
+2. Go to **Proxy Hosts** → **Add Proxy Host**
+
+### 2. Details Tab
+
+| Field | Value |
+|-------|-------|
+| **Domain Names** | `nomad.home.local` (or your domain) |
+| **Scheme** | `http` |
+| **Forward Hostname / IP** | `nomad-app` (if on same Docker network) or your host IP |
+| **Forward Port** | `8080` |
+| **Cache Assets** | Enabled |
+| **Block Common Exploits** | Enabled |
+| **Websockets Support** | **Enabled** (required for real-time updates) |
+
+### 3. SSL Tab (Optional)
+
+| Field | Value |
+|-------|-------|
+| **SSL Certificate** | Request a new certificate or select existing |
+| **Force SSL** | Enabled |
+| **HTTP/2 Support** | Enabled |
+
+### 4. Advanced Tab
+
+Add the following custom Nginx configuration:
+
+```nginx
+# Large file upload support (10GB)
+client_max_body_size 10G;
+
+# Timeout settings for large operations
+proxy_read_timeout 600s;
+proxy_send_timeout 600s;
+proxy_connect_timeout 60s;
+
+# Forward real IP headers
+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;
+proxy_set_header X-Forwarded-Host $host;
+```
+
+### 5. Docker Network Configuration
+
+If Nginx Proxy Manager runs in Docker, ensure it can reach the Nomad containers.
+
+**Option A: Same Docker network**
+
+Add NPM to the `nomad-internal` network in your NPM docker-compose:
+
+```yaml
+services:
+ npm:
+ # ... existing config ...
+ networks:
+ - nomad-internal
+
+networks:
+ nomad-internal:
+ external: true
+```
+
+**Option B: Host networking**
+
+Use your server's IP address instead of container names:
+
+- Forward Hostname: `192.168.1.100` (your server IP)
+- Forward Port: `8080`
+
+## Verification
+
+After setup, visit your configured domain. You should see the Nomad dashboard.
+
+Check the health endpoint: `https://nomad.home.local/api/health`
diff --git a/homelab/reverse-proxy-examples/traefik.yml b/homelab/reverse-proxy-examples/traefik.yml
new file mode 100644
index 0000000..146abd9
--- /dev/null
+++ b/homelab/reverse-proxy-examples/traefik.yml
@@ -0,0 +1,65 @@
+# =============================================================================
+# PROJECT N.O.M.A.D. — Homelab Edition
+# Traefik Reverse Proxy Configuration
+# =============================================================================
+#
+# Add these labels to your nomad-app service in docker-compose.yml
+# when using Traefik as your reverse proxy.
+#
+# Prerequisites:
+# - Traefik running with Docker provider enabled
+# - A Traefik network (e.g., "traefik-public")
+#
+# Usage:
+# Add to nomad-app service in docker-compose.yml:
+# labels: (copy from below)
+# networks:
+# - nomad-internal
+# - traefik-public
+#
+# =============================================================================
+
+# Docker Compose labels for nomad-app service:
+#
+# labels:
+# - "traefik.enable=true"
+# # HTTP router
+# - "traefik.http.routers.nomad.rule=Host(`nomad.home.local`)"
+# - "traefik.http.routers.nomad.entrypoints=web"
+# # HTTPS router (optional — uncomment for TLS)
+# # - "traefik.http.routers.nomad-secure.rule=Host(`nomad.home.local`)"
+# # - "traefik.http.routers.nomad-secure.entrypoints=websecure"
+# # - "traefik.http.routers.nomad-secure.tls=true"
+# # - "traefik.http.routers.nomad-secure.tls.certresolver=letsencrypt"
+# # Service
+# - "traefik.http.services.nomad.loadbalancer.server.port=8080"
+# # WebSocket support
+# - "traefik.http.middlewares.nomad-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
+# # Large file upload support
+# - "traefik.http.middlewares.nomad-buffering.buffering.maxRequestBodyBytes=10737418240"
+
+# =============================================================================
+# Traefik dynamic configuration file (alternative to labels)
+# Place in your Traefik dynamic config directory
+# =============================================================================
+
+http:
+ routers:
+ nomad:
+ rule: "Host(`nomad.home.local`)"
+ service: nomad
+ entryPoints:
+ - web
+ # Uncomment for TLS:
+ # tls:
+ # certResolver: letsencrypt
+
+ services:
+ nomad:
+ loadBalancer:
+ servers:
+ - url: "http://nomad-app:8080"
+ healthCheck:
+ path: /api/health
+ interval: 30s
+ timeout: 5s
diff --git a/homelab/truenas/Chart.yaml b/homelab/truenas/Chart.yaml
new file mode 100644
index 0000000..f90414e
--- /dev/null
+++ b/homelab/truenas/Chart.yaml
@@ -0,0 +1,25 @@
+apiVersion: v2
+name: project-nomad
+description: >-
+ Project N.O.M.A.D. — Homelab Edition.
+ Network Operations Monitoring and Automation Dashboard.
+ Offline-first knowledge server with AI chat, information library,
+ education platform, offline maps, and homelab monitoring.
+type: application
+version: 1.0.0
+appVersion: "1.29.0"
+home: https://github.com/DocwatZ/project-nomad-homelab-edition
+icon: https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/admin/public/favicons/favicon-96x96.png
+sources:
+ - https://github.com/DocwatZ/project-nomad-homelab-edition
+maintainers:
+ - name: DocwatZ
+ url: https://github.com/DocwatZ
+keywords:
+ - nomad
+ - homelab
+ - monitoring
+ - offline
+ - knowledge-base
+ - nas
+ - truenas
diff --git a/homelab/truenas/templates/deployment.yaml b/homelab/truenas/templates/deployment.yaml
new file mode 100644
index 0000000..a1eb990
--- /dev/null
+++ b/homelab/truenas/templates/deployment.yaml
@@ -0,0 +1,96 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .Release.Name }}-app
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: app
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: app
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: app
+ spec:
+ containers:
+ - name: nomad-app
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - containerPort: {{ .Values.app.port }}
+ protocol: TCP
+ env:
+ - name: NODE_ENV
+ value: {{ .Values.app.nodeEnv | quote }}
+ - name: PORT
+ value: {{ .Values.app.port | quote }}
+ - name: HOST
+ value: "0.0.0.0"
+ - name: LOG_LEVEL
+ value: {{ .Values.app.logLevel | quote }}
+ - name: APP_KEY
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-secrets
+ key: app-key
+ - name: URL
+ value: {{ .Values.app.url | quote }}
+ - name: DB_HOST
+ value: {{ .Release.Name }}-database
+ - name: DB_PORT
+ value: "3306"
+ - name: DB_DATABASE
+ value: {{ .Values.database.name | quote }}
+ - name: DB_USER
+ value: {{ .Values.database.user | quote }}
+ - name: DB_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Release.Name }}-secrets
+ key: db-password
+ - name: DB_SSL
+ value: "false"
+ - name: REDIS_HOST
+ value: {{ .Release.Name }}-redis
+ - name: REDIS_PORT
+ value: "6379"
+ - name: NOMAD_STORAGE_PATH
+ value: "/app/storage"
+ volumeMounts:
+ - name: storage
+ mountPath: /app/storage
+ livenessProbe:
+ httpGet:
+ path: /api/health
+ port: {{ .Values.app.port }}
+ initialDelaySeconds: 60
+ periodSeconds: 30
+ readinessProbe:
+ httpGet:
+ path: /api/health
+ port: {{ .Values.app.port }}
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ resources:
+ {{- toYaml .Values.resources.app | nindent 12 }}
+ volumes:
+ - name: storage
+ {{- if .Values.storage.data.existingClaim }}
+ persistentVolumeClaim:
+ claimName: {{ .Values.storage.data.existingClaim }}
+ {{- else if .Values.storage.data.hostPath }}
+ hostPath:
+ path: {{ .Values.storage.data.hostPath }}
+ type: DirectoryOrCreate
+ {{- else }}
+ persistentVolumeClaim:
+ claimName: {{ .Release.Name }}-data
+ {{- end }}
diff --git a/homelab/truenas/templates/secrets.yaml b/homelab/truenas/templates/secrets.yaml
new file mode 100644
index 0000000..4d8d3ba
--- /dev/null
+++ b/homelab/truenas/templates/secrets.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ .Release.Name }}-secrets
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+type: Opaque
+data:
+ app-key: {{ .Values.app.appKey | b64enc | quote }}
+ db-password: {{ .Values.database.password | b64enc | quote }}
+ db-root-password: {{ .Values.database.rootPassword | b64enc | quote }}
diff --git a/homelab/truenas/templates/services.yaml b/homelab/truenas/templates/services.yaml
new file mode 100644
index 0000000..1785681
--- /dev/null
+++ b/homelab/truenas/templates/services.yaml
@@ -0,0 +1,63 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Release.Name }}-app
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: app
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: {{ .Values.app.port }}
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: app
+---
+{{- if .Values.database.enabled }}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Release.Name }}-database
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: database
+spec:
+ type: ClusterIP
+ ports:
+ - port: 3306
+ targetPort: 3306
+ protocol: TCP
+ name: mysql
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: database
+{{- end }}
+---
+{{- if .Values.redis.enabled }}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Release.Name }}-redis
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: redis
+spec:
+ type: ClusterIP
+ ports:
+ - port: 6379
+ targetPort: 6379
+ protocol: TCP
+ name: redis
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/component: redis
+{{- end }}
diff --git a/homelab/truenas/values.yaml b/homelab/truenas/values.yaml
new file mode 100644
index 0000000..ec24c4c
--- /dev/null
+++ b/homelab/truenas/values.yaml
@@ -0,0 +1,118 @@
+# =============================================================================
+# PROJECT N.O.M.A.D. — Homelab Edition
+# TrueNAS SCALE Helm Values
+# =============================================================================
+
+# Application image
+image:
+ repository: ghcr.io/crosstalk-solutions/project-nomad
+ tag: latest
+ pullPolicy: Always
+
+# Replica count
+replicaCount: 1
+
+# Application settings
+app:
+ port: 8080
+ nodeEnv: production
+ logLevel: info
+ # Generate with: openssl rand -hex 32
+ appKey: ""
+ # External URL for accessing Nomad
+ url: "http://localhost:8080"
+
+# Database settings
+database:
+ enabled: true
+ image:
+ repository: mysql
+ tag: "8.0"
+ rootPassword: ""
+ name: nomad
+ user: nomad
+ password: ""
+ storage:
+ size: 10Gi
+ # TrueNAS storage class
+ storageClass: ""
+
+# Redis settings
+redis:
+ enabled: true
+ image:
+ repository: redis
+ tag: 7-alpine
+ maxMemory: 256mb
+ storage:
+ size: 1Gi
+ storageClass: ""
+
+# Nginx reverse proxy
+nginx:
+ enabled: true
+ image:
+ repository: nginx
+ tag: alpine
+
+# Storage
+storage:
+ # Main data storage for Nomad content
+ data:
+ enabled: true
+ size: 100Gi
+ storageClass: ""
+ # Use an existing PVC
+ existingClaim: ""
+ # Host path (for TrueNAS ix-volumes)
+ hostPath: ""
+
+ # Log storage
+ logs:
+ enabled: true
+ size: 5Gi
+ storageClass: ""
+
+# Service configuration
+service:
+ type: ClusterIP
+ port: 80
+
+# Ingress configuration
+ingress:
+ enabled: false
+ className: ""
+ annotations: {}
+ hosts:
+ - host: nomad.home.local
+ paths:
+ - path: /
+ pathType: Prefix
+ tls: []
+
+# Resource limits
+resources:
+ app:
+ limits:
+ memory: 2Gi
+ requests:
+ cpu: 250m
+ memory: 512Mi
+ worker:
+ limits:
+ memory: 2Gi
+ requests:
+ cpu: 100m
+ memory: 256Mi
+ database:
+ limits:
+ memory: 1Gi
+ requests:
+ cpu: 250m
+ memory: 256Mi
+ redis:
+ limits:
+ memory: 512Mi
+ requests:
+ cpu: 50m
+ memory: 64Mi
diff --git a/homelab/unraid-template.xml b/homelab/unraid-template.xml
new file mode 100644
index 0000000..ba9674f
--- /dev/null
+++ b/homelab/unraid-template.xml
@@ -0,0 +1,46 @@
+
+
-# Project N.O.M.A.D.
-### Node for Offline Media, Archives, and Data
+# Project N.O.M.A.D. — Homelab Edition
+### Network Operations Monitoring and Automation Dashboard
-**Knowledge That Never Goes Offline**
+**Knowledge That Never Goes Offline — Now Container-Native for Your Homelab**
[](https://www.projectnomad.us)
[](https://discord.com/invite/crosstalksolutions)
@@ -14,34 +14,94 @@
---
-Project N.O.M.A.D. is a self-contained, offline-first knowledge and education server packed with critical tools, knowledge, and AI to keep you informed and empowered—anytime, anywhere.
+Project N.O.M.A.D. Homelab Edition is a container-native fork of [Project N.O.M.A.D.](https://github.com/CrosstalkSolutions/project-nomad), optimized for NAS systems and homelab environments. It runs as a portable Docker Compose stack on **Unraid**, **TrueNAS SCALE**, and any standard Docker host.
-## Installation & Quickstart
-Project N.O.M.A.D. can be installed on any Debian-based operating system (we recommend Ubuntu). Installation is completely terminal-based, and all tools and resources are designed to be accessed through the browser, so there's no need for a desktop environment if you'd rather setup N.O.M.A.D. as a "server" and access it through other clients.
+## Quick Start (Docker Compose)
-*Note: sudo/root privileges are required to run the install script*
-
-#### Quick Install
```bash
-sudo apt-get update && sudo apt-get install -y curl && curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/install_nomad.sh -o install_nomad.sh && sudo bash install_nomad.sh
+# 1. Clone the repository
+git clone https://github.com/DocwatZ/project-nomad-homelab-edition.git
+cd project-nomad-homelab-edition
+
+# 2. Configure environment
+cp .env.example .env
+sed -i "s/^APP_KEY=replaceme/APP_KEY=$(openssl rand -hex 32)/" .env
+sed -i "s/^DB_PASSWORD=replaceme/DB_PASSWORD=$(openssl rand -base64 24)/" .env
+sed -i "s/^MYSQL_ROOT_PASSWORD=replaceme/MYSQL_ROOT_PASSWORD=$(openssl rand -base64 24)/" .env
+
+# 3. Create data directories
+sudo mkdir -p /opt/project-nomad/{storage,redis,logs/nginx}
+
+# 4. Start the stack
+docker compose up -d
```
-Project N.O.M.A.D. is now installed on your device! Open a browser and navigate to `http://localhost:8080` (or `http://DEVICE_IP:8080`) to start exploring!
+Open `http://localhost:8080` to access the dashboard.
-## How It Works
-N.O.M.A.D. is a management UI ("Command Center") and API that orchestrates a collection of containerized tools and resources via [Docker](https://www.docker.com/). It handles installation, configuration, and updates for everything — so you don't have to.
+## Container Architecture
-**Built-in capabilities include:**
-- **AI Chat with Knowledge Base** — local AI chat powered by [Ollama](https://ollama.com/), with document upload and semantic search (RAG via [Qdrant](https://qdrant.tech/))
-- **Information Library** — offline Wikipedia, medical references, ebooks, and more via [Kiwix](https://kiwix.org/)
-- **Education Platform** — Khan Academy courses with progress tracking via [Kolibri](https://learningequality.org/kolibri/)
-- **Offline Maps** — downloadable regional maps via [ProtoMaps](https://protomaps.com)
-- **Data Tools** — encryption, encoding, and analysis via [CyberChef](https://gchq.github.io/CyberChef/)
-- **Notes** — local note-taking via [FlatNotes](https://github.com/dullage/flatnotes)
-- **System Benchmark** — hardware scoring with a [community leaderboard](https://benchmark.projectnomad.us)
-- **Easy Setup Wizard** — guided first-time configuration with curated content collections
+| Service | Image | Purpose |
+|---------|-------|---------|
+| **nomad-app** | project-nomad | Web UI + API server |
+| **nomad-worker** | project-nomad | Background job processing |
+| **nomad-database** | mysql:8.0 | Persistent data storage |
+| **nomad-cache** | redis:7-alpine | Cache + job queues |
+| **nomad-nginx** | nginx:alpine | Reverse proxy |
-N.O.M.A.D. also includes built-in tools like a Wikipedia content selector, ZIM library manager, and content explorer.
+All services communicate over a private Docker network (`nomad-internal`).
+
+## NAS Compatibility
+
+| Platform | Storage Path | Guide |
+|----------|-------------|-------|
+| **Unraid** | `/mnt/user/appdata/project-nomad` | [Unraid Guide](docs/homelab/unraid-guide.md) |
+| **TrueNAS SCALE** | `/mnt/pool/apps/project-nomad` | [TrueNAS Guide](docs/homelab/truenas-guide.md) |
+| **Linux** | `/opt/project-nomad` | [Installation Guide](docs/homelab/installation-guide.md) |
+
+### NAS Install Templates
+
+- **Unraid**: [Community Apps XML template](homelab/unraid-template.xml)
+- **TrueNAS SCALE**: [Helm chart](homelab/truenas/)
+- **Any Docker host**: [docker-compose.yml](docker-compose.yml)
+
+## Storage Layout
+
+```
+NOMAD_DATA_DIR/
+├── storage/ # Content files (ZIM, maps, uploads) — NAS share OK
+├── redis/ # Redis persistence
+└── logs/
+ └── nginx/ # Nginx access/error logs
+```
+
+The MySQL database uses a **Docker named volume** for optimal I/O, avoiding NFS/SMB latency.
+
+## Reverse Proxy Support
+
+Works behind common homelab reverse proxies:
+
+| Proxy | Configuration |
+|-------|--------------|
+| **Nginx Proxy Manager** | [Setup guide](homelab/reverse-proxy-examples/nginx-proxy-manager.md) |
+| **Traefik** | [Traefik config](homelab/reverse-proxy-examples/traefik.yml) |
+| **Caddy** | [Caddyfile](homelab/reverse-proxy-examples/caddy.Caddyfile) |
+
+## Monitoring Agent
+
+Deploy lightweight monitoring agents on homelab nodes:
+
+```bash
+docker run -d --name nomad-agent \
+ -p 9100:9100 \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
+ -v /proc:/host/proc:ro \
+ -e NODE_NAME=my-server \
+ nomad-agent
+```
+
+Features: CPU/RAM/disk metrics, Docker container monitoring, Prometheus `/metrics` endpoint.
+
+See the [Agent Guide](docs/homelab/agent-guide.md) for details.
## What's Included
@@ -56,113 +116,69 @@ N.O.M.A.D. also includes built-in tools like a Wikipedia content selector, ZIM l
| System Benchmark | Built-in | Hardware scoring, Builder Tags, and community leaderboard |
## Device Requirements
-While many similar offline survival computers are designed to be run on bare-minimum, lightweight hardware, Project N.O.M.A.D. is quite the opposite. To install and run the
-available AI tools, we highly encourage the use of a beefy, GPU-backed device to make the most of your install.
-
-At it's core, however, N.O.M.A.D. is still very lightweight. For a barebones installation of the management application itself, the following minimal specs are required:
-
-*Note: Project N.O.M.A.D. is not sponsored by any hardware manufacturer and is designed to be as hardware-agnostic as possible. The harware listed below is for example/comparison use only*
#### Minimum Specs
-- Processor: 2 GHz dual-core processor or better
-- RAM: 4GB system memory
-- Storage: At least 5 GB free disk space
-- OS: Debian-based (Ubuntu recommended)
-- Stable internet connection (required during install only)
-
-To run LLM's and other included AI tools:
+- 2 GHz dual-core processor
+- 4 GB RAM (8 GB recommended)
+- 5 GB free disk space
+- Docker 20.10+ and Docker Compose v2+
+- Any Linux, Unraid, or TrueNAS SCALE host
#### Optimal Specs
-- Processor: AMD Ryzen 7 or Intel Core i7 or better
-- RAM: 32 GB system memory
-- Graphics: NVIDIA RTX 3060 or AMD equivalent or better (more VRAM = run larger models)
-- Storage: At least 250 GB free disk space (preferably on SSD)
-- OS: Debian-based (Ubuntu recommended)
-- Stable internet connection (required during install only)
+- AMD Ryzen 7 / Intel Core i7 or better
+- 32 GB RAM
+- NVIDIA RTX 3060+ (for AI features)
+- 250 GB SSD
+- Stable internet connection (for initial content downloads)
-**For detailed build recommendations at three price points ($200–$800+), see the [Hardware Guide](https://www.projectnomad.us/hardware).**
+## Documentation
-Again, Project N.O.M.A.D. itself is quite lightweight - it's the tools and resources you choose to install with N.O.M.A.D. that will determine the specs required for your unique deployment
+| Guide | Description |
+|-------|-------------|
+| [Installation Guide](docs/homelab/installation-guide.md) | Docker Compose setup |
+| [Unraid Guide](docs/homelab/unraid-guide.md) | Unraid-specific installation |
+| [TrueNAS Guide](docs/homelab/truenas-guide.md) | TrueNAS SCALE installation |
+| [Agent Guide](docs/homelab/agent-guide.md) | Monitoring agent setup |
+| [Architecture](docs/homelab/architecture.md) | System design and data flow |
+| [Monitoring](docs/homelab/monitoring.md) | Monitoring stack and Prometheus integration |
+
+## Configuration
+
+All configuration is handled through environment variables in `.env`. See [`.env.example`](.env.example) for all options.
+
+Key settings:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `APP_KEY` | — | Encryption key (**required**) |
+| `DB_PASSWORD` | — | Database password (**required**) |
+| `URL` | http://localhost:8080 | External access URL |
+| `NOMAD_DATA_DIR` | /opt/project-nomad | Host storage directory |
+| `PORT` | 8080 | Application port |
+| `LOG_LEVEL` | info | Log verbosity |
+
+## Updating
+
+```bash
+docker compose pull
+docker compose up -d
+```
## About Internet Usage & Privacy
Project N.O.M.A.D. is designed for offline usage. An internet connection is only required during the initial installation (to download dependencies) and if you (the user) decide to download additional tools and resources at a later time. Otherwise, N.O.M.A.D. does not require an internet connection and has ZERO built-in telemetry.
-To test internet connectivity, N.O.M.A.D. attempts to make a request to Cloudflare's utility endpoint, `https://1.1.1.1/cdn-cgi/trace` and checks for a successful response.
-
-## About Security
-By design, Project N.O.M.A.D. is intended to be open and available without hurdles - it includes no authentication. If you decide to connect your device to a local network after install (e.g. for allowing other devices to access it's resources), you can block/open ports to control which services are exposed.
-
-**Will authentication be added in the future?** Maybe. It's not currently a priority, but if there's enough demand for it, we may consider building in an optional authentication layer in a future release to support uses cases where multiple users need access to the same instance but with different permission levels (e.g. family use with parental controls, classroom use with teacher/admin accounts, etc.). For now, we recommend using network-level controls to manage access if you're planning to expose your N.O.M.A.D. instance to other devices on a local network. N.O.M.A.D. is not designed to be exposed directly to the internet, and we strongly advise against doing so unless you really know what you're doing, have taken appropriate security measures, and understand the risks involved.
-
## Contributing
-Contributions are welcome and appreciated! Please read this section fully to understand how to contribute to the project.
+Contributions are welcome and appreciated! Please read the [Contributing Guide](CONTRIBUTING.md) for details.
-### General Guidelines
-
-- **Open an issue first**: Before starting work on a new feature or bug fix, please open an issue to discuss your proposed changes. This helps ensure that your contribution aligns with the project's goals and avoids duplicate work. Title the issue clearly and provide a detailed description of the problem or feature you want to work on.
-- **Fork the repository**: Click the "Fork" button at the top right of the repository page to create a copy of the project under your GitHub account.
-- **Create a new branch**: In your forked repository, create a new branch for your work. Use a descriptive name for the branch that reflects the purpose of your changes (e.g., `fix/issue-123` or `feature/add-new-tool`).
-- **Make your changes**: Implement your changes in the new branch. Follow the existing code style and conventions used in the project. Be sure to test your changes locally to ensure they work as expected.
-- **Add Release Notes**: If your changes include new features, bug fixes, or improvements, please see the "Release Notes" section below to properly document your contribution for the next release.
-- **Conventional Commits**: When committing your changes, please use conventional commit messages to provide clear and consistent commit history. The format is `