Add container stack, nginx config, NAS templates, and monitoring agent

Co-authored-by: DocwatZ <227472400+DocwatZ@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-13 17:32:28 +00:00
parent 1352258230
commit 935d0ab22b
16 changed files with 1298 additions and 0 deletions

90
.env.example Normal file
View File

@ -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

44
agent/Dockerfile Normal file
View File

@ -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"]

300
agent/agent.mjs Normal file
View File

@ -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)
})

14
agent/healthcheck.mjs Normal file
View File

@ -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)
}

11
agent/package.json Normal file
View File

@ -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"
}

201
docker-compose.yml Normal file
View File

@ -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

View File

@ -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"
}
}

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

118
homelab/truenas/values.yaml Normal file
View File

@ -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

View File

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<Container version="2">
<Name>ProjectNomad</Name>
<Repository>ghcr.io/crosstalk-solutions/project-nomad:latest</Repository>
<Registry>https://github.com/crosstalk-solutions/project-nomad/pkgs/container/project-nomad</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>bash</Shell>
<Privileged>false</Privileged>
<Support>https://github.com/DocwatZ/project-nomad-homelab-edition/issues</Support>
<Project>https://github.com/DocwatZ/project-nomad-homelab-edition</Project>
<Overview>
Project N.O.M.A.D. — Homelab Edition (Network Operations Monitoring and Automation Dashboard).
An offline-first knowledge server with AI chat, information library, education platform,
offline maps, and homelab monitoring. Requires MySQL 8.0 and Redis 7 containers.
</Overview>
<Category>HomeAutomation: Tools: Productivity:</Category>
<WebUI>http://[IP]:[PORT:8080]</WebUI>
<TemplateURL>https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/homelab/unraid-template.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/admin/public/favicons/favicon-96x96.png</Icon>
<ExtraParams>--restart unless-stopped</ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<Requires>
Requires separate MySQL 8.0 and Redis 7 containers. See documentation for full stack setup.
</Requires>
<Config Name="Web UI Port" Target="8080" Default="8080" Mode="tcp" Description="Web interface port" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
<Config Name="Storage Path" Target="/app/storage" Default="/mnt/user/appdata/project-nomad/storage" Mode="rw" Description="Path for Nomad content storage (ZIM files, maps, uploads)" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/project-nomad/storage</Config>
<Config Name="Docker Socket" Target="/var/run/docker.sock" Default="/var/run/docker.sock" Mode="ro" Description="Docker socket for container management" Type="Path" Display="advanced" Required="false" Mask="false">/var/run/docker.sock</Config>
<Config Name="NODE_ENV" Target="NODE_ENV" Default="production" Mode="" Description="Node environment" Type="Variable" Display="advanced" Required="true" Mask="false">production</Config>
<Config Name="APP_KEY" Target="APP_KEY" Default="" Mode="" Description="Application encryption key. Generate with: openssl rand -hex 32" Type="Variable" Display="always" Required="true" Mask="true"/>
<Config Name="URL" Target="URL" Default="http://localhost:8080" Mode="" Description="External URL for accessing Nomad" Type="Variable" Display="always" Required="true" Mask="false">http://localhost:8080</Config>
<Config Name="DB_HOST" Target="DB_HOST" Default="nomad-database" Mode="" Description="MySQL database hostname" Type="Variable" Display="always" Required="true" Mask="false">nomad-database</Config>
<Config Name="DB_PORT" Target="DB_PORT" Default="3306" Mode="" Description="MySQL database port" Type="Variable" Display="advanced" Required="true" Mask="false">3306</Config>
<Config Name="DB_DATABASE" Target="DB_DATABASE" Default="nomad" Mode="" Description="MySQL database name" Type="Variable" Display="advanced" Required="true" Mask="false">nomad</Config>
<Config Name="DB_USER" Target="DB_USER" Default="nomad" Mode="" Description="MySQL database user" Type="Variable" Display="always" Required="true" Mask="false">nomad</Config>
<Config Name="DB_PASSWORD" Target="DB_PASSWORD" Default="" Mode="" Description="MySQL database password" Type="Variable" Display="always" Required="true" Mask="true"/>
<Config Name="DB_SSL" Target="DB_SSL" Default="false" Mode="" Description="Enable database SSL" Type="Variable" Display="advanced" Required="false" Mask="false">false</Config>
<Config Name="REDIS_HOST" Target="REDIS_HOST" Default="nomad-cache" Mode="" Description="Redis hostname" Type="Variable" Display="always" Required="true" Mask="false">nomad-cache</Config>
<Config Name="REDIS_PORT" Target="REDIS_PORT" Default="6379" Mode="" Description="Redis port" Type="Variable" Display="advanced" Required="true" Mask="false">6379</Config>
<Config Name="LOG_LEVEL" Target="LOG_LEVEL" Default="info" Mode="" Description="Log level (debug, info, warn, error)" Type="Variable" Display="advanced" Required="false" Mask="false">info</Config>
<Config Name="NOMAD_STORAGE_PATH" Target="NOMAD_STORAGE_PATH" Default="/app/storage" Mode="" Description="Internal storage path (do not change unless you know what you are doing)" Type="Variable" Display="advanced" Required="true" Mask="false">/app/storage</Config>
</Container>

77
nginx/default.conf Normal file
View File

@ -0,0 +1,77 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# Nginx Reverse Proxy Configuration
# =============================================================================
upstream nomad_app {
server nomad-app:8080;
keepalive 32;
}
server {
listen 80;
server_name _;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Client body size (for file uploads)
client_max_body_size 10G;
# Timeouts for large file operations
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 60s;
# Health check endpoint (bypasses proxy for fast response)
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# WebSocket support for Transmit (real-time updates)
location /__transmit {
proxy_pass http://nomad_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
}
# Main application
location / {
proxy_pass http://nomad_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Buffer settings for large responses
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 32k;
}
# Static assets caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://nomad_app;
proxy_set_header Host $host;
expires 7d;
add_header Cache-Control "public, immutable";
}
}