mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01: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