If you've tried to run Project N.O.M.A.D. on a TerraMaster NAS and hit a wall of "Permission denied" errors and containers that won't stay up — this post is for you. After real-world testing on a TerraMaster F4-423, I've got a working config that handles every quirk TOS 7 throws at you.
But before we get into the YAML, let's talk about what we're building here and why it matters.
What Is Project N.O.M.A.D.?
Project N.O.M.A.D. (Networked Offline Mobile Autonomous Deployment) is an open-source, self-hosted platform that bundles a curated stack of offline knowledge, AI, and educational tools into a single Docker Compose setup. It's maintained by CrossTalk Solutions and designed to run on hardware you own — no cloud required.
The stack includes:
- Kiwix — offline Wikipedia, medical references, Stack Overflow snapshots, and hundreds of other ZIM-format knowledge bases
- Ollama — local LLM inference, so you have AI assistance without a connection
- Kolibri — offline educational content from Khan Academy and others
- Flatnotes — a clean, markdown-based personal wiki and note system
- CyberChef — the GCHQ data transformation tool, fully offline
- Qdrant — local vector database for AI-powered search across your knowledge base
The whole thing is managed through a clean web admin panel. You add content packs, enable or disable services, and N.O.M.A.D. handles the Docker orchestration. The GitHub repository has full documentation and an active issue tracker.
The TerraMaster F4-423: A Purpose-Built Offline Knowledge Machine
The TerraMaster F4-423 is a 4-bay NAS running an Intel Celeron N5105 quad-core processor with up to 32GB of DDR4 RAM. It runs TerraMaster OS (TOS), which is a Linux-based NAS operating system with a Docker app built in.
For a NOMAD deployment, the specs line up well:
- N5105 CPU — efficient enough for 24/7 operation, capable enough to run smaller Ollama models (Phi-3 mini, Llama 3.2 3B) without a GPU
- 32GB RAM ceiling — Ollama appreciates headroom; 16GB is the comfortable floor for running a model alongside the rest of the stack
- 4-bay storage — room for RAID redundancy on your knowledge base and still have capacity for ZIM files, which can run large (a full English Wikipedia ZIM is ~100GB)
- Low idle power draw — reported at ~15–20W at idle, which matters when you're running this around the clock or on backup power
For a "just works" offline server, it's a solid choice. The F4-424 Pro steps up to an N305 CPU and 64GB RAM if you want to run larger models — but for most N.O.M.A.D. use cases, the F4-423 is the sweet spot.
Why a NAS Is the Right Hardware for This
Let me be direct about what Project N.O.M.A.D. is actually for: it's the knowledge infrastructure you want running when normal infrastructure stops being reliable.
Power outages, ISP failures, natural disasters, network-level disruptions — in any scenario where internet access goes away, a NAS running N.O.M.A.D. becomes your local library, your AI assistant, your medical reference, your educational resource, and your secure note system. All of it accessible from any device on your local network with nothing but a browser.
TerraMaster NAS hardware is particularly well-suited to this because:
- Designed for always-on operation — the hardware, thermal design, and TOS software are all built around 24/7 uptime, not desktop duty cycles
- Scheduled power on/off — TOS supports automatic boot on a schedule, so you can have it power on, pull container updates, and be ready before you need it — without leaving it running 24/7 if that's not your preference
- UPS support — TOS integrates with USB UPS devices for graceful shutdown during power events, protecting your data
- RAID storage — your offline knowledge base is too valuable to lose to a single drive failure; the 4-bay design lets you run RAID 5 with a hot spare
- Low power — 15-20W idle means it can run on a modest battery backup for hours, not minutes
When the excrement hits the rotating wind device, the last thing you want is to be troubleshooting a laptop that runs hot, a Raspberry Pi that's too slow to serve Ollama, or a cloud service that's unreachable. A purpose-built NAS with a solid RAID array and a UPS behind it is infrastructure you can actually rely on.
Note: I'll cover the full hardware build — drives, UPS, network config, and how I have the physical setup organized — in a follow-up post in the Builds section.
The TOS 7 Problem (And Why the Normal Install Doesn't Work)
Before the working config, the issues worth understanding:
- Docker runs as the first user, not root. On TOS,
containerdand the Docker daemon run as the admin user you created during setup — not as true root. Any container that expects to write as root will fail silently or throw permission errors. - TerraMaster's Advanced ACL system is aggressive. TOS applies ACLs to volumes that override standard Unix permissions. Folders created through the TOS UI may be inaccessible to Docker containers even with
777permissions set. - The official N.O.M.A.D. updater can break on TOS. It sometimes fails to apply new features cleanly because of how TOS handles Docker socket access. Manual
docker-compose.ymlmanagement is more reliable.
The fixes for all three are in the config below.
Lessons Learned (Read Before You Deploy)
- Force services that hit permission issues to run as
user: "0:0"— this applies especially to Kiwix, Ollama, and Kolibri - Use the real volume path in all mounts:
/Volume1/docker/nomad/..., not relative paths or symlinks - Set ownership of your
storage/folder tree to0:0(root) with755permissions before first launch:
chown -R 0:0 /Volume1/docker/nomad/storage chmod -R 755 /Volume1/docker/nomad/storage
/Volume1/docker/nomad/
├── docker-compose.yml
├── mysql/
├── redis/
└── storage/
├── zim/
├── flatnotes/
├── ollama/
├── kolibri/
└── qdrant/
---
## The Working docker-compose.yml
This is the config that runs cleanly on TOS 7 after everything else failed. Replace `YOUR-NAS-IP` with your NAS's local IP, and change all placeholder passwords before deploying.
name: project-nomad
services:
admin:
image: ghcr.io/crosstalk-solutions/project-nomad:latest
pull_policy: always
container_name: nomad_admin
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "8080:8080"
volumes:
- /Volume1/docker/nomad/storage:/app/storage
- /var/run/docker.sock:/var/run/docker.sock
- nomad-update-shared:/app/update-shared
environment:
- NODE_ENV=production
- PORT=8080
- LOG_LEVEL=info
- APP_KEY=ChangeThisToAStrongRandom32CharString123
- HOST=0.0.0.0
- URL=http://YOUR-NAS-IP:8080
- DB_HOST=mysql
- DB_PORT=3306
- DB_DATABASE=nomad
- DB_USER=nomad_user
- DB_PASSWORD=ChangeThisToAStrongPassword123!
- DB_NAME=nomad
- DB_SSL=false
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
dozzle:
image: amir20/dozzle:v10.0
container_name: nomad_dozzle
restart: unless-stopped
ports:
- "9999:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_ENABLE_ACTIONS=false
- DOZZLE_ENABLE_SHELL=false
mysql:
image: mysql:8.0
container_name: nomad_mysql
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=ChangeThisToAStrongPassword123!
- MYSQL_DATABASE=nomad
- MYSQL_USER=nomad_user
- MYSQL_PASSWORD=ChangeThisToAStrongPassword123!
volumes:
- /Volume1/docker/nomad/mysql:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: nomad_redis
restart: unless-stopped
volumes:
- /Volume1/docker/nomad/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
updater:
image: ghcr.io/crosstalk-solutions/project-nomad-sidecar-updater:latest
pull_policy: always
container_name: nomad_updater
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /Volume1/docker/nomad:/opt/project-nomad
- nomad-update-shared:/shared
disk-collector:
image: ghcr.io/crosstalk-solutions/project-nomad-disk-collector:latest
pull_policy: always
container_name: nomad_disk_collector
restart: unless-stopped
volumes:
- /:/host:ro,rslave
- /Volume1/docker/nomad/storage:/storage
# ── Services ─────────────────────────────────────────────────
kiwix:
image: ghcr.io/kiwix/kiwix-serve:latest
container_name: nomad_kiwix_server
user: "0:0"
restart: unless-stopped
ports:
- "8090:8080"
volumes:
- /Volume1/docker/nomad/storage/zim:/data
command: --library /data/kiwix-library.xml --monitorLibrary
flatnotes:
image: dullage/flatnotes:v5.5.4
container_name: nomad_flatnotes
restart: unless-stopped
ports:
- "8200:8080"
volumes:
- /Volume1/docker/nomad/storage/flatnotes:/app/data
environment:
- FLATNOTES_AUTH_TYPE=password
- FLATNOTES_USERNAME=admin
- FLATNOTES_PASSWORD=ChangeThisToAStrongPassword123!
- FLATNOTES_SECRET_KEY=ChangeThisToARandomLongString123456789
ollama:
image: ollama/ollama:0.20.5
container_name: nomad_ollama
user: "0:0"
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- /Volume1/docker/nomad/storage/ollama:/root/.ollama
environment:
- OLLAMA_NUM_PARALLEL=4
- OLLAMA_MAX_LOADED_MODELS=2
kolibri:
image: treehouses/kolibri:0.12.8
container_name: nomad_kolibri
user: "0:0"
restart: unless-stopped
ports:
- "8300:8080"
volumes:
- /Volume1/docker/nomad/storage/kolibri:/root/.kolibri
cyberchef:
image: ghcr.io/gchq/cyberchef:10.23
container_name: nomad_cyberchef
restart: unless-stopped
ports:
- "8100:80"
qdrant:
image: qdrant/qdrant:v1.16
container_name: nomad_qdrant
restart: unless-stopped
ports:
- "6333-6334:6333-6334"
volumes:
- /Volume1/docker/nomad/storage/qdrant:/qdrant/storage
volumes:
nomad-update-shared:
driver: local
---
## Auto-Start Script for TOS Scheduled Power-On
If you use TOS's scheduled power on/off (Power Management → Schedule), this startup script handles the delay needed for Docker and networking to fully initialize after boot:
#!/bin/bash
# /Volume1/docker/nomad/nomad-startup.sh
# Project N.O.M.A.D. startup for TerraMaster TOS 7 — optimized for scheduled power-on
echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting Project N.O.M.A.D. after boot..." >> /var/log/nomad-startup.log
# Allow Docker and network stack to fully initialize
sleep 35
cd /Volume1/docker/nomad
echo "$(date '+%Y-%m-%d %H:%M:%S') - Pulling latest images..." >> /var/log/nomad-startup.log
docker-compose pull --quiet
echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting containers..." >> /var/log/nomad-startup.log
docker-compose up -d --remove-orphans
echo "$(date '+%Y-%m-%d %H:%M:%S') - Startup complete." >> /var/log/nomad-startup.log
Add this to TOS's Task Scheduler (Control Panel → Task Scheduler) set to run on system startup. The 35-second sleep is not optional — the N5105 needs time after a scheduled power-on before `containerd` is fully ready.
Service Access URLs
Once everything is up, your stack is available at:| Service | URL | Notes |
|---|---|---|
| N.O.M.A.D. Admin | http://YOUR-NAS-IP:8080 | Main dashboard — manage content packs here |
| Kiwix | http://YOUR-NAS-IP:8090 | Offline Wikipedia, medical refs, Stack Overflow |
| Flatnotes | http://YOUR-NAS-IP:8200 | Personal wiki and markdown notes |
| CyberChef | http://YOUR-NAS-IP:8100 | Data transformation toolkit |
| Kolibri | http://YOUR-NAS-IP:8300 | Offline educational content |
| Ollama | http://YOUR-NAS-IP:11434 | Local LLM inference endpoint |
| Dozzle (logs) | http://YOUR-NAS-IP:9999 | Container log viewer |
What's Next
This covers the Docker config and the TOS-specific gotchas. The follow-up Builds post will cover the full physical setup: drive selection and RAID config, UPS integration, network segmentation, and how this fits into a broader preparedness infrastructure.
If you're running into issues with a specific service or a different TerraMaster model, the Project N.O.M.A.D. GitHub issues are active and worth checking before debugging blind.