Merge pull request #1 from DocwatZ/copilot/project-nomad-homelab-edition

Container-native architecture for NAS and homelab environments
This commit is contained in:
Doc 2026-03-13 18:11:29 +00:00 committed by GitHub
commit 0784b444c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 2908 additions and 134 deletions

90
.env.example Normal file
View File

@ -0,0 +1,90 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# Network Operations Monitoring and Automation Dashboard
# =============================================================================
# Copy this file to .env and configure for your environment.
# All values marked "replaceme" MUST be changed before starting.
# =============================================================================
# -----------------------------------------------------------------------------
# Application Settings
# -----------------------------------------------------------------------------
NODE_ENV=production
PORT=8080
HOST=0.0.0.0
LOG_LEVEL=info
# Generate a random key: openssl rand -hex 32
APP_KEY=replaceme
# The external URL where Nomad will be accessed (used for links, redirects)
# Examples: http://192.168.1.100:8080, https://nomad.home.local
URL=http://localhost:8080
# Optional: Base URL path if running behind a reverse proxy with a subpath
# BASE_URL=/
# -----------------------------------------------------------------------------
# Database (MySQL 8.0)
# -----------------------------------------------------------------------------
DB_HOST=nomad-database
DB_PORT=3306
DB_DATABASE=nomad
DB_USER=nomad
DB_PASSWORD=replaceme
DB_SSL=false
# MySQL root password (used for initial database creation)
MYSQL_ROOT_PASSWORD=replaceme
# -----------------------------------------------------------------------------
# Redis Cache / Queue
# -----------------------------------------------------------------------------
REDIS_HOST=nomad-cache
REDIS_PORT=6379
# Optional: Redis password (uncomment to enable)
# REDIS_PASSWORD=
# -----------------------------------------------------------------------------
# Storage Paths (inside container — mapped via Docker volumes)
# -----------------------------------------------------------------------------
# Path where Nomad stores content (ZIM files, maps, uploads)
NOMAD_STORAGE_PATH=/app/storage
# -----------------------------------------------------------------------------
# NAS / Host Storage Mapping
# -----------------------------------------------------------------------------
# Set these to your NAS storage paths on the HOST system.
# These are used in docker-compose.yml volume mappings.
#
# Unraid example: /mnt/user/appdata/project-nomad
# TrueNAS example: /mnt/pool/apps/project-nomad
# Linux example: /opt/project-nomad
NOMAD_DATA_DIR=/opt/project-nomad
# -----------------------------------------------------------------------------
# Reverse Proxy Settings
# -----------------------------------------------------------------------------
# Set to 'true' if running behind a TLS-terminating reverse proxy
# TRUSTED_PROXY=false
# Trusted proxy IP ranges (comma-separated CIDR)
# TRUSTED_PROXY_IPS=172.16.0.0/12,192.168.0.0/16,10.0.0.0/8
# -----------------------------------------------------------------------------
# Monitoring Agent Settings (optional)
# -----------------------------------------------------------------------------
# AGENT_SECRET=replaceme
# AGENT_PORT=9100
# AGENT_COLLECT_INTERVAL=30
# -----------------------------------------------------------------------------
# Optional: External API URL for Nomad benchmark leaderboard
# -----------------------------------------------------------------------------
# NOMAD_API_URL=https://api.projectnomad.io
# -----------------------------------------------------------------------------
# Optional: Internet connectivity test URL
# -----------------------------------------------------------------------------
# INTERNET_STATUS_TEST_URL=https://connectivity-check.ubuntu.com

View File

