Production Docker stack infrastructure for a Synology DS723+ NAS running DSM 7.3. All stacks are managed via Dockhand and deployed under the canonical root /volume2/docker.
- NAS: Synology DS723+ . DSM 7.3 . Docker Engine via Container Manager
- Stack root:
/volume2/docker - Dockhand data:
/volume2/docker/dockhand(outside stack root, persists across repo resets) - Network model: All service ports bind to
10.0.1.15(LAN IP) -- no0.0.0.0bindings - Compose format:
compose.yamlthroughout (nodocker-compose.yml)
Live target NAS: otsorundscore (DSM 7.3.2-86009 U3, DS723+, AMD Ryzen R1600, 32 GB, Cool mode, CPU-only). Authoritative spec:
docs/host-profile-otsorundscore.md-- identity, hardware, derived budgetsdocs/dsm-732-runtime-quirks.md-- Container Manager / BTRFS / Cool-mode operational quirks
Two linters enforce host-profile rules in CI (chained from scripts/compose-validate.sh and scripts/verify-repo-layout.sh):
| Linter | Rule | Override |
|---|---|---|
scripts/lint-rfc1918.sh |
Every bridge subnet: must live inside 10/8, 172.16/12, or 192.168/16. |
-- |
scripts/lint-host-budget.sh |
Sum mem_limit across all stacks <= HOST_MEM_BUDGET_MB (default 32 000 MB = physical RAM ceiling). |
HOST_MEM_BUDGET_MB=26000 bash scripts/lint-host-budget.sh for stricter "leave headroom for DSM" mode. |
ots-docker/
+--- stacks/ # One subdirectory per stack
| +--- _dns-server/ # DNS zone files + BIND config (DSM native package, not compose)
| +--- _haproxy/ # HAProxy reverse proxy (bare-metal, not compose)
| +--- acme-sh/ # Let's Encrypt certificate automation
| +--- code-server/ # VS Code in the browser
| +--- codex-docs/ # Documentation platform
| +--- dozzle/ # Docker log viewer
| +--- flowise/ # LLM workflow builder (FlowiseAI)
| +--- github-desktop/ # GitHub Desktop (containerised)
| +--- grafana-prom/ # Grafana + Prometheus monitoring
| +--- homepage/ # Dashboard / service portal
| +--- influxdb/ # Time-series DB (ntopng metrics → Grafana)
| +--- it-tools/ # IT utility toolkit
| +--- n8n/ # Workflow automation (n8n)
| +--- ollama/ # Local LLM inference (Ollama, CPU-only)
| +--- openresume/ # Resume builder
| +--- otspsu/ # PSU OTS application
| +--- remotely/ # Remote desktop / support
| +--- searxng/ # Privacy-respecting metasearch
| +--- synology-api-bridge/ # Internal DSM HTTP shim (FastAPI)
| +--- watchtower/ # Automated image update management
| +--- zabbix/ # Infrastructure monitoring
| +--- archives/ # Retired stacks (kept for reference)
|
| Native DSM packages (no compose stack needed):
| Adminer, MariaDB, PostgreSQL — managed via Synology Package Center
|
+--- scripts/ # Operational scripts
| +--- dockhand-sync.sh # Re-sync dockhand/ -> /volume2/docker/dockhand
| +--- compose-validate.sh # Validate all compose.yaml files
| +--- verify-repo-layout.sh # Check repo structure invariants
| +--- init-nas.sh # First-boot NAS initialisation
| +--- nas-reset.sh # Factory-reset helper
| +--- fix-permissions.sh # Repair bind-mount ownership
| +--- restore-env.sh # Restore .env from .env.example
| +--- maintenance/ # Scheduled maintenance scripts
|
+--- docs/ # (planned) Architecture and runbook docs
cd /volume2/docker
git clone https://github.com/olutechsys/ots-docker.gitBefore deploying stacks for the first time, perform these one-time setup steps on the NAS:
- Create the backbone network (required by several stacks):
docker network create \
--driver bridge \
--subnet 172.26.0.0/24 \
--gateway 172.26.0.1 \
ce-internalVerify: docker network inspect ce-internal
- Populate all
.envfiles from.env.example(templates are intentionally provided but.envfiles are git-ignored):
# Create missing .env files from .env.example
find stacks/ -name .env.example -exec sh -c 'cp "$1" "${1%.example}"' _ {} \;Edit each .env with actual values (passwords, API keys) -- do not commit .env files to git.
- Validate all compose files prior to import:
bash /volume2/docker/scripts/compose-validate.sh(Then continue with Dockhand installation steps below.)
sudo cp /volume2/docker/dockhand/scripts/dockhand-start.sh /usr/local/etc/rc.d/dockhand.sh
sudo chmod +x /usr/local/etc/rc.d/dockhand.sh
sudo /usr/local/etc/rc.d/dockhand.shDockhand will be available at http://10.0.1.15:3866 after health check passes (60s).
First-time setup: See dockhand/README.md for UI initialization and git webhook configuration.
DSM boot persistence: DSM 7.3 does not auto-execute /usr/local/etc/rc.d/*.sh on reboot. To make Dockhand start automatically, follow dockhand/docs/DSM_BOOT_PERSISTENCE.md to create a DSM Task Scheduler "Boot-up" task.
Access the web UI at http://10.0.1.15:3866 and:
- Create admin user (Settings > Authentication > Users)
- Add Docker environment (Settings > Environments > +Add: "DS723", Unix socket)
- Register git webhook (Settings > Webhooks) for auto-sync on repo push
See dockhand/README.md for detailed steps.
Dockhand auto-imports stacks from your ots-docker repo via:
- Git webhook: Push to repo -> Dockhand auto-deploys (recommended)
- Manual upload: Dockhand UI -> Stacks -> Create Stack -> Upload compose.yaml
For each stack, populate .env from .env.example before deployment.
bash /volume2/docker/scripts/compose-validate.sh
bash /volume2/docker/scripts/verify-repo-layout.sh| Convention | Detail |
|---|---|
| Compose filename | Always compose.yaml (never docker-compose.yml) |
| Port bindings | 10.0.1.15:HOST_PORT:CONTAINER_PORT -- LAN-only |
| UID/GID | Default PUID=0 / PGID=0 for Synology bind-mount ownership |
| Database images | Version-pinned; watchtower.enable=false label to prevent auto-upgrade |
| Floating tags | Prohibited for stateful services; pinned semver required |
| Secrets | Never in compose.yaml; always via .env (excluded from git) |
| Log rotation | json-file, max-size=10m, max-file=3 on all services |
All bridge networks use explicit /24 subnets to prevent Docker's auto-assigned /16 ranges from creating collisions. The DNS server ACL covers 172.16.0.0/12 to allow all Docker bridge subnets to resolve internal hostnames.
| Stack | Network name | Subnet | Notes |
|---|---|---|---|
| (backbone) | ce-internal | 172.26.0.0/24 | External; created by init-nas.sh |
| github-desktop | github-desktop-net | 172.20.0.0/24 | KasmVNC browser container |
| ollama | ollama-net | 172.27.0.0/24 | CPU-only inference |
| pyroscope | pyroscope-net | 172.28.0.0/24 | Pyroscope continuous profiling |
| pytorch | pytorch-net | 172.28.1.0/24 | PyTorch Jupyter Notebook |
| code-server | code-server-net | 172.28.2.0/24 | |
| grafana-prom | grafana-net | 172.29.0.0/24 | |
| grafana-prom | prometheus-net | 172.29.1.0/24 | |
| zabbix | zabbix-net | 172.30.0.0/24 | |
| flowise | flowise-net | 172.30.1.0/24 | LLM workflow builder |
| n8n | n8n-net | 172.30.2.0/24 | Workflow automation |
| homepage | homepage-net | 172.30.5.0/24 | Dashboard |
| dozzle | dozzle-net | 172.31.0.0/24 | |
| watchtower | watchtower-net | 172.31.1.0/24 | |
| loki-alloy | loki-net | 172.31.7.0/24 | Grafana Loki + Alloy logging |
| otspsu | otspsu-net | 172.31.10.0/24 | |
| (influxdb) | ce-internal | 172.26.0.0/24 | Shares backbone; Grafana reaches influxdb:8086 |
The Docker default bridge 172.17.0.0/16 is reserved and must not be re-used.
Retired stacks: databases, db-tools, agents_gateway_data, and mcp-tools-config have been removed. MariaDB, PostgreSQL, and Adminer run as native DSM packages. Subnets 172.31.7.0/24 and 172.31.8.0/24 are now unallocated and available for new stacks.
Important: ce-internal is the only external (pre-created) network and must be created manually before importing stacks.
-
ce-internal(172.26.0.0/24) -- External backbone network shared by: grafana-prom, influxdb, ollama, synology-api-bridge. Create it once on the NAS usingdocker network create(see Pre-Deployment Setup). -
All other networks listed above are created by their respective stacks at
docker compose uptime (internal to the stack) and do not require manual creation.
Refer to the "Pre-Deployment Setup" section for commands and validation steps.
Dockhand lifecycle is managed exclusively by the RC script -- not by a compose stack.
- Script:
dockhand/scripts/dockhand-start.sh-> installed at/usr/local/etc/rc.d/dockhand.sh - Image:
fnsys/dockhand:latest(git-backed Compose orchestration) - Port:
10.0.1.15:3866(HTTP WebUI) - Data:
/volume2/docker/dockhand(outside this repo) - Features: Git webhooks, Compose visual editor, multi-environment support
- Docker binary:
/usr/local/bin/docker - Docker root:
/volume2/@docker - Stack root:
/volume2/docker - User home:
/volume1/homes/laolufayese(remains on volume1) - HAProxy: installed at
/usr/local/etc/haproxy/via@appstore/haproxy(volume1) - Reverse proxy timeouts: set to 600s in DSM -> Application Portal to avoid WebSocket drops
- Stacks using HTTPS backends: configure DSM proxy as HTTPS->HTTPS to avoid 400 Bad Request
- All
.envfiles are excluded from git (see.gitignore) - The
stacks/acme-sh/data/,stacks/ollama/data/, andstacks/github-desktop/config/ssl/directories are gitignored - they contain runtime certificates, private keys, and SSH identity material - Database Watchtower exemptions prevent accidental major-version upgrades that would corrupt data directories
docs/host-profile-otsorundscore.md-- NAS hardware spec, memory budget, and derived resource ceilings.stacks/_dns-server/RUNBOOK.md-- DNS split-horizon setup, BIND view structure, and validation steps.stacks/_haproxy/README.md-- HAProxy reverse proxy configuration, cert deployment, and DNS integration.docs/archive/-- superseded plans and audit reports (historical reference only).
Private infrastructure repository. All rights reserved.