mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-08 09:46:15 +02:00
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:
parent
1352258230
commit
935d0ab22b
90
.env.example
Normal file
90
.env.example
Normal 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
44
agent/Dockerfile
Normal 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
300
agent/agent.mjs
Normal 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
14
agent/healthcheck.mjs
Normal 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
11
agent/package.json
Normal 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
201
docker-compose.yml
Normal 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
|
||||||
49
homelab/reverse-proxy-examples/caddy.Caddyfile
Normal file
49
homelab/reverse-proxy-examples/caddy.Caddyfile
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
87
homelab/reverse-proxy-examples/nginx-proxy-manager.md
Normal file
87
homelab/reverse-proxy-examples/nginx-proxy-manager.md
Normal 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`
|
||||||
65
homelab/reverse-proxy-examples/traefik.yml
Normal file
65
homelab/reverse-proxy-examples/traefik.yml
Normal 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
|
||||||
25
homelab/truenas/Chart.yaml
Normal file
25
homelab/truenas/Chart.yaml
Normal 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
|
||||||
96
homelab/truenas/templates/deployment.yaml
Normal file
96
homelab/truenas/templates/deployment.yaml
Normal 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 }}
|
||||||
12
homelab/truenas/templates/secrets.yaml
Normal file
12
homelab/truenas/templates/secrets.yaml
Normal 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 }}
|
||||||
63
homelab/truenas/templates/services.yaml
Normal file
63
homelab/truenas/templates/services.yaml
Normal 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
118
homelab/truenas/values.yaml
Normal 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
|
||||||
46
homelab/unraid-template.xml
Normal file
46
homelab/unraid-template.xml
Normal 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
77
nginx/default.conf
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user