@ -1,7 +1,8 @@
FROM node:22-slim AS base
# Install bash & curl for entrypoint script compatibility, graphicsmagick for pdf2pic, and vips-dev & build-base for sharp
RUN apt-get update && apt-get install -y bash curl graphicsmagick libvips-dev build-essential
# Install bash & curl for entrypoint script compatibility, graphicsmagick for pdf2pic, and vips-dev & build-base for sharp
RUN apt-get update && apt-get install -y bash curl graphicsmagick libvips-dev build-essential \
&& rm -rf /var/lib/apt/lists/*
# All deps stage
FROM base AS deps
@ -29,14 +30,14 @@ ARG BUILD_DATE
ARG VCS_REF
# Labels
LABEL org.opencontainers.image.title="Project N.O.M.A.D" \
org.opencontainers.image.description="The Project N.O.M.A.D Official Docker image" \
LABEL org.opencontainers.image.title="Project N.O.M.A.D — Homelab Edition" \
org.opencontainers.image.description="Network Operations Monitoring and Automation Dashboard — Container-native homelab platform" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.vendor="Crosstalk Solutions, LLC" \
org.opencontainers.image.documentation="https://github.com/CrosstalkSolutions/project-nomad/blob/main/README.md" \
org.opencontainers.image.source="https://github.com/CrosstalkSolutions/project-nomad" \
org.opencontainers.image.documentation="https://github.com/DocwatZ/project-nomad-homelab-edition/blob/main/README.md" \
org.opencontainers.image.source="https://github.com/DocwatZ/project-nomad-homelab-edition" \
org.opencontainers.image.licenses="Apache-2.0"
ENV NODE_ENV=production
@ -47,5 +48,13 @@ COPY --from=build /app/build /app
COPY package.json /app/version.json
COPY admin/docs /app/docs
COPY README.md /app/README.md
# Create storage directory with proper permissions
RUN mkdir -p /app/storage && chown -R node:node /app/storage
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
CMD curl -f http://localhost:8080/api/health || exit 1
CMD ["node", "./bin/server.js"]

246
README.md
View File

@ -1,10 +1,10 @@
<div align="center">
<img src="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/admin/public/project_nomad_logo.png" width="200" height="200"/>
# Project N.O.M.A.D.
### Node for Offline Media, Archives, and Data
# Project N.O.M.A.D. — Homelab Edition
### Network Operations Monitoring and Automation Dashboard
**Knowledge That Never Goes Offline**
**Knowledge That Never Goes Offline — Now Container-Native for Your Homelab**
[![Website](https://img.shields.io/badge/Website-projectnomad.us-blue)](https://www.projectnomad.us)
[![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2)](https://discord.com/invite/crosstalksolutions)
@ -14,34 +14,94 @@
---
Project N.O.M.A.D. is a self-contained, offline-first knowledge and education server packed with critical tools, knowledge, and AI to keep you informed and empowered—anytime, anywhere.
Project N.O.M.A.D. Homelab Edition is a container-native fork of [Project N.O.M.A.D.](https://github.com/CrosstalkSolutions/project-nomad), optimized for NAS systems and homelab environments. It runs as a portable Docker Compose stack on **Unraid**, **TrueNAS SCALE**, and any standard Docker host.
## Installation & Quickstart
Project N.O.M.A.D. can be installed on any Debian-based operating system (we recommend Ubuntu). Installation is completely terminal-based, and all tools and resources are designed to be accessed through the browser, so there's no need for a desktop environment if you'd rather setup N.O.M.A.D. as a "server" and access it through other clients.
## Quick Start (Docker Compose)
*Note: sudo/root privileges are required to run the install script*
#### Quick Install
```bash
sudo apt-get update && sudo apt-get install -y curl && curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/install_nomad.sh -o install_nomad.sh && sudo bash install_nomad.sh
# 1. Clone the repository
git clone https://github.com/DocwatZ/project-nomad-homelab-edition.git
cd project-nomad-homelab-edition
# 2. Configure environment
cp .env.example .env
sed -i "s/^APP_KEY=replaceme/APP_KEY=$(openssl rand -hex 32)/" .env
sed -i "s/^DB_PASSWORD=replaceme/DB_PASSWORD=$(openssl rand -base64 24)/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=replaceme/MYSQL_ROOT_PASSWORD=$(openssl rand -base64 24)/" .env
# 3. Create data directories
sudo mkdir -p /opt/project-nomad/{storage,redis,logs/nginx}
# 4. Start the stack
docker compose up -d
```
Project N.O.M.A.D. is now installed on your device! Open a browser and navigate to `http://localhost:8080` (or `http://DEVICE_IP:8080`) to start exploring!
Open `http://localhost:8080` to access the dashboard.
## How It Works
N.O.M.A.D. is a management UI ("Command Center") and API that orchestrates a collection of containerized tools and resources via [Docker](https://www.docker.com/). It handles installation, configuration, and updates for everything — so you don't have to.
## Container Architecture
**Built-in capabilities include:**
- **AI Chat with Knowledge Base** — local AI chat powered by [Ollama](https://ollama.com/), with document upload and semantic search (RAG via [Qdrant](https://qdrant.tech/))
- **Information Library** — offline Wikipedia, medical references, ebooks, and more via [Kiwix](https://kiwix.org/)
- **Education Platform** — Khan Academy courses with progress tracking via [Kolibri](https://learningequality.org/kolibri/)
- **Offline Maps** — downloadable regional maps via [ProtoMaps](https://protomaps.com)
- **Data Tools** — encryption, encoding, and analysis via [CyberChef](https://gchq.github.io/CyberChef/)
- **Notes** — local note-taking via [FlatNotes](https://github.com/dullage/flatnotes)
- **System Benchmark** — hardware scoring with a [community leaderboard](https://benchmark.projectnomad.us)
- **Easy Setup Wizard** — guided first-time configuration with curated content collections
| Service | Image | Purpose |
|---------|-------|---------|
| **nomad-app** | project-nomad | Web UI + API server |
| **nomad-worker** | project-nomad | Background job processing |
| **nomad-database** | mysql:8.0 | Persistent data storage |
| **nomad-cache** | redis:7-alpine | Cache + job queues |
| **nomad-nginx** | nginx:alpine | Reverse proxy |
N.O.M.A.D. also includes built-in tools like a Wikipedia content selector, ZIM library manager, and content explorer.
All services communicate over a private Docker network (`nomad-internal`).
## NAS Compatibility
| Platform | Storage Path | Guide |
|----------|-------------|-------|
| **Unraid** | `/mnt/user/appdata/project-nomad` | [Unraid Guide](docs/homelab/unraid-guide.md) |
| **TrueNAS SCALE** | `/mnt/pool/apps/project-nomad` | [TrueNAS Guide](docs/homelab/truenas-guide.md) |
| **Linux** | `/opt/project-nomad` | [Installation Guide](docs/homelab/installation-guide.md) |
### NAS Install Templates
- **Unraid**: [Community Apps XML template](homelab/unraid-template.xml)
- **TrueNAS SCALE**: [Helm chart](homelab/truenas/)
- **Any Docker host**: [docker-compose.yml](docker-compose.yml)
## Storage Layout
```
NOMAD_DATA_DIR/
├── storage/ # Content files (ZIM, maps, uploads) — NAS share OK
├── redis/ # Redis persistence
└── logs/
└── nginx/ # Nginx access/error logs
```
The MySQL database uses a **Docker named volume** for optimal I/O, avoiding NFS/SMB latency.
## Reverse Proxy Support
Works behind common homelab reverse proxies:
| Proxy | Configuration |
|-------|--------------|
| **Nginx Proxy Manager** | [Setup guide](homelab/reverse-proxy-examples/nginx-proxy-manager.md) |
| **Traefik** | [Traefik config](homelab/reverse-proxy-examples/traefik.yml) |
| **Caddy** | [Caddyfile](homelab/reverse-proxy-examples/caddy.Caddyfile) |
## Monitoring Agent
Deploy lightweight monitoring agents on homelab nodes:
```bash
docker run -d --name nomad-agent \
-p 9100:9100 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /proc:/host/proc:ro \
-e NODE_NAME=my-server \
nomad-agent
```
Features: CPU/RAM/disk metrics, Docker container monitoring, Prometheus `/metrics` endpoint.
See the [Agent Guide](docs/homelab/agent-guide.md) for details.
## What's Included
@ -56,113 +116,69 @@ N.O.M.A.D. also includes built-in tools like a Wikipedia content selector, ZIM l
| System Benchmark | Built-in | Hardware scoring, Builder Tags, and community leaderboard |
## Device Requirements
While many similar offline survival computers are designed to be run on bare-minimum, lightweight hardware, Project N.O.M.A.D. is quite the opposite. To install and run the
available AI tools, we highly encourage the use of a beefy, GPU-backed device to make the most of your install.
At it's core, however, N.O.M.A.D. is still very lightweight. For a barebones installation of the management application itself, the following minimal specs are required:
*Note: Project N.O.M.A.D. is not sponsored by any hardware manufacturer and is designed to be as hardware-agnostic as possible. The harware listed below is for example/comparison use only*
#### Minimum Specs
- Processor: 2 GHz dual-core processor or better
- RAM: 4GB system memory
- Storage: At least 5 GB free disk space
- OS: Debian-based (Ubuntu recommended)
- Stable internet connection (required during install only)
To run LLM's and other included AI tools:
- 2 GHz dual-core processor
- 4 GB RAM (8 GB recommended)
- 5 GB free disk space
- Docker 20.10+ and Docker Compose v2+
- Any Linux, Unraid, or TrueNAS SCALE host
#### Optimal Specs
- Processor: AMD Ryzen 7 or Intel Core i7 or better
- RAM: 32 GB system memory
- Graphics: NVIDIA RTX 3060 or AMD equivalent or better (more VRAM = run larger models)
- Storage: At least 250 GB free disk space (preferably on SSD)
- OS: Debian-based (Ubuntu recommended)
- Stable internet connection (required during install only)
- AMD Ryzen 7 / Intel Core i7 or better
- 32 GB RAM
- NVIDIA RTX 3060+ (for AI features)
- 250 GB SSD
- Stable internet connection (for initial content downloads)
**For detailed build recommendations at three price points ($200$800+), see the [Hardware Guide](https://www.projectnomad.us/hardware).**
## Documentation
Again, Project N.O.M.A.D. itself is quite lightweight - it's the tools and resources you choose to install with N.O.M.A.D. that will determine the specs required for your unique deployment
| Guide | Description |
|-------|-------------|
| [Installation Guide](docs/homelab/installation-guide.md) | Docker Compose setup |
| [Unraid Guide](docs/homelab/unraid-guide.md) | Unraid-specific installation |
| [TrueNAS Guide](docs/homelab/truenas-guide.md) | TrueNAS SCALE installation |
| [Agent Guide](docs/homelab/agent-guide.md) | Monitoring agent setup |
| [Architecture](docs/homelab/architecture.md) | System design and data flow |
| [Monitoring](docs/homelab/monitoring.md) | Monitoring stack and Prometheus integration |
## Configuration
All configuration is handled through environment variables in `.env`. See [`.env.example`](.env.example) for all options.
Key settings:
| Variable | Default | Description |
|----------|---------|-------------|
| `APP_KEY` | — | Encryption key (**required**) |
| `DB_PASSWORD` | — | Database password (**required**) |
| `URL` | http://localhost:8080 | External access URL |
| `NOMAD_DATA_DIR` | /opt/project-nomad | Host storage directory |
| `PORT` | 8080 | Application port |
| `LOG_LEVEL` | info | Log verbosity |
## Updating
```bash
docker compose pull
docker compose up -d
```
## About Internet Usage & Privacy
Project N.O.M.A.D. is designed for offline usage. An internet connection is only required during the initial installation (to download dependencies) and if you (the user) decide to download additional tools and resources at a later time. Otherwise, N.O.M.A.D. does not require an internet connection and has ZERO built-in telemetry.
To test internet connectivity, N.O.M.A.D. attempts to make a request to Cloudflare's utility endpoint, `https://1.1.1.1/cdn-cgi/trace` and checks for a successful response.
## About Security
By design, Project N.O.M.A.D. is intended to be open and available without hurdles - it includes no authentication. If you decide to connect your device to a local network after install (e.g. for allowing other devices to access it's resources), you can block/open ports to control which services are exposed.
**Will authentication be added in the future?** Maybe. It's not currently a priority, but if there's enough demand for it, we may consider building in an optional authentication layer in a future release to support uses cases where multiple users need access to the same instance but with different permission levels (e.g. family use with parental controls, classroom use with teacher/admin accounts, etc.). For now, we recommend using network-level controls to manage access if you're planning to expose your N.O.M.A.D. instance to other devices on a local network. N.O.M.A.D. is not designed to be exposed directly to the internet, and we strongly advise against doing so unless you really know what you're doing, have taken appropriate security measures, and understand the risks involved.
## Contributing
Contributions are welcome and appreciated! Please read this section fully to understand how to contribute to the project.
Contributions are welcome and appreciated! Please read the [Contributing Guide](CONTRIBUTING.md) for details.
### General Guidelines
- **Open an issue first**: Before starting work on a new feature or bug fix, please open an issue to discuss your proposed changes. This helps ensure that your contribution aligns with the project's goals and avoids duplicate work. Title the issue clearly and provide a detailed description of the problem or feature you want to work on.
- **Fork the repository**: Click the "Fork" button at the top right of the repository page to create a copy of the project under your GitHub account.
- **Create a new branch**: In your forked repository, create a new branch for your work. Use a descriptive name for the branch that reflects the purpose of your changes (e.g., `fix/issue-123` or `feature/add-new-tool`).
- **Make your changes**: Implement your changes in the new branch. Follow the existing code style and conventions used in the project. Be sure to test your changes locally to ensure they work as expected.
- **Add Release Notes**: If your changes include new features, bug fixes, or improvements, please see the "Release Notes" section below to properly document your contribution for the next release.
- **Conventional Commits**: When committing your changes, please use conventional commit messages to provide clear and consistent commit history. The format is `<type>(<scope>): <description>`, where:
- `type` is the type of change (e.g., `feat` for new features, `fix` for bug fixes, `docs` for documentation changes, etc.)
- `scope` is an optional area of the codebase that your change affects (e.g., `api`, `ui`, `docs`, etc.)
- `description` is a brief summary of the change
- **Submit a pull request**: Once your changes are ready, submit a pull request to the main repository. Provide a clear description of your changes and reference any related issues. The project maintainers will review your pull request and may provide feedback or request changes before it can be merged.
- **Be responsive to feedback**: If the maintainers request changes or provide feedback on your pull request, please respond in a timely manner. Stale pull requests may be closed if there is no activity for an extended period.
- **Follow the project's code of conduct**: Please adhere to the project's code of conduct when interacting with maintainers and other contributors. Be respectful and considerate in your communications.
- **No guarantee of acceptance**: The project is community-driven, and all contributions are appreciated, but acceptance is not guaranteed. The maintainers will evaluate each contribution based on its quality, relevance, and alignment with the project's goals.
- **Thank you for contributing to Project N.O.M.A.D.!** Your efforts help make this project better for everyone.
### Versioning
This project uses semantic versioning. The version is managed in the root `package.json`
and automatically updated by semantic-release. For simplicity's sake, the "project-nomad" image
uses the same version defined there instead of the version in `admin/package.json` (stays at 0.0.0), as it's the only published image derived from the code.
### Release Notes
Human-readable release notes live in [`admin/docs/release-notes.md`](admin/docs/release-notes.md) and are displayed in the Command Center's built-in documentation.
When working on changes, add a summary to the `## Unreleased` section at the top of that file under the appropriate heading:
- **Features** — new user-facing capabilities
- **Bug Fixes** — corrections to existing behavior
- **Improvements** — enhancements, refactors, docs, or dependency updates
Use the format `- **Area**: Description` to stay consistent with existing entries. When a release is triggered, CI automatically stamps the version and date, commits the update, and pushes the content to the GitHub release.
This project uses semantic versioning. The version is managed in the root `package.json`
and automatically updated by semantic-release.
## Community & Resources
- **Website:** [www.projectnomad.us](https://www.projectnomad.us) - Learn more about the project
- **Discord:** [Join the Community](https://discord.com/invite/crosstalksolutions) - Get help, share your builds, and connect with other NOMAD users
- **Benchmark Leaderboard:** [benchmark.projectnomad.us](https://benchmark.projectnomad.us) - See how your hardware stacks up against other NOMAD builds
- **Website:** [www.projectnomad.us](https://www.projectnomad.us)
- **Discord:** [Join the Community](https://discord.com/invite/crosstalksolutions)
- **Benchmark Leaderboard:** [benchmark.projectnomad.us](https://benchmark.projectnomad.us)
## License
Project N.O.M.A.D. is licensed under the [Apache License 2.0](LICENSE).
## Helper Scripts
Once installed, Project N.O.M.A.D. has a few helper scripts should you ever need to troubleshoot issues or perform maintenance that can't be done through the Command Center. All of these scripts are found in Project N.O.M.A.D.'s install directory, `/opt/project-nomad`
###
###### Start Script - Starts all installed project containers
```bash
sudo bash /opt/project-nomad/start_nomad.sh
```
###
###### Stop Script - Stops all installed project containers
```bash
sudo bash /opt/project-nomad/stop_nomad.sh
```
###
###### Update Script - Attempts to pull the latest images for the Command Center and its dependencies (i.e. mysql) and recreate the containers. Note: this *only* updates the Command Center containers. It does not update the installable application containers - that should be done through the Command Center UI
```bash
sudo bash /opt/project-nomad/update_nomad.sh
```
###### Uninstall Script - Need to start fresh? Use the uninstall script to make your life easy. Note: this cannot be undone!
```bash
curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/uninstall_nomad.sh -o uninstall_nomad.sh && sudo bash uninstall_nomad.sh
```
Project N.O.M.A.D. is licensed under the [Apache License 2.0](LICENSE).

44
agent/Dockerfile Normal file
View File

@ -0,0 +1,44 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# Lightweight Monitoring Agent
# =============================================================================
# A minimal monitoring agent that collects system metrics and reports
# them to the Nomad server. Designed for low resource usage.
#
# Build: docker build -t nomad-agent ./agent
# Run: docker run -d --name nomad-agent \
# -e NOMAD_SERVER_URL=http://your-nomad-server:8080 \
# -e AGENT_SECRET=your-secret \
# -v /var/run/docker.sock:/var/run/docker.sock:ro \
# -v /proc:/host/proc:ro \
# -v /sys:/host/sys:ro \
# nomad-agent
# =============================================================================
FROM node:22-alpine
RUN apk add --no-cache curl
WORKDIR /agent
COPY package.json ./
RUN npm ci --omit=dev 2>/dev/null || npm init -y
COPY agent.mjs ./
COPY healthcheck.mjs ./
# Run as non-root
RUN addgroup -g 1001 -S nomad && \
adduser -S nomad -u 1001 -G nomad
USER nomad
ENV NODE_ENV=production
ENV AGENT_PORT=9100
ENV COLLECT_INTERVAL=30
EXPOSE 9100
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD node healthcheck.mjs || exit 1
CMD ["node", "agent.mjs"]

308
agent/agent.mjs Normal file
View File

@ -0,0 +1,308 @@
#!/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 using Node.js http module */
async function collectDockerMetrics() {
const containers = []
try {
const socketPath = '/var/run/docker.sock'
if (!existsSync(socketPath)) return containers
// Use Node.js http module for Unix socket support (fetch API does not support socketPath)
const { request } = await import('node:http')
const data = await new Promise((resolve, reject) => {
const req = request({ socketPath, path: '/containers/json?all=true', method: 'GET' }, (res) => {
let body = ''
res.on('data', (chunk) => { body += chunk })
res.on('end', () => {
try { resolve(JSON.parse(body)) } catch { resolve([]) }
})
})
req.on('error', () => resolve([]))
req.setTimeout(5000, () => { req.destroy(); resolve([]) })
req.end()
})
for (const container of data) {
containers.push({
id: container.Id?.substring(0, 12),
name: container.Names?.[0]?.replace(/^\//, ''),
image: container.Image,
state: container.State,
status: container.Status,
})
}
} catch {
// Docker metrics may not be available
}
return containers
}
/** Collect all metrics */
async function collectAllMetrics() {
const [dockerContainers] = await Promise.all([collectDockerMetrics()])
return {
timestamp: new Date().toISOString(),
node: {
name: config.nodeName,
platform: platform(),
arch: arch(),
release: release(),
uptime: Math.floor(uptime()),
},
cpu: collectCpuMetrics(),
memory: collectMemoryMetrics(),
disks: collectDiskMetrics(),
network: collectNetworkMetrics(),
docker: {
containers: dockerContainers,
containerCount: dockerContainers.length,
},
}
}
// ---------------------------------------------------------------------------
// Metrics Reporting
// ---------------------------------------------------------------------------
/** Send metrics to Nomad server */
async function reportMetrics(metrics) {
if (!config.serverUrl) return
try {
const url = `${config.serverUrl}/api/agent/report`
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.agentSecret}`,
},
body: JSON.stringify(metrics),
signal: AbortSignal.timeout(10000),
})
} catch {
// Silently retry on next interval
}
}
// ---------------------------------------------------------------------------
// Prometheus Metrics Endpoint
// ---------------------------------------------------------------------------
/** Format metrics as Prometheus exposition format */
function formatPrometheusMetrics(metrics) {
const lines = []
// CPU metrics
lines.push('# HELP nomad_agent_cpu_usage_percent CPU usage percentage')
lines.push('# TYPE nomad_agent_cpu_usage_percent gauge')
lines.push(`nomad_agent_cpu_usage_percent{node="${metrics.node.name}"} ${metrics.cpu.usagePercent}`)
lines.push('# HELP nomad_agent_cpu_count Number of CPU cores')
lines.push('# TYPE nomad_agent_cpu_count gauge')
lines.push(`nomad_agent_cpu_count{node="${metrics.node.name}"} ${metrics.cpu.count}`)
// Memory metrics
lines.push('# HELP nomad_agent_memory_total_bytes Total memory in bytes')
lines.push('# TYPE nomad_agent_memory_total_bytes gauge')
lines.push(`nomad_agent_memory_total_bytes{node="${metrics.node.name}"} ${metrics.memory.totalBytes}`)
lines.push('# HELP nomad_agent_memory_used_bytes Used memory in bytes')
lines.push('# TYPE nomad_agent_memory_used_bytes gauge')
lines.push(`nomad_agent_memory_used_bytes{node="${metrics.node.name}"} ${metrics.memory.usedBytes}`)
lines.push('# HELP nomad_agent_memory_usage_percent Memory usage percentage')
lines.push('# TYPE nomad_agent_memory_usage_percent gauge')
lines.push(`nomad_agent_memory_usage_percent{node="${metrics.node.name}"} ${metrics.memory.usagePercent}`)
// Uptime
lines.push('# HELP nomad_agent_uptime_seconds System uptime in seconds')
lines.push('# TYPE nomad_agent_uptime_seconds gauge')
lines.push(`nomad_agent_uptime_seconds{node="${metrics.node.name}"} ${metrics.node.uptime}`)
// Docker containers
lines.push('# HELP nomad_agent_docker_containers Number of Docker containers')
lines.push('# TYPE nomad_agent_docker_containers gauge')
lines.push(`nomad_agent_docker_containers{node="${metrics.node.name}"} ${metrics.docker.containerCount}`)
for (const container of metrics.docker.containers) {
const running = container.state === 'running' ? 1 : 0
lines.push(`nomad_agent_docker_container_running{node="${metrics.node.name}",name="${container.name}",image="${container.image}"} ${running}`)
}
return lines.join('\n') + '\n'
}
// ---------------------------------------------------------------------------
// HTTP Server
// ---------------------------------------------------------------------------
let latestMetrics = null
const server = createServer(async (req, res) => {
if (req.url === '/health' || req.url === '/healthz') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'healthy', node: config.nodeName }))
return
}
if (req.url === '/metrics') {
const metrics = latestMetrics || (await collectAllMetrics())
res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8' })
res.end(formatPrometheusMetrics(metrics))
return
}
if (req.url === '/api/metrics') {
const metrics = latestMetrics || (await collectAllMetrics())
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(metrics))
return
}
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Not found' }))
})
// ---------------------------------------------------------------------------
// Main Loop
// ---------------------------------------------------------------------------
async function collectAndReport() {
try {
latestMetrics = await collectAllMetrics()
await reportMetrics(latestMetrics)
} catch (err) {
console.error('Collection error:', err.message)
}
}
server.listen(config.port, '0.0.0.0', () => {
console.log(`[nomad-agent] Node: ${config.nodeName}`)
console.log(`[nomad-agent] Metrics server listening on port ${config.port}`)
console.log(`[nomad-agent] Collection interval: ${config.collectInterval / 1000}s`)
if (config.serverUrl) {
console.log(`[nomad-agent] Reporting to: ${config.serverUrl}`)
} else {
console.log('[nomad-agent] No NOMAD_SERVER_URL set — metrics available via /metrics endpoint only')
}
// Initial collection
collectAndReport()
// Periodic collection
setInterval(collectAndReport, config.collectInterval)
})

14
agent/healthcheck.mjs Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
// Simple healthcheck for the Nomad agent
const port = process.env.AGENT_PORT || 9100
try {
const res = await fetch(`http://localhost:${port}/health`)
if (res.ok) {
process.exit(0)
}
process.exit(1)
} catch {
process.exit(1)
}

11
agent/package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "nomad-agent",
"version": "1.0.0",
"description": "Project N.O.M.A.D. lightweight monitoring agent",
"type": "module",
"main": "agent.mjs",
"scripts": {
"start": "node agent.mjs"
},
"license": "Apache-2.0"
}

201
docker-compose.yml Normal file
View File

@ -0,0 +1,201 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# Network Operations Monitoring and Automation Dashboard
# Docker Compose Stack for NAS and Homelab Environments
# =============================================================================
#
# Usage:
# 1. Copy .env.example to .env and configure
# 2. docker compose up -d
#
# Compatible with: Unraid, TrueNAS SCALE, standard Docker hosts
# =============================================================================
name: project-nomad
services:
# ---------------------------------------------------------------------------
# Nomad Application (Web UI + API)
# ---------------------------------------------------------------------------
nomad-app:
image: ghcr.io/crosstalk-solutions/project-nomad:latest
container_name: nomad-app
restart: unless-stopped
ports:
- "${PORT:-8080}:8080"
volumes:
- ${NOMAD_DATA_DIR:-./data}/storage:/app/storage
- /var/run/docker.sock:/var/run/docker.sock
- ./install/entrypoint.sh:/usr/local/bin/entrypoint.sh:ro
environment:
- NODE_ENV=${NODE_ENV:-production}
- PORT=8080
- HOST=0.0.0.0
- LOG_LEVEL=${LOG_LEVEL:-info}
- APP_KEY=${APP_KEY:?Set APP_KEY in .env}
- URL=${URL:-http://localhost:8080}
- DB_HOST=nomad-database
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-nomad}
- DB_USER=${DB_USER:-nomad}
- DB_PASSWORD=${DB_PASSWORD:?Set DB_PASSWORD in .env}
- DB_SSL=false
- REDIS_HOST=nomad-cache
- REDIS_PORT=6379
- NOMAD_STORAGE_PATH=/app/storage
- NOMAD_API_URL=${NOMAD_API_URL:-}
- INTERNET_STATUS_TEST_URL=${INTERNET_STATUS_TEST_URL:-}
entrypoint: ["/usr/local/bin/entrypoint.sh"]
depends_on:
nomad-database:
condition: service_healthy
nomad-cache:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- nomad-internal
labels:
- "com.project-nomad.service=app"
- "com.project-nomad.description=Nomad Web Application"
# ---------------------------------------------------------------------------
# Nomad Worker (Background Job Processing)
# ---------------------------------------------------------------------------
nomad-worker:
image: ghcr.io/crosstalk-solutions/project-nomad:latest
container_name: nomad-worker
restart: unless-stopped
volumes:
- ${NOMAD_DATA_DIR:-./data}/storage:/app/storage
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=${NODE_ENV:-production}
- PORT=8081
- HOST=0.0.0.0
- LOG_LEVEL=${LOG_LEVEL:-info}
- APP_KEY=${APP_KEY}
- URL=${URL:-http://localhost:8080}
- DB_HOST=nomad-database
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-nomad}
- DB_USER=${DB_USER:-nomad}
- DB_PASSWORD=${DB_PASSWORD}
- DB_SSL=false
- REDIS_HOST=nomad-cache
- REDIS_PORT=6379
- NOMAD_STORAGE_PATH=/app/storage
command: ["node", "ace", "queue:work", "--all"]
depends_on:
nomad-database:
condition: service_healthy
nomad-cache:
condition: service_healthy
networks:
- nomad-internal
deploy:
resources:
limits:
memory: 2G
labels:
- "com.project-nomad.service=worker"
- "com.project-nomad.description=Nomad Background Worker"
# ---------------------------------------------------------------------------
# Database (MySQL 8.0)
# ---------------------------------------------------------------------------
nomad-database:
image: mysql:8.0
container_name: nomad-database
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}
- MYSQL_DATABASE=${DB_DATABASE:-nomad}
- MYSQL_USER=${DB_USER:-nomad}
- MYSQL_PASSWORD=${DB_PASSWORD}
volumes:
- nomad-db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
networks:
- nomad-internal
deploy:
resources:
limits:
memory: 1G
labels:
- "com.project-nomad.service=database"
- "com.project-nomad.description=Nomad MySQL Database"
# ---------------------------------------------------------------------------
# Cache / Queue (Redis 7)
# ---------------------------------------------------------------------------
nomad-cache:
image: redis:7-alpine
container_name: nomad-cache
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- ${NOMAD_DATA_DIR:-./data}/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 15s
timeout: 5s
retries: 5
networks:
- nomad-internal
deploy:
resources:
limits:
memory: 512M
labels:
- "com.project-nomad.service=cache"
- "com.project-nomad.description=Nomad Redis Cache"
# ---------------------------------------------------------------------------
# Nginx Reverse Proxy
# ---------------------------------------------------------------------------
nomad-nginx:
image: nginx:alpine
container_name: nomad-nginx
restart: unless-stopped
ports:
- "${NGINX_HTTP_PORT:-80}:80"
- "${NGINX_HTTPS_PORT:-443}:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ${NOMAD_DATA_DIR:-./data}/logs/nginx:/var/log/nginx
depends_on:
nomad-app:
condition: service_healthy
networks:
- nomad-internal
labels:
- "com.project-nomad.service=nginx"
- "com.project-nomad.description=Nomad Reverse Proxy"
# =============================================================================
# Networks
# =============================================================================
networks:
nomad-internal:
driver: bridge
name: nomad-internal
# =============================================================================
# Volumes
# =============================================================================
# Database uses a named Docker volume for optimal I/O performance.
# This avoids NFS/SMB latency issues common on NAS systems.
# All other data uses bind mounts configured via NOMAD_DATA_DIR.
volumes:
nomad-db-data:
driver: local

146
docs/homelab/agent-guide.md Normal file
View File

@ -0,0 +1,146 @@
# Project N.O.M.A.D. — Agent Installation Guide
## Overview
The Nomad monitoring agent is a lightweight container that collects system metrics from remote homelab nodes and reports them to the Nomad server. It exposes a Prometheus-compatible `/metrics` endpoint.
## Features
- **System metrics**: CPU usage, memory, disk, network
- **Docker monitoring**: Container status, running/stopped counts
- **Prometheus endpoint**: Native `/metrics` output for scraping
- **Low resource usage**: ~20MB RAM, minimal CPU
- **Auto-reporting**: Sends metrics to Nomad server on configurable interval
## Quick Start
### Docker (Recommended)
```bash
docker run -d \
--name nomad-agent \
--restart unless-stopped \
-p 9100:9100 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-e NOMAD_SERVER_URL=http://your-nomad-server:8080 \
-e AGENT_SECRET=your-shared-secret \
-e NODE_NAME=my-server \
ghcr.io/docwatz/nomad-agent:latest
```
### Docker Compose
Add to your existing `docker-compose.yml` or create a new one:
```yaml
services:
nomad-agent:
build: ./agent
container_name: nomad-agent
restart: unless-stopped
ports:
- "9100:9100"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /proc:/host/proc:ro
- /sys:/host/sys:ro
environment:
- NOMAD_SERVER_URL=http://your-nomad-server:8080
- AGENT_SECRET=your-shared-secret
- NODE_NAME=my-server
- COLLECT_INTERVAL=30
```
### Build from Source
```bash
cd agent/
docker build -t nomad-agent .
```
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `AGENT_PORT` | 9100 | Metrics server port |
| `COLLECT_INTERVAL` | 30 | Collection interval in seconds |
| `NOMAD_SERVER_URL` | — | Nomad server URL for reporting |
| `AGENT_SECRET` | — | Shared secret for authentication |
| `NODE_NAME` | hostname | Display name for this node |
| `HOST_PROC` | /host/proc | Path to host /proc mount |
| `HOST_SYS` | /host/sys | Path to host /sys mount |
## API Endpoints
| Endpoint | Format | Description |
|----------|--------|-------------|
| `GET /health` | JSON | Health check |
| `GET /metrics` | Prometheus | Prometheus exposition format |
| `GET /api/metrics` | JSON | Full metrics as JSON |
## Prometheus Integration
Add the agent as a scrape target in your `prometheus.yml`:
```yaml
scrape_configs:
- job_name: 'nomad-agent'
static_configs:
- targets:
- 'server1:9100'
- 'server2:9100'
- 'nas:9100'
scrape_interval: 30s
```
### Available Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `nomad_agent_cpu_usage_percent` | gauge | CPU usage percentage |
| `nomad_agent_cpu_count` | gauge | Number of CPU cores |
| `nomad_agent_memory_total_bytes` | gauge | Total memory |
| `nomad_agent_memory_used_bytes` | gauge | Used memory |
| `nomad_agent_memory_usage_percent` | gauge | Memory usage percentage |
| `nomad_agent_uptime_seconds` | gauge | System uptime |
| `nomad_agent_docker_containers` | gauge | Total Docker containers |
| `nomad_agent_docker_container_running` | gauge | Per-container running status |
## Security
- The agent runs as a non-root user inside the container
- Docker socket is mounted read-only
- Communication with the Nomad server uses a shared secret via `Authorization: Bearer` header
- For production use, place behind a TLS-terminating reverse proxy
## Resource Usage
| Metric | Value |
|--------|-------|
| RAM | ~15-25 MB |
| CPU | < 1% (idle) |
| Image size | ~60 MB |
| Network | ~1 KB per report |
## Deploying on Multiple Nodes
Deploy an agent on each homelab node:
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Server 1 │ │ Server 2 │ │ NAS │
│ nomad-agent │ │ nomad-agent │ │ nomad-agent │
│ :9100 │ │ :9100 │ │ :9100 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
┌──────▼──────┐
│ Nomad Server│
│ :8080 │
└─────────────┘
```
Each agent auto-registers with the Nomad server using `NODE_NAME` and begins sending telemetry at the configured interval.

View File

@ -0,0 +1,218 @@
# Project N.O.M.A.D. — Homelab Edition: Architecture
## System Overview
Project N.O.M.A.D. Homelab Edition is a container-native knowledge platform designed for NAS and homelab environments. The architecture prioritizes reliability, minimal resource usage, and compatibility with storage-backed systems.
## Container Stack
```
┌─────────────────────────────────────────────────────────────────┐
│ Docker Host / NAS │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ nomad-internal network │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ nomad-nginx │ │ nomad-app │ │nomad-worker │ │ │
│ │ │ (Nginx) ├───►│ (AdonisJS) │ │ (Queue Jobs)│ │ │
│ │ │ :80/:443 │ │ :8080 │ │ │ │ │
│ │ └──────────────┘ └──────┬───────┘ └──────┬──────┘ │ │
│ │ │ │ │ │
│ │ ┌──────▼───────┐ ┌──────▼──────┐ │ │
│ │ │nomad-database│ │ nomad-cache │ │ │
│ │ │ (MySQL 8.0) │ │ (Redis 7) │ │ │
│ │ │ :3306 │ │ :6379 │ │ │
│ │ └──────────────┘ └─────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ Volumes: │
│ ├── nomad-db-data (Docker volume - local SSD) │
│ ├── NOMAD_DATA_DIR/storage (bind mount - NAS share) │
│ ├── NOMAD_DATA_DIR/redis (bind mount) │
│ └── NOMAD_DATA_DIR/logs/nginx (bind mount) │
└─────────────────────────────────────────────────────────────────┘
```
## Service Roles
### nomad-app (Application Server)
- **Technology:** Node.js 22 + AdonisJS 6
- **Role:** Serves the web UI (React/Inertia), handles API requests, manages content
- **Port:** 8080
- **Dependencies:** nomad-database, nomad-cache
### nomad-worker (Background Worker)
- **Technology:** Same image as nomad-app
- **Role:** Processes background jobs — content downloads, AI model downloads, embeddings, benchmarks
- **Queues:** downloads, model-downloads, benchmarks, embeddings
- **Dependencies:** nomad-database, nomad-cache
### nomad-database (Database)
- **Technology:** MySQL 8.0
- **Role:** Persistent storage for services, content metadata, chat sessions, benchmarks, settings
- **Storage:** Docker named volume (optimized for I/O)
### nomad-cache (Cache / Queue Broker)
- **Technology:** Redis 7 Alpine
- **Role:** BullMQ job queues, session cache, real-time event transport
- **Config:** AOF persistence, 256MB max memory with LRU eviction
### nomad-nginx (Reverse Proxy)
- **Technology:** Nginx Alpine
- **Role:** TLS termination, request routing, static asset caching, WebSocket proxy
- **Ports:** 80 (HTTP), 443 (HTTPS)
## Data Flow
### Content Download Pipeline
```
User Request
nomad-app (API) ──► nomad-cache (Redis Queue)
nomad-worker
Downloads content
/app/storage/
(NAS bind mount)
```
### AI Chat Pipeline
```
User Message
nomad-app ──► Ollama (AI Model)
│ │
│ ▼
│ Response + RAG context
│ │
▼ ▼
nomad-database (Chat History)
```
### Real-Time Updates
```
nomad-worker ──► nomad-cache (Redis Pub/Sub)
nomad-app (Transmit SSE)
Browser (EventSource)
```
## Storage Architecture
### Design Principles
1. **Database on local volume** — Docker named volume for high-IOPS MySQL operations
2. **Content on NAS share** — Bind mount to NAS storage for large files (ZIM, maps, PDFs)
3. **Logs rotated automatically** — Nginx logs on bind mount, easily accessible
### Storage Tiers
| Tier | Type | Use Case | I/O Profile |
|------|------|----------|-------------|
| **Hot** | Docker named volume | MySQL database | High IOPS, small writes |
| **Warm** | Bind mount (SSD/cache) | Redis, temp files | Medium IOPS |
| **Cold** | Bind mount (NAS array) | Content library, backups | Sequential reads, large files |
### NAS Compatibility
| Platform | Storage Path | Notes |
|----------|-------------|-------|
| **Unraid** | `/mnt/user/appdata/project-nomad` | Uses cache drive for DB |
| **TrueNAS SCALE** | `/mnt/pool/apps/project-nomad` | ZFS dataset recommended |
| **Linux** | `/opt/project-nomad` | Standard filesystem |
| **Synology** | `/volume1/docker/project-nomad` | Btrfs volume |
## Network Architecture
### Internal Network
All services communicate over the `nomad-internal` bridge network. Only the following ports are exposed to the host:
| Port | Service | Purpose |
|------|---------|---------|
| 80 | nomad-nginx | HTTP (configurable) |
| 443 | nomad-nginx | HTTPS (configurable) |
| 8080 | nomad-app | Direct app access (optional) |
### Reverse Proxy Compatibility
The stack works behind external reverse proxies:
```
Internet/LAN
┌───────────────────┐
│ External Proxy │ Nginx Proxy Manager / Traefik / Caddy
│ (TLS termination) │
└─────────┬─────────┘
┌─────────────────────┐
│ nomad-nginx (:80) │ OR directly to nomad-app (:8080)
└─────────┬───────────┘
┌─────────────────────┐
│ nomad-app (:8080) │
└─────────────────────┘
```
When using an external reverse proxy, you can disable `nomad-nginx`:
```yaml
# In docker-compose.yml override
services:
nomad-nginx:
profiles: ["proxy"] # Only starts with --profile proxy
```
## Resource Requirements
### Minimum (8 GB RAM system)
| Service | RAM | CPU |
|---------|-----|-----|
| nomad-app | 256 MB | 0.25 cores |
| nomad-worker | 256 MB | 0.1 cores |
| nomad-database | 256 MB | 0.25 cores |
| nomad-cache | 64 MB | 0.05 cores |
| nomad-nginx | 16 MB | 0.01 cores |
| **Total** | **~850 MB** | **~0.66 cores** |
### Recommended (32 GB RAM system)
| Service | RAM | CPU |
|---------|-----|-----|
| nomad-app | 1 GB | 1 core |
| nomad-worker | 2 GB | 1 core |
| nomad-database | 1 GB | 0.5 cores |
| nomad-cache | 256 MB | 0.1 cores |
| nomad-nginx | 32 MB | 0.05 cores |
| **Total** | **~3.3 GB** | **~2.65 cores** |
## Security Model
- No authentication by default (network-level access control recommended)
- All inter-service communication on private Docker network
- Database and Redis not exposed to host by default
- Docker socket mounted read-only where possible
- Agent communication via shared secret (Bearer token)

View File

@ -0,0 +1,251 @@
# Project N.O.M.A.D. — Docker Compose Deployment Guide
## Overview
This guide covers deploying Project N.O.M.A.D. Homelab Edition using Docker Compose on any Linux host, VM, or NAS system.
## Prerequisites
- Docker 20.10+
- Docker Compose v2+ (comes with Docker Desktop or `docker compose` plugin)
- 4 GB RAM minimum
- 5 GB free disk space
### Verify Docker Installation
```bash
docker --version # Docker 20.10+
docker compose version # Docker Compose v2+
```
## Deployment
### Step 1: Get the Files
```bash
git clone https://github.com/DocwatZ/project-nomad-homelab-edition.git
cd project-nomad-homelab-edition
```
### Step 2: Configure Environment
```bash
cp .env.example .env
```
Generate secure credentials:
```bash
# Generate application key
APP_KEY=$(openssl rand -hex 32)
sed -i "s/^APP_KEY=replaceme/APP_KEY=$APP_KEY/" .env
# Generate database password
DB_PASS=$(openssl rand -base64 24)
sed -i "s/^DB_PASSWORD=replaceme/DB_PASSWORD=$DB_PASS/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=replaceme/MYSQL_ROOT_PASSWORD=$DB_PASS/" .env
```
Configure the external URL:
```bash
# Replace with your server IP or domain
sed -i "s|^URL=.*|URL=http://$(hostname -I | awk '{print $1}'):8080|" .env
```
### Step 3: Create Data Directories
```bash
NOMAD_DIR=$(grep NOMAD_DATA_DIR .env | cut -d= -f2)
sudo mkdir -p ${NOMAD_DIR}/{storage,redis,logs/nginx}
sudo chown -R $(id -u):$(id -g) ${NOMAD_DIR}
```
### Step 4: Launch
```bash
docker compose up -d
```
First launch takes 1-3 minutes for database initialization and migrations.
### Step 5: Verify
```bash
# Check all services are running
docker compose ps
# Check application health
curl -s http://localhost:8080/api/health
# View logs
docker compose logs -f nomad-app
```
## Service Management
### Start / Stop / Restart
```bash
# Stop all services
docker compose down
# Start all services
docker compose up -d
# Restart a specific service
docker compose restart nomad-app
# View logs
docker compose logs -f
docker compose logs -f nomad-app
```
### Update to Latest Version
```bash
docker compose pull
docker compose up -d
```
### Full Reset (Destroys Data)
```bash
docker compose down -v
sudo rm -rf /opt/project-nomad/*
docker compose up -d
```
## Customization
### Disable Nginx Proxy
If you already have a reverse proxy (Traefik, Nginx Proxy Manager, Caddy), you can skip the built-in Nginx:
```bash
# Start without nginx
docker compose up -d nomad-app nomad-worker nomad-database nomad-cache
```
Access the app directly on port 8080.
### Disable Worker (Lightweight Mode)
For minimal resource usage, you can run without the dedicated worker. The app will process jobs inline:
```bash
docker compose up -d nomad-app nomad-database nomad-cache
```
> Note: Background downloads and AI features may be slower without the worker.
### Custom Port
Edit `.env`:
```
PORT=9090
```
### Custom Storage Location
Edit `.env`:
```
NOMAD_DATA_DIR=/mnt/my-nas-share/project-nomad
```
## Compose File Structure
The `docker-compose.yml` defines five services:
```
docker-compose.yml
├── nomad-app # Web application (port 8080)
├── nomad-worker # Background job processor
├── nomad-database # MySQL 8.0 database
├── nomad-cache # Redis 7 cache/queue
└── nomad-nginx # Nginx reverse proxy (port 80/443)
```
### Override File
Create a `docker-compose.override.yml` for local customizations:
```yaml
services:
nomad-app:
# Add extra environment variables
environment:
- NOMAD_API_URL=https://api.projectnomad.io
# Add extra volumes
volumes:
- /mnt/nas-share/content:/app/storage/content:ro
```
## Monitoring
### Health Checks
All services include health checks. View status:
```bash
docker compose ps
```
### Resource Usage
```bash
docker stats --no-stream
```
### Prometheus Metrics
Deploy the monitoring agent for Prometheus-compatible metrics:
```bash
docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d
```
See [Monitoring Architecture](./monitoring.md) for details.
## Troubleshooting
### Service Won't Start
```bash
# Check logs for the failing service
docker compose logs nomad-app
docker compose logs nomad-database
# Check if ports are in use
ss -tlnp | grep -E '(8080|3306|6379|80)'
```
### Database Connection Error
The app waits for the database health check. If it times out:
```bash
# Check database status
docker compose exec nomad-database mysqladmin ping -h localhost
# Check database logs
docker compose logs nomad-database
```
### Permission Denied on Volumes
```bash
sudo chown -R 1000:1000 /opt/project-nomad/storage
```
### Out of Disk Space
```bash
# Check Docker disk usage
docker system df
# Clean up unused images
docker system prune -a
```

View File

@ -0,0 +1,163 @@
# Project N.O.M.A.D. — Homelab Edition: Installation Guide
## Overview
Project N.O.M.A.D. Homelab Edition runs as a Docker Compose stack, making it compatible with any system that supports Docker — including NAS platforms like Unraid and TrueNAS SCALE.
## Prerequisites
- **Docker** 20.10+ and **Docker Compose** v2+
- **4 GB RAM** minimum (8 GB+ recommended)
- **5 GB** free disk space (more for content downloads)
- Network access to pull Docker images
## Quick Start
### 1. Clone or Download
```bash
git clone https://github.com/DocwatZ/project-nomad-homelab-edition.git
cd project-nomad-homelab-edition
```
Or download and extract the ZIP from the GitHub releases page.
### 2. Configure Environment
```bash
# Copy the example environment file
cp .env.example .env
# Generate a secure application key
APP_KEY=$(openssl rand -hex 32)
# Generate a secure database password
DB_PASS=$(openssl rand -base64 24)
# Update .env with generated values
sed -i "s/^APP_KEY=replaceme/APP_KEY=$APP_KEY/" .env
sed -i "s/^DB_PASSWORD=replaceme/DB_PASSWORD=$DB_PASS/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=replaceme/MYSQL_ROOT_PASSWORD=$DB_PASS/" .env
```
Edit `.env` to set your external URL:
```bash
# Set to your server's IP or domain
URL=http://192.168.1.100:8080
```
### 3. Create Data Directories
```bash
# Default location (or set NOMAD_DATA_DIR in .env)
sudo mkdir -p /opt/project-nomad/{storage,redis,logs/nginx}
sudo chown -R 1000:1000 /opt/project-nomad
```
### 4. Start the Stack
```bash
docker compose up -d
```
### 5. Access the Dashboard
Open your browser and navigate to the URL you configured (default: `http://localhost:8080`).
If using the Nginx proxy: `http://localhost` (port 80).
## Service Architecture
| Service | Container | Port | Purpose |
|---------|-----------|------|---------|
| **nomad-app** | Nomad application | 8080 | Web UI + API |
| **nomad-worker** | Background jobs | — | Queue processing |
| **nomad-database** | MySQL 8.0 | 3306 (internal) | Persistent data |
| **nomad-cache** | Redis 7 | 6379 (internal) | Cache + job queues |
| **nomad-nginx** | Nginx | 80, 443 | Reverse proxy |
## Volume Layout
```
NOMAD_DATA_DIR/
├── storage/ # Content files (ZIM, maps, uploads)
├── redis/ # Redis persistence
└── logs/
└── nginx/ # Nginx access/error logs
```
The MySQL database uses a Docker named volume (`nomad-db-data`) for optimal I/O performance. This avoids latency issues common with NFS/SMB-backed storage on NAS systems.
## Updating
```bash
# Pull latest images
docker compose pull
# Recreate containers with new images
docker compose up -d
```
## Stopping
```bash
docker compose down
```
To also remove data volumes:
```bash
docker compose down -v
```
## Configuration Reference
See `.env.example` for all available configuration options. Key settings:
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | 8080 | Application port |
| `APP_KEY` | — | Encryption key (required) |
| `URL` | http://localhost:8080 | External access URL |
| `DB_PASSWORD` | — | Database password (required) |
| `NOMAD_DATA_DIR` | /opt/project-nomad | Host data directory |
| `LOG_LEVEL` | info | Logging verbosity |
| `NGINX_HTTP_PORT` | 80 | Nginx HTTP port |
| `NGINX_HTTPS_PORT` | 443 | Nginx HTTPS port |
## Troubleshooting
### Container won't start
```bash
# Check container logs
docker compose logs nomad-app
# Verify database is healthy
docker compose ps nomad-database
```
### Database connection errors
Ensure the database is healthy before the app starts. The compose file handles this with health checks, but on slow systems you may need to wait longer:
```bash
# Check database health
docker compose exec nomad-database mysqladmin ping -h localhost
```
### Permission issues on NAS
Ensure the storage directories are writable by the container user (UID 1000):
```bash
sudo chown -R 1000:1000 /path/to/your/storage
```
## Next Steps
- [Unraid Installation Guide](./unraid-guide.md)
- [TrueNAS SCALE Installation Guide](./truenas-guide.md)
- [Agent Installation Guide](./agent-guide.md)
- [Architecture Overview](./architecture.md)

225
docs/homelab/monitoring.md Normal file
View File

@ -0,0 +1,225 @@
# Project N.O.M.A.D. — Monitoring Architecture
## Overview
The Nomad Homelab Edition includes built-in monitoring capabilities and integrates with standard homelab monitoring stacks.
## Monitoring Layers
```
┌─────────────────────────────────────────────────────────────┐
│ Nomad Dashboard │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ NAS │ │ Server │ │Container │ │ Network │ │
│ │ Health │ │ Metrics │ │ Status │ │ Devices │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────▼────────────▼────────────▼────────────▼─────┐ │
│ │ Nomad App (Aggregator) │ │
│ └──────────────────────┬──────────────────────────┘ │
└─────────────────────────┼──────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌──────▼──────┐
│ Agent 1 │ │ Agent 2 │ │ Agent N │
│ (Server) │ │ (NAS) │ │ (Remote) │
│ :9100 │ │ :9100 │ │ :9100 │
└───────────┘ └─────────┘ └─────────────┘
```
## Built-in Monitoring
### System Resource Monitoring
The Nomad application provides built-in system information through the system controller:
- **CPU**: Model, core count, usage
- **Memory**: Total, used, free, usage percentage
- **Disk**: Mount points, usage, filesystem types
- **Docker**: Container status, image versions, resource usage
### Docker Container Monitoring
Nomad monitors its own container stack and any Docker containers on the host via the Docker socket:
- Container health status
- Image versions and update availability
- Resource consumption
- Log access (via Dozzle integration in original install)
### Health Endpoints
| Endpoint | Service | Purpose |
|----------|---------|---------|
| `GET /api/health` | nomad-app | Application health |
| `GET /nginx-health` | nomad-nginx | Reverse proxy health |
| `GET /health` | nomad-agent | Agent health |
## Agent-Based Monitoring
### Architecture
The Nomad monitoring agent runs on remote homelab nodes and reports metrics via:
1. **Push model**: Agent sends JSON metrics to `POST /api/agent/report` on the Nomad server
2. **Pull model**: Prometheus scrapes the agent's `/metrics` endpoint
### Agent Metrics
```
nomad_agent_cpu_usage_percent - CPU utilization
nomad_agent_cpu_count - CPU core count
nomad_agent_memory_total_bytes - Total RAM
nomad_agent_memory_used_bytes - Used RAM
nomad_agent_memory_usage_percent - RAM utilization
nomad_agent_uptime_seconds - System uptime
nomad_agent_docker_containers - Docker container count
nomad_agent_docker_container_running - Per-container status
```
### Deployment
Deploy an agent on each node you want to monitor:
```bash
docker run -d --name nomad-agent \
--restart unless-stopped \
-p 9100:9100 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-e NOMAD_SERVER_URL=http://nomad-server:8080 \
-e NODE_NAME=$(hostname) \
nomad-agent
```
## Prometheus Integration
### Full Monitoring Stack
For a complete monitoring setup, add Prometheus and Grafana to your docker-compose:
```yaml
# docker-compose.monitoring.yml
services:
prometheus:
image: prom/prometheus:latest
container_name: nomad-prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
networks:
- nomad-internal
grafana:
image: grafana/grafana:latest
container_name: nomad-grafana
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
networks:
- nomad-internal
volumes:
prometheus-data:
grafana-data:
```
### Prometheus Configuration
```yaml
# monitoring/prometheus.yml
global:
scrape_interval: 30s
evaluation_interval: 30s
scrape_configs:
- job_name: 'nomad-agents'
static_configs:
- targets:
- 'nomad-agent:9100' # Local agent
- 'server2:9100' # Remote server
- 'nas:9100' # NAS agent
```
### Node Exporter (Optional)
For deeper host-level metrics, add the Prometheus Node Exporter alongside the Nomad agent:
```yaml
node-exporter:
image: prom/node-exporter:latest
container_name: nomad-node-exporter
restart: unless-stopped
ports:
- "9101:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
```
## Observability
### Structured Logging
The Nomad application uses structured JSON logging:
```json
{
"level": "info",
"timestamp": "2026-03-13T12:00:00.000Z",
"msg": "HTTP request completed",
"method": "GET",
"url": "/api/health",
"status": 200,
"duration": "12ms"
}
```
Configure log level via `LOG_LEVEL` environment variable: `debug`, `info`, `warn`, `error`.
### Log Access
```bash
# Application logs
docker compose logs -f nomad-app
# Worker logs
docker compose logs -f nomad-worker
# All service logs
docker compose logs -f
# Nginx access logs (on host)
tail -f ${NOMAD_DATA_DIR}/logs/nginx/access.log
```
### Alerting Recommendations
For homelab alerting, integrate with:
| Tool | Use Case |
|------|----------|
| **Uptime Kuma** | Service uptime monitoring |
| **Grafana Alerting** | Metric-based alerts |
| **Ntfy** | Push notifications |
| **Gotify** | Self-hosted notifications |
Example Uptime Kuma monitor:
- **URL**: `http://nomad-app:8080/api/health`
- **Interval**: 60 seconds
- **Expected status**: 200

View File

@ -0,0 +1,191 @@
# Project N.O.M.A.D. — TrueNAS SCALE Installation Guide
## Overview
TrueNAS SCALE supports both Docker Compose (via custom app) and Helm charts for application deployment. This guide covers both methods.
## Method 1: Docker Compose via Custom App (Recommended)
TrueNAS SCALE Electric Eel (24.10+) supports Docker Compose as a custom app deployment method.
### Prerequisites
- TrueNAS SCALE 24.10+ (Electric Eel or newer)
- A dataset for application data
### Steps
#### 1. Create a Dataset
1. Go to **Storage** → **Pools**
2. Create a new dataset: `project-nomad`
- Path: `/mnt/pool/apps/project-nomad`
- Record Size: 128K
- Compression: LZ4
#### 2. Create Subdirectories
Open a TrueNAS shell:
```bash
mkdir -p /mnt/pool/apps/project-nomad/{storage,redis,logs/nginx,config}
```
#### 3. Deploy as Custom App
1. Go to **Apps****Discover Apps** → **Custom App**
2. Upload or paste the `docker-compose.yml` from this repository
3. Configure environment variables:
- `APP_KEY`: Generate with `openssl rand -hex 32`
- `DB_PASSWORD`: Set a secure password
- `MYSQL_ROOT_PASSWORD`: Same as DB_PASSWORD
- `URL`: `http://YOUR_TRUENAS_IP:8080`
- `NOMAD_DATA_DIR`: `/mnt/pool/apps/project-nomad`
#### 4. Start the Application
Click **Deploy** and wait for all services to start (first launch may take 2-3 minutes).
## Method 2: Helm Chart
### Prerequisites
- TrueNAS SCALE with Kubernetes enabled
- Helm CLI (if deploying manually)
### Steps
#### 1. Add the Chart
```bash
# Clone the repository
git clone https://github.com/DocwatZ/project-nomad-homelab-edition.git
cd project-nomad-homelab-edition/homelab/truenas
# Install with Helm
helm install project-nomad . \
--set app.appKey=$(openssl rand -hex 32) \
--set database.password=$(openssl rand -base64 24) \
--set database.rootPassword=$(openssl rand -base64 24) \
--set app.url=http://YOUR_TRUENAS_IP:8080 \
--set storage.data.hostPath=/mnt/pool/apps/project-nomad/storage
```
#### 2. Verify Deployment
```bash
helm status project-nomad
kubectl get pods -l app.kubernetes.io/instance=project-nomad
```
## TrueNAS-Specific Configuration
### Storage Best Practices
**ZFS Dataset Recommendations:**
| Dataset | Record Size | Compression | Purpose |
|---------|-------------|-------------|---------|
| `project-nomad/storage` | 1M | LZ4 | Large content files |
| `project-nomad/database` | 16K | LZ4 | MySQL data (if not using Docker volume) |
| `project-nomad/redis` | 128K | LZ4 | Redis persistence |
**Performance Tip:** Use a Docker named volume for the MySQL database (default in docker-compose.yml). Docker volumes on TrueNAS use the app pool, which typically has better I/O than network-accessed datasets.
### Network Configuration
TrueNAS SCALE apps run in an isolated network by default. To access Nomad from your LAN:
1. The compose file maps port 8080 to the host
2. Access via `http://YOUR_TRUENAS_IP:8080`
If using the Nginx proxy (port 80):
- Ensure port 80 isn't used by TrueNAS web UI
- Change `NGINX_HTTP_PORT` in `.env` if needed
### Permissions
TrueNAS uses ACLs for dataset permissions. Set the dataset owner to match the container user:
```bash
# Default container user is UID 1000. Verify with:
docker compose exec nomad-app id
# Set ownership accordingly
chown -R 1000:1000 /mnt/pool/apps/project-nomad/storage
```
Or configure ACLs in the TrueNAS UI:
1. Go to **Storage** → select your dataset → **Edit Permissions**
2. Set User to the UID shown by the command above (default: `1000`)
3. Set Group to the GID shown (default: `1000`)
4. Apply recursively
> **Note:** If your TrueNAS user mapping differs, adjust the UID/GID to match your container runtime configuration.
## Updating on TrueNAS
### Docker Compose Method
```bash
cd /mnt/pool/apps/project-nomad
docker compose pull
docker compose up -d
```
### Helm Method
```bash
helm upgrade project-nomad ./homelab/truenas \
--reuse-values
```
## Monitoring on TrueNAS
TrueNAS SCALE includes built-in reporting. You can supplement it with the Nomad monitoring agent:
```bash
docker run -d --name nomad-agent \
--restart unless-stopped \
-p 9100:9100 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-e NOMAD_SERVER_URL=http://nomad-app:8080 \
-e NODE_NAME=truenas \
nomad-agent
```
## Troubleshooting
### App Won't Start
```bash
# Check container status
docker compose ps
# View logs
docker compose logs nomad-app
docker compose logs nomad-database
```
### Database Issues
If the database fails to initialize:
```bash
# Check MySQL logs
docker compose logs nomad-database
# Verify the database volume
docker volume inspect project-nomad_nomad-db-data
```
### Port Conflicts
TrueNAS web UI uses ports 80/443 by default. Adjust in `.env`:
```
NGINX_HTTP_PORT=8081
NGINX_HTTPS_PORT=8443
```

View File

@ -0,0 +1,194 @@
# Project N.O.M.A.D. — Unraid Installation Guide
## Overview
This guide covers installing Project N.O.M.A.D. Homelab Edition on Unraid using either Docker Compose or the Community Apps template.
## Method 1: Docker Compose (Recommended)
### Prerequisites
- Unraid 6.12+
- Docker enabled in Unraid settings
- Docker Compose plugin installed (available via Community Apps)
### Steps
#### 1. Create the Application Directory
Open an Unraid terminal (or SSH):
```bash
mkdir -p /mnt/user/appdata/project-nomad
cd /mnt/user/appdata/project-nomad
```
#### 2. Download Configuration Files
```bash
# Download docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/docker-compose.yml -o docker-compose.yml
# Download .env.example
curl -fsSL https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/.env.example -o .env
# Download nginx config
mkdir -p nginx
curl -fsSL https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/nginx/default.conf -o nginx/default.conf
# Download entrypoint
mkdir -p install
curl -fsSL https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/install/entrypoint.sh -o install/entrypoint.sh
chmod +x install/entrypoint.sh
```
#### 3. Configure Environment
```bash
# Generate secrets
APP_KEY=$(openssl rand -hex 32)
DB_PASS=$(openssl rand -base64 24)
# Update .env
sed -i "s/^APP_KEY=replaceme/APP_KEY=$APP_KEY/" .env
sed -i "s/^DB_PASSWORD=replaceme/DB_PASSWORD=$DB_PASS/" .env
sed -i "s/^MYSQL_ROOT_PASSWORD=replaceme/MYSQL_ROOT_PASSWORD=$DB_PASS/" .env
# Set Unraid storage path
sed -i "s|^NOMAD_DATA_DIR=.*|NOMAD_DATA_DIR=/mnt/user/appdata/project-nomad|" .env
# Set your server URL (replace with your Unraid IP)
sed -i "s|^URL=.*|URL=http://$(hostname -I | awk '{print $1}'):8080|" .env
```
#### 4. Create Storage Directories
```bash
mkdir -p /mnt/user/appdata/project-nomad/{storage,redis,logs/nginx}
```
#### 5. Start the Stack
```bash
docker compose up -d
```
#### 6. Access Nomad
Open your browser: `http://YOUR_UNRAID_IP:8080`
### Storage Layout on Unraid
```
/mnt/user/appdata/project-nomad/
├── docker-compose.yml
├── .env
├── nginx/
│ └── default.conf
├── install/
│ └── entrypoint.sh
├── storage/ # Content files (cache-only share recommended)
├── redis/ # Redis data
└── logs/
└── nginx/ # Nginx logs
```
**Tip:** For large content libraries (ZIM files, maps), consider storing them on a separate Unraid share with cache-preferred settings for better I/O performance.
## Method 2: Community Apps Template
### Steps
1. Install the **Docker Compose Manager** plugin from Community Apps
2. In Unraid web UI, go to **Docker** → **Add Container**
3. Click **Template** and paste the template URL:
```
https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/homelab/unraid-template.xml
```
4. Configure the required fields:
- **APP_KEY**: Generate with `openssl rand -hex 32`
- **DB_PASSWORD**: Set a secure password
- **URL**: Your Unraid server URL
5. Click **Apply**
> **Note:** The Community Apps template creates only the Nomad application container. You still need separate MySQL and Redis containers. The Docker Compose method handles all services automatically.
## Unraid-Specific Tips
### Use Cache Drive for Database
For best performance, store the MySQL database on your Unraid cache drive:
```bash
# In .env, the database uses a Docker named volume by default
# This automatically stores on your cache drive
```
### Reverse Proxy with Unraid's SWAG/LSIO
If you use the SWAG (Secure Web Application Gateway) container:
1. Create a proxy config in `/mnt/user/appdata/swag/nginx/proxy-confs/`:
```nginx
# nomad.subdomain.conf
server {
listen 443 ssl;
server_name nomad.*;
include /config/nginx/ssl.conf;
location / {
proxy_pass http://nomad-app:8080;
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 10G;
}
}
```
2. Ensure SWAG and Nomad share a Docker network.
### Auto-Start on Boot
Docker Compose stacks with `restart: unless-stopped` will automatically restart when Unraid boots and Docker starts.
## Updating on Unraid
```bash
cd /mnt/user/appdata/project-nomad
docker compose pull
docker compose up -d
```
## Troubleshooting
### Permission Issues
Unraid runs containers as root by default. If you encounter permission issues:
```bash
chown -R nobody:users /mnt/user/appdata/project-nomad/storage
chmod -R 755 /mnt/user/appdata/project-nomad/storage
```
### Network Conflicts
If port 8080 conflicts with another container, change the port in `.env`:
```
PORT=8088
```
### Logs
```bash
# View all container logs
docker compose logs -f
# View specific service logs
docker compose logs -f nomad-app
```

View File

@ -0,0 +1,49 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# Caddy Reverse Proxy Configuration
# =============================================================================
#
# Usage:
# 1. Replace "nomad.home.local" with your actual domain/hostname
# 2. Place this file in your Caddy configuration directory
# 3. Reload Caddy: caddy reload
#
# Caddy automatically provisions TLS certificates via Let's Encrypt
# when using a public domain. For local domains, it generates
# self-signed certificates automatically.
# =============================================================================
nomad.home.local {
# Reverse proxy to Nomad application
reverse_proxy nomad-app:8080 {
# Health checks
health_uri /api/health
health_interval 30s
health_timeout 5s
# WebSocket support
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
}
# Large file upload support (10GB)
request_body {
max_size 10GB
}
# Access logging
log {
output file /var/log/caddy/nomad-access.log
format json
}
# Security headers
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
}

View File

@ -0,0 +1,87 @@
# Nginx Proxy Manager Configuration for Project N.O.M.A.D.
## Prerequisites
- Nginx Proxy Manager installed and running
- Project N.O.M.A.D. stack running (via `docker compose up -d`)
- Both NPM and Nomad on the same Docker network, or using the host IP
## Setup Steps
### 1. Add Proxy Host
1. Open Nginx Proxy Manager web UI (typically `http://your-server:81`)
2. Go to **Proxy Hosts** → **Add Proxy Host**
### 2. Details Tab
| Field | Value |
|-------|-------|
| **Domain Names** | `nomad.home.local` (or your domain) |
| **Scheme** | `http` |
| **Forward Hostname / IP** | `nomad-app` (if on same Docker network) or your host IP |
| **Forward Port** | `8080` |
| **Cache Assets** | Enabled |
| **Block Common Exploits** | Enabled |
| **Websockets Support** | **Enabled** (required for real-time updates) |
### 3. SSL Tab (Optional)
| Field | Value |
|-------|-------|
| **SSL Certificate** | Request a new certificate or select existing |
| **Force SSL** | Enabled |
| **HTTP/2 Support** | Enabled |
### 4. Advanced Tab
Add the following custom Nginx configuration:
```nginx
# Large file upload support (10GB)
client_max_body_size 10G;
# Timeout settings for large operations
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 60s;
# Forward real IP headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
```
### 5. Docker Network Configuration
If Nginx Proxy Manager runs in Docker, ensure it can reach the Nomad containers.
**Option A: Same Docker network**
Add NPM to the `nomad-internal` network in your NPM docker-compose:
```yaml
services:
npm:
# ... existing config ...
networks:
- nomad-internal
networks:
nomad-internal:
external: true
```
**Option B: Host networking**
Use your server's IP address instead of container names:
- Forward Hostname: `192.168.1.100` (your server IP)
- Forward Port: `8080`
## Verification
After setup, visit your configured domain. You should see the Nomad dashboard.
Check the health endpoint: `https://nomad.home.local/api/health`

View File

@ -0,0 +1,65 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# Traefik Reverse Proxy Configuration
# =============================================================================
#
# Add these labels to your nomad-app service in docker-compose.yml
# when using Traefik as your reverse proxy.
#
# Prerequisites:
# - Traefik running with Docker provider enabled
# - A Traefik network (e.g., "traefik-public")
#
# Usage:
# Add to nomad-app service in docker-compose.yml:
# labels: (copy from below)
# networks:
# - nomad-internal
# - traefik-public
#
# =============================================================================
# Docker Compose labels for nomad-app service:
#
# labels:
# - "traefik.enable=true"
# # HTTP router
# - "traefik.http.routers.nomad.rule=Host(`nomad.home.local`)"
# - "traefik.http.routers.nomad.entrypoints=web"
# # HTTPS router (optional — uncomment for TLS)
# # - "traefik.http.routers.nomad-secure.rule=Host(`nomad.home.local`)"
# # - "traefik.http.routers.nomad-secure.entrypoints=websecure"
# # - "traefik.http.routers.nomad-secure.tls=true"
# # - "traefik.http.routers.nomad-secure.tls.certresolver=letsencrypt"
# # Service
# - "traefik.http.services.nomad.loadbalancer.server.port=8080"
# # WebSocket support
# - "traefik.http.middlewares.nomad-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
# # Large file upload support
# - "traefik.http.middlewares.nomad-buffering.buffering.maxRequestBodyBytes=10737418240"
# =============================================================================
# Traefik dynamic configuration file (alternative to labels)
# Place in your Traefik dynamic config directory
# =============================================================================
http:
routers:
nomad:
rule: "Host(`nomad.home.local`)"
service: nomad
entryPoints:
- web
# Uncomment for TLS:
# tls:
# certResolver: letsencrypt
services:
nomad:
loadBalancer:
servers:
- url: "http://nomad-app:8080"
healthCheck:
path: /api/health
interval: 30s
timeout: 5s

View File

@ -0,0 +1,25 @@
apiVersion: v2
name: project-nomad
description: >-
Project N.O.M.A.D. — Homelab Edition.
Network Operations Monitoring and Automation Dashboard.
Offline-first knowledge server with AI chat, information library,
education platform, offline maps, and homelab monitoring.
type: application
version: 1.0.0
appVersion: "1.29.0"
home: https://github.com/DocwatZ/project-nomad-homelab-edition
icon: https://raw.githubusercontent.com/DocwatZ/project-nomad-homelab-edition/main/admin/public/favicons/favicon-96x96.png
sources:
- https://github.com/DocwatZ/project-nomad-homelab-edition
maintainers:
- name: DocwatZ
url: https://github.com/DocwatZ
keywords:
- nomad
- homelab
- monitoring
- offline
- knowledge-base
- nas
- truenas

View File

@ -0,0 +1,96 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: app
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: app
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: app
spec:
containers:
- name: nomad-app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.app.port }}
protocol: TCP
env:
- name: NODE_ENV
value: {{ .Values.app.nodeEnv | quote }}
- name: PORT
value: {{ .Values.app.port | quote }}
- name: HOST
value: "0.0.0.0"
- name: LOG_LEVEL
value: {{ .Values.app.logLevel | quote }}
- name: APP_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-secrets
key: app-key
- name: URL
value: {{ .Values.app.url | quote }}
- name: DB_HOST
value: {{ .Release.Name }}-database
- name: DB_PORT
value: "3306"
- name: DB_DATABASE
value: {{ .Values.database.name | quote }}
- name: DB_USER
value: {{ .Values.database.user | quote }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-secrets
key: db-password
- name: DB_SSL
value: "false"
- name: REDIS_HOST
value: {{ .Release.Name }}-redis
- name: REDIS_PORT
value: "6379"
- name: NOMAD_STORAGE_PATH
value: "/app/storage"
volumeMounts:
- name: storage
mountPath: /app/storage
livenessProbe:
httpGet:
path: /api/health
port: {{ .Values.app.port }}
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: {{ .Values.app.port }}
initialDelaySeconds: 30
periodSeconds: 10
resources:
{{- toYaml .Values.resources.app | nindent 12 }}
volumes:
- name: storage
{{- if .Values.storage.data.existingClaim }}
persistentVolumeClaim:
claimName: {{ .Values.storage.data.existingClaim }}
{{- else if .Values.storage.data.hostPath }}
hostPath:
path: {{ .Values.storage.data.hostPath }}
type: DirectoryOrCreate
{{- else }}
persistentVolumeClaim:
claimName: {{ .Release.Name }}-data
{{- end }}

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-secrets
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
type: Opaque
data:
app-key: {{ .Values.app.appKey | b64enc | quote }}
db-password: {{ .Values.database.password | b64enc | quote }}
db-root-password: {{ .Values.database.rootPassword | b64enc | quote }}

View File

@ -0,0 +1,63 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-app
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: app
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.app.port }}
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: app
---
{{- if .Values.database.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-database
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: database
spec:
type: ClusterIP
ports:
- port: 3306
targetPort: 3306
protocol: TCP
name: mysql
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: database
{{- end }}
---
{{- if .Values.redis.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-redis
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: redis
spec:
type: ClusterIP
ports:
- port: 6379
targetPort: 6379
protocol: TCP
name: redis
selector:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: redis
{{- end }}

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

@ -0,0 +1,118 @@
# =============================================================================
# PROJECT N.O.M.A.D. — Homelab Edition
# TrueNAS SCALE Helm Values
# =============================================================================
# Application image
image:
repository: ghcr.io/crosstalk-solutions/project-nomad
tag: latest
pullPolicy: Always
# Replica count
replicaCount: 1
# Application settings
app:
port: 8080
nodeEnv: production
logLevel: info
# Generate with: openssl rand -hex 32
appKey: ""
# External URL for accessing Nomad
url: "http://localhost:8080"
# Database settings
database:
enabled: true
image:
repository: mysql
tag: "8.0"
rootPassword: ""
name: nomad
user: nomad
password: ""
storage:
size: 10Gi
# TrueNAS storage class
storageClass: ""
# Redis settings
redis:
enabled: true
image:
repository: redis
tag: 7-alpine
maxMemory: 256mb
storage:
size: 1Gi
storageClass: ""
# Nginx reverse proxy
nginx:
enabled: true
image:
repository: nginx
tag: alpine
# Storage
storage:
# Main data storage for Nomad content
data:
enabled: true
size: 100Gi
storageClass: ""
# Use an existing PVC
existingClaim: ""
# Host path (for TrueNAS ix-volumes)
hostPath: ""
# Log storage
logs:
enabled: true
size: 5Gi
storageClass: ""
# Service configuration
service:
type: ClusterIP
port: 80
# Ingress configuration
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: nomad.home.local
paths:
- path: /
pathType: Prefix
tls: []
# Resource limits
resources:
app:
limits:
memory: 2Gi
requests:
cpu: 250m
memory: 512Mi
worker:
limits:
memory: 2Gi
requests:
cpu: 100m
memory: 256Mi
database:
limits:
memory: 1Gi
requests:
cpu: 250m
memory: 256Mi
redis:
limits:
memory: 512Mi
requests:
cpu: 50m
memory: 64Mi

View File

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

View File

@ -2,25 +2,80 @@
set -e
echo "Starting entrypoint script..."
echo "Running wait-for-it.sh to ensure MySQL is ready..."
echo "============================================"
echo " Project N.O.M.A.D. — Homelab Edition"
echo " Starting up..."
echo "============================================"
# Use wait-for-it.sh to wait for MySQL to be available
# wait-for-it.sh <host>:<port> [-t timeout] [-- command args]
/usr/local/bin/wait-for-it.sh ${DB_HOST}:${DB_PORT} -t 60 -- echo "MySQL is up and running!"
# ---------------------------------------------------------------------------
# Wait for database to be ready (no external dependencies like wait-for-it.sh)
# ---------------------------------------------------------------------------
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-3306}"
MAX_RETRIES=60
RETRY_INTERVAL=2
# Run AdonisJS migrations
echo "Running AdonisJS migrations..."
wait_for_tcp() {
local host="$1"
local port="$2"
node -e "
const net = require('net');
const s = new net.Socket();
s.setTimeout(2000);
s.connect($port, '$host', () => { s.destroy(); process.exit(0); });
s.on('error', () => process.exit(1));
s.on('timeout', () => { s.destroy(); process.exit(1); });
" 2>/dev/null
}
echo "Waiting for database at ${DB_HOST}:${DB_PORT}..."
retries=0
while [ $retries -lt $MAX_RETRIES ]; do
if wait_for_tcp "${DB_HOST}" "${DB_PORT}"; then
echo "Database is ready!"
break
fi
retries=$((retries + 1))
echo " Waiting for database... (attempt ${retries}/${MAX_RETRIES})"
sleep $RETRY_INTERVAL
done
if [ $retries -eq $MAX_RETRIES ]; then
echo "ERROR: Database did not become ready in time. Continuing anyway..."
fi
# ---------------------------------------------------------------------------
# Ensure storage directories exist
# ---------------------------------------------------------------------------
STORAGE_PATH="${NOMAD_STORAGE_PATH:-/app/storage}"
echo "Ensuring storage directories exist at ${STORAGE_PATH}..."
mkdir -p "${STORAGE_PATH}" 2>/dev/null || true
# ---------------------------------------------------------------------------
# Run database migrations
# ---------------------------------------------------------------------------
echo "Running database migrations..."
node ace migration:run --force
# Seed the database if needed
# ---------------------------------------------------------------------------
# Seed the database
# ---------------------------------------------------------------------------
echo "Seeding the database..."
node ace db:seed
# Start background workers for all queues
echo "Starting background workers for all queues..."
node ace queue:work --all &
# ---------------------------------------------------------------------------
# Start background workers (only if not running as a dedicated worker)
# ---------------------------------------------------------------------------
if [ "${NOMAD_ROLE}" != "worker" ]; then
echo "Starting background workers for all queues..."
node ace queue:work --all &
fi
# Start the AdonisJS application
echo "Starting AdonisJS application..."
# ---------------------------------------------------------------------------
# Start the application
# ---------------------------------------------------------------------------
echo "============================================"
echo " N.O.M.A.D. is ready!"
echo " Listening on port ${PORT:-8080}"
echo "============================================"
exec node bin/server.js

77
nginx/default.conf Normal file
View File

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