DPlaneOS Wiki
Everything you need to deploy, configure, and operate DPlaneOS. From first boot to high-availability clusters, this wiki covers the full operational surface.
Jump to
Introduction
DPlaneOS is an integration layer and NixOS appliance for homelab and small-office NAS deployments. It is not building a storage OS from scratch. The hard problems were already solved by other projects: NixOS handles the OS, bootloader, service supervision, and reproducible builds. OpenZFS handles pools, datasets, snapshots, send/receive, scrub, encryption, and integrity. PostgreSQL, nginx, Patroni, etcd, Samba, Docker, and a dozen other upstream projects handle the rest.
What DPlaneOS contributes is the seams: roughly 49k lines of Go that tie all of the above together coherently. That means ~400 HTTP routes mapping React UI actions to validated, allowlisted exec calls; state persisted in PostgreSQL with goose migrations; a GitOps reconciler that compares declared state.yaml against the live system and handles drift; an HMAC-chained audit log; ZFS event delivery over WebSocket; the A/B OTA flow with health-checked auto-revert; and the HA state machine including SCSI-3 PR fencing and Patroni integration. Plus ~30k lines of React 19 / TypeScript frontend: 48 pages, ~67 modal/dialog components, one embedded PTY terminal.
It ships as a NixOS appliance ISO. Boot it, answer a handful of prompts, and you have a running system. The daemon (dplaned) listens on 127.0.0.1:9000 behind nginx. It accepts no shell input and validates every argument individually through a hardcoded exec allowlist. Nothing about its design is exotic computer science; it is integration work that nobody had bothered to do under one roof.
Project Status
| Area | Status | Notes |
|---|---|---|
| ZFS pool / dataset / snapshot management | Stable | The core. Used daily in production. |
| SMB, NFS, iSCSI sharing | Stable | Samba + kernel NFS + LIO. |
| Container management (Docker, Compose) | Stable | Including ZFS-clone sandboxes. |
| Hot-swap detection and auto-import | Stable | udev + ZED, exercised regularly. |
| LDAP / Active Directory integration | Beta | Works; less battle-tested than local auth. |
| A/B OTA updates with auto-revert | Beta | Mechanism is sound; sample size is small. |
| GitOps reconciliation | Beta | Structural sync works; edge cases at the property-coverage frontier still surfacing. |
| PostgreSQL HA (Patroni + etcd) | Experimental | Tested in lab, never under real load. |
| Replicated-topology HA with witness | Experimental | Same. Read the Showstopper Mitigation Guide first. |
| Shared-SAS HA with SCSI-3 PR fencing | Experimental | Tested on one hardware configuration. Yours will differ. |
| NVMe-oF over TCP | Experimental | Functional, not yet stress-tested. |
System Requirements
Minimum
| Component | Minimum | Recommended |
|---|---|---|
| CPU | Dual-core x86_64 or aarch64 | Quad-core Intel/AMD or better |
| RAM | 4 GB | 16 GB+ (more for large ZFS ARC) |
| Boot disk | 20 GB SSD (separate from data disks) | 60 GB+ NVMe |
| Network | 100 Mbps Ethernet | 1 Gbps+ Ethernet |
| Arch | x86_64 or aarch64 | Raspberry Pi 5 supported (aarch64) |
Pre-Installation Checklist
- At least two physical drives: one for the OS (boot disk), one or more for ZFS data pools
- A USB stick (2 GB minimum) to write the installer ISO
- Static IP configured on your router/switch (recommended for server use)
- SSH public key ready (password SSH login is disabled post-install)
Installation
1. Download the ISO
From the latest release, download the ISO for your hardware:
| Hardware | ISO filename |
|---|---|
| x86_64 (Intel/AMD) | dplaneos-v<version>-installer-amd64.iso |
| aarch64 (Raspberry Pi 5, ARM servers) | dplaneos-v<version>-installer-arm64.iso |
2. Verify and Write to USB
# Verify the checksum sha256sum -c dplaneos-v11.6.5-installer-amd64.iso.sha256 # Write to USB (Linux / macOS) - replace /dev/sdX sudo dd if=dplaneos-v11.6.5-installer-amd64.iso \ of=/dev/sdX bs=4M status=progress conv=fsync # Windows: use Rufus (https://rufus.ie) in DD image mode
lsblk to identify your USB device. Writing to the wrong device will destroy data.3. Boot and Install
- Boot the target machine from the USB stick. The installer launches automatically on tty1.
- Select Install DPlaneOS from the boot menu. (The "Install Witness Node" option is only for HA replicated-topology clusters.)
- Follow the prompts: target disk, admin password, boot mode (UEFI).
- The installer partitions the disk, installs the NixOS closure from the ISO (no internet required), and reboots.
- Remove the USB stick when prompted.
Headless install: SSH to the installer as root with password dplaneos (installer environment only; this password does not persist). Run bash /etc/dplaneos-install/install.sh if the wizard does not launch automatically.
Installation time: 5-10 minutes from boot to running system.
4. Verify Services After Install
# Main daemon sudo systemctl status dplaned # nginx sudo systemctl status nginx # Database (should return 4 rows: admin, operator, viewer, user) sudo -u postgres psql dplaneos -c "SELECT count(*) FROM roles;" # Health check curl http://127.0.0.1:9000/api/system/health
First Boot & Setup
Open http://<server-ip>/ in a browser. The setup wizard runs automatically on first access.
- Create the admin account — set username and password. No pre-generated password is printed; you choose it here.
- Configure storage — select disks and create a ZFS pool (optional; can be done later from the Storage page).
- Set hostname and timezone.
If the wizard is interrupted (browser closed mid-flow), reopen the URL. It detects the partial state and resumes from the disk selection step.
HTTPS Setup
Go to Settings → TLS. The daemon provisions certificates via ACME (Let's Encrypt by default). For internal-only deployments, generate a self-signed certificate from the same settings page.
Pool Management
RAID Configurations
| Disks | Configuration | Usable Space | Drives You Can Lose |
|---|---|---|---|
| 2 | Mirror | 50% | 1 |
| 3 | RAIDZ1 | 67% | 1 |
| 4 | RAIDZ2 | 50% | 2 |
| 6 | RAIDZ2 | 67% | 2 |
| 6 | RAIDZ3 | 50% | 3 |
Create via UI: Storage → Pools → Create Pool → select disks → choose topology → enter pool name → Create.
Pool Maintenance Commands
# Pool health zpool status zpool status -v tank # verbose, shows errors # Start a scrub (verify on-disk integrity) zpool scrub tank # Check scrub progress zpool status tank | grep scan # Import pools (after OS reinstall or move) zpool import -a # import all available zpool import tank # import by name # Clear transient errors (e.g. after a cable fix) zpool clear tank # Force online a vdev that was taken offline zpool online tank /dev/disk/by-id/ata-...
Recommended Scrub Schedule
Monthly scrubs via cron:
sudo crontab -e
# Add:
0 2 1 * * /usr/sbin/zpool scrub tank
Datasets
Create datasets via Storage → Datasets → Create Dataset.
Recommended Properties
| Property | Recommended Value | Notes |
|---|---|---|
| compression | lz4 | Saves ~30% on typical data with negligible CPU cost |
| atime | off | Eliminates read-triggered writes; improves IOPS |
| recordsize | 128K (default) | Use 1M for sequential media; 4K-16K for databases |
| quota | Set per dataset | Prevents one dataset from consuming the whole pool |
| acltype | posixacl | Required for POSIX ACL management (set by default on new pools) |
Dataset Hierarchy Best Practice
tank/
data/
users/
alice/ # quota=500G per user
bob/
media/ # atime=off, recordsize=1M
backups/ # compression=zstd
docker/ # /var/lib/docker bind mount
Encryption
DPlaneOS supports native ZFS encryption. Encrypted datasets are created via Storage → Datasets → Create Dataset → Enable Encryption. Choose a passphrase or raw key.
Key Management Commands
# Load a key (unlock after reboot) sudo zfs load-key tank/encrypted # Check key status zfs get keystatus tank/encrypted # Change the passphrase sudo zfs change-key tank/encrypted # Unload key (lock without unmounting) sudo zfs unload-key tank/encrypted
The DPlaneOS UI exposes lock, unlock, and key-change operations from the dataset detail page. Locked datasets show a padlock icon in the storage tree.
Snapshots
Snapshots are instantaneous, space-efficient (copy-on-write), and can be replicated to a remote system. They are the primary backup mechanism for user data.
Manual Snapshots
# Take a snapshot zfs snapshot tank/data@backup-$(date +%Y%m%d) # List snapshots zfs list -t snapshot tank/data # Restore a single file (snapshots appear in .zfs/snapshot/) ls /mnt/data/.zfs/snapshot/backup-20260509/myfile.txt cp /mnt/data/.zfs/snapshot/backup-20260509/myfile.txt /mnt/data/ # Roll back to a snapshot (destroys all newer data on the dataset) zfs rollback tank/data@backup-20260509
Automatic Snapshots via NixOS
# In configuration.nix services.zfs.autoSnapshot = { enable = true; frequent = 4; # every 15 min, keep 4 hourly = 24; # keep 24 hourly daily = 7; # keep 7 daily weekly = 4; # keep 4 weekly monthly = 12; # keep 12 monthly };
Scheduled Snapshots via the UI
Storage → Datasets → select a dataset → Snapshots tab. Set a cron schedule, retention count, and whether to include child datasets. Schedules are stored in /etc/dplaneos/snapshot-schedules.json and survive reboots.
Opting a Dataset Out of Auto-Snapshots
zfs set com.sun:auto-snapshot=false tank/scratch
SMART Monitoring
DPlaneOS runs scheduled SMART tests and surfaces results on the dashboard. SMART tasks can be declared in state.yaml under smart_tasks or configured from Storage → SMART.
Supported test types: short (5-15 min), long (hours, more thorough), conveyance (after transport). Recommended schedule: short weekly, long monthly.
# Check a drive's SMART status directly sudo smartctl -a /dev/disk/by-id/ata-... # Run a short test immediately sudo smartctl -t short /dev/disk/by-id/ata-...
File Management
The file explorer is accessible from the Files navigation item. It browses datasets and directories in real time and supports chunked multi-GB uploads directly to the server.
Operations
- Upload: chunked multi-GB uploads, resumable on connection drop
- Download: single files or directory archives
- Rename / Copy / Move / Delete via right-click context menu
- POSIX ACL management: right-click → Manage Permissions (ACL) → add entries for specific users or groups, set Read/Write/Execute, apply recursively
acltype=posixacl on the dataset. New pools created through the UI have this set by default. Set it manually with zfs set acltype=posixacl tank/dataset.Safe Base Paths
File operations (create, delete, rename, chown, chmod) are restricted to safe base paths: /mnt/*, /home/*, /tank/*, /data/*, /media/*, /opt/*, /srv/*, /tmp/*, /var/lib/dplaneos/. Requests outside these paths are rejected.
SMB / Samba
Create SMB shares via Shares → Create Share. The daemon regenerates smb.conf and reloads Samba on every change.
Time Machine Support
Enable the Time Machine toggle when creating a share. DPlaneOS configures the vfs_fruit and vfs_streams_xattr modules automatically, making the share appear as a valid Time Machine destination in macOS.
Troubleshooting SMB
# Test the smb.conf sudo testparm # Check Samba status sudo systemctl status smbd # View active connections sudo smbstatus # Reload Samba config without restarting sudo smbcontrol smbd reload-config
NFS
Create NFS exports via Shares → NFS → Add Export. The daemon writes /etc/exports and calls exportfs -ra on every change.
# Check active exports sudo exportfs -v # Manually reload exports sudo exportfs -ra # Show NFS status sudo systemctl status nfs-server
Domain = your.domain in /etc/idmapd.conf to ensure consistent UID/GID mapping between client and server.iSCSI
iSCSI targets are configured via Shares → iSCSI. DPlaneOS uses the Linux kernel's LIO/nvmet framework via configfs. No additional packages are required.
Typical use cases: VMware datastores, Windows Server disk attachments, bare-metal boot over SAN.
NVMe-oF / TCP
NVMe-oF over TCP is configured via Settings → Optional Protocols → NVMe-oF or declared in state.yaml under fabrics. The daemon uses kernel nvmet via configfs.
Docker & Compose
Requires the docker:write permission. Manage containers from the Containers navigation item.
Deploying Containers
Containers → Deploy Container → fill in image, name, ports, volumes, and environment variables → Deploy.
Compose Stacks
Containers → Stacks → Create Stack → paste or upload a docker-compose.yaml. The daemon calls docker compose up -d through the exec allowlist.
ZFS-Clone Sandboxes
For atomic container updates with rollback, DPlaneOS can create a ZFS clone of a dataset, run the new container version against it, and swap on success or rollback on failure. Configure this in the container's advanced options.
Container Management
| Action | UI Path |
|---|---|
| Start / Stop / Restart | Containers → select container → action buttons |
| View logs | Containers → select container → Logs tab |
| Exec into container | Containers → select container → Exec tab → enter bash |
| Remove | Containers → select container → Delete → confirm |
| Pull image | Containers → Images → Pull |
Custom Container Icons
Icons are resolved in this priority order:
dplaneos.iconlabel on the container (Material Symbol name, filename, or URL)- Built-in image-name mapping (covers 80+ well-known images)
- Fallback generic icon
# Copy your icon to the custom icons directory cp myapp.svg /var/lib/dplaneos/custom_icons/ # Reference it in docker-compose.yaml
services:
myapp:
image: myapp:latest
labels:
dplaneos.icon: myapp.svg # filename in custom_icons/
# Or a Material Symbol name:
# dplaneos.icon: database
Supported formats: .svg, .png, .webp. No daemon restart required after adding icons.
Local Users
Creating a User
- Navigate to Settings → Users → Create User.
- Fill in username (lowercase, no spaces), email, and password (12+ characters recommended).
- Click Create, then assign a role.
Password Policy
Minimum 8 characters with uppercase, lowercase, digit, and special character. 12+ strongly recommended. The forced-change flag (must_change_password) restricts all API routes except change-password, logout, and session until the flag is cleared.
Password Reset (Admin)
Settings → Users → click the lock icon next to the user → set a temporary password. The user's existing sessions are immediately revoked.
LDAP / Active Directory
Quick Setup
- Navigate to Identity → Directory Service.
- Select a preset: Active Directory, OpenLDAP, FreeIPA, or Custom.
- Enter server address, Bind DN, Bind Password, and Base DN.
- Click Test Connection.
- Click Save Configuration and enable the toggle.
- Run a sync: Identity → Sync Now (or POST
/api/ldap/sync).
Group to Role Mapping
| LDAP Group | DPlaneOS Role | Access Level |
|---|---|---|
| IT_Admins | Admin | Full system access |
| Storage_Team | Operator | Storage, Docker, Shares |
| Domain Users | User | Files, Dashboard |
| Auditors | Viewer | Read-only |
Troubleshooting LDAP
| Issue | Solution |
|---|---|
| Connection failed | Verify server address, port (389/636), and firewall rules |
| Bind failed | Verify Bind DN and password; check service account is not locked |
| User not found | Check User Filter: AD uses sAMAccountName, OpenLDAP uses uid |
| No groups mapped | Verify Group Base DN and Group Filter |
| Admin locked out | User ID 1 always uses local auth. Log in with local credentials. |
TOTP Two-Factor Authentication
TOTP 2FA can be enabled per user. When enabled, login issues a pending_totp session that can only call /api/auth/totp/verify. All other routes reject the pending session until the second factor is verified.
Enabling TOTP
- Settings → Security → Two-Factor Authentication → Enable.
- Scan the QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.).
- Enter the first code to confirm setup.
- Save the backup codes in a safe location. They are shown once and stored hashed.
RBAC & Permissions
Built-in Roles
| Role | Can Do | Cannot Do |
|---|---|---|
| Admin | All 31 permissions; manage users, roles, system settings | N/A (cannot be deleted) |
| Operator | Storage, Docker, file management, sharing | Create users, assign roles, system settings |
| Viewer | Read all data, download files, view logs | Modify anything |
| User | Upload/download files, view own usage, read system status | Everything else |
Permission Reference
| Resource | Actions | Covers |
|---|---|---|
| storage | read, write, delete, admin | Pools, datasets, quotas, encryption, replication |
| snapshots | read, write | Snapshot schedules and management |
| shares | read, write, admin | SMB/NFS share configuration and reload |
| files | read, write | File Explorer: browse/download and upload/modify/delete |
| docker | read, write, delete, admin | Containers, images, compose, prune |
| network | read, write | Network interfaces and routing |
| firewall | read, write | Firewall rules |
| users | read, write, admin | User accounts, groups, and sessions |
| roles | read, write | Role and permission management |
| system | read, write, admin | System settings, reboot, poweroff, audit log rotation |
| monitoring | read | Metrics and health dashboard |
| audit | read | Audit log chain view and chain verification |
| certificates | read, write | TLS certificate management |
GitOps & state.yaml
The GitOps engine lets you declare the desired state of your NAS in a state.yaml file committed to a Git repository. The reconciler compares this declared state to the live system and applies the diff.
zfs get used is non-zero. A git push cannot delete data. Only metadata and configuration are managed.Top-Level Structure
version: "1" ignore_extraneous: false # if true, ignore resources not in desired state pools: [] # ZFS pools datasets: [] # ZFS datasets and properties shares: [] # SMB shares nfs: [] # NFS exports stacks: [] # Docker Compose stacks system: # hostname, DNS, NTP, firewall, networking, SSH users: [] # local user accounts groups: [] # local groups replication: [] # ZFS replication schedules ldap: # LDAP/AD directory integration acme: # ACME certificate automation certificates: [] # manually provisioned TLS certs smart_tasks: [] # SMART test schedules fabrics: # NVMe-oF targets (optional)
Pool Declaration
pools: - name: tank topology: data: - type: mirror disks: - /dev/disk/by-id/ata-WDC_WD40EFRX_00... - /dev/disk/by-id/ata-WDC_WD40EFRX_01... cache: # optional L2ARC - /dev/disk/by-id/nvme-... properties: ashift: 12 # 4K sector alignment autotrim: on # for SSDs
Dataset Declaration
datasets: - name: tank/media properties: compression: lz4 atime: off recordsize: 1M quota: 4T - name: tank/media/movies properties: compression: off # already compressed video
Reconciliation
On every POST /api/gitops/apply, the engine:
- Reads
state.yamlfrom the configured Git repository. - Reads the current live system state (ZFS, Docker, Samba, NFS, DB).
- Computes a diff plan: items to CREATE, MODIFY, DELETE, or block.
- Executes the plan transactionally (halts on first failure; already-applied items are safe by design).
- Verifies convergence by re-reading live state after apply.
A background drift detector runs every 5 minutes and broadcasts drift events to connected UI clients. Operators can trigger an immediate check or apply via the UI or API.
# Trigger apply via API curl -X POST http://localhost:9000/api/gitops/apply \ -H "X-Session-ID: your-session-token" # Check drift status curl http://localhost:9000/api/gitops/status \ -H "X-Session-ID: your-session-token"
Capture Workflow
The Capture workflow generates a state.yaml that represents the current live state of the system. Use it to bootstrap GitOps from an existing DPlaneOS installation.
GitOps → Capture → review the generated YAML → commit to your Git repository → set the repo as the GitOps source → apply.
After capture, the reconciler will show zero drift between the captured state.yaml and the live system (assuming no changes were made between capture and apply).
HA Topologies
Topology A: Shared-SAS (Recommended)
Both nodes connect to the same physical disk shelf. SCSI-3 Persistent Reservations enforce storage exclusion at the disk-controller level. No separate witness machine is required; the third etcd member for Patroni quorum runs co-located on node A as a lightweight process.
Best for: two-box deployments where you can connect both servers to a shared JBOD or disk shelf.
Topology B: Replicated ZFS (Stretched)
Nodes do not share physical storage. ZFS state is replicated over the network. A separate witness node provides the quorum tiebreaker. IPMI or SBD fencing ensures the non-surviving node is powered off before promotion.
Best for: geographically separated deployments. Requires a third machine (Raspberry Pi or VM) as the witness node.
| Feature | Shared-SAS | Replicated |
|---|---|---|
| Separate witness required | No | Yes |
| Storage fencing mechanism | SCSI-3 PR (hardware) | IPMI / SBD (software) |
| Geographic separation | No | Yes |
| Recommended for homelabs | Yes | Only if needed |
Patroni & etcd
PostgreSQL is replicated from primary to standby in streaming replication mode. Patroni manages leader election via etcd. Failover sequence:
- etcd detects the primary member is down.
- Patroni on the standby wins the election.
- Standby is promoted to primary.
- HAProxy (
:5000) begins routing to the new primary. - The daemon reconnects within seconds.
HAProxy runs on each node and listens on 127.0.0.1:5000. It health-checks Patroni (GET /primary returns 200 on primary, 503 on standby) and routes only to the current primary. The daemon always connects to localhost:5000.
RTO (Recovery Time Objective): 10-30 seconds for automatic PostgreSQL failover.
SCSI-3 PR Fencing
The dplane-fenced binary manages SCSI-3 Write Exclusive Registrants Only (WERO) persistent reservations. Reservations survive system reboots (APTPL=1 stores them in disk controller NVRAM).
- At startup: each node registers an 8-byte key derived from
/etc/machine-id. - Primary: holds the WERO reservation on all pool member disks.
- Graceful failover: primary calls
FencedRelease()before exporting pools. - Unclean failover: surviving node preempts the dead node's reservation via
FencedPreempt(device).
Result: the non-reservation-holder's I/O is rejected at the SAS/SCSI layer. Split-brain cannot cause data corruption in the shared-SAS topology.
dplane-fenced runs in its own systemd slice (dplaneos-fenced.slice), isolated from dplaneos.slice, so it survives dplaned restarts.
Rolling OTA Upgrades
- Update the standby node:
sudo dplaneos-ota-updateon node B. Node B reboots into the new slot. Health check passes. Node B is now on the new version. - Fail over: trigger a Patroni failover so node B becomes primary. The VIP moves to node B.
- Update the old primary:
sudo dplaneos-ota-updateon node A. Node A reboots. Health check passes. Node A comes up as standby on the new version. - Optionally fail back to node A.
Clients connected to the VIP see no downtime during steps 1 and 3 (the node being updated is not serving traffic). Step 2 causes a brief failover pause (~10-30s) which is the only client-visible interruption.
Backup: ZFS Snapshots
What Needs Backing Up
| Data | Location | Strategy |
|---|---|---|
| User files | ZFS pools on data disks | ZFS snapshots + ZFS send/receive |
| NAS config (runtime) | state.yaml in Git | git push to remote |
| NAS config (OS-level) | configuration.nix / flake.nix | git push to remote |
| Database | /var/lib/dplaneos/pgsql/ | pg_dump or ZFS snapshot of /persist |
| Docker state | /var/lib/docker/ | ZFS snapshot if on ZFS dataset |
ZFS Send/Receive (Replication)
# Initial full send to a remote host zfs send -R tank/data@snap1 | \ ssh backup@remote "zfs receive -F backup/data" # Incremental send (after the initial full send) zfs send -i tank/data@snap1 tank/data@snap2 | \ ssh backup@remote "zfs receive backup/data" # Local clone zfs send tank/data@snap1 | zfs receive tank2/data
Configure replication schedules via Storage → Replication → Add Schedule or declare them in state.yaml under replication. The daemon handles SSH key exchange and incremental sends automatically.
Cloud Sync (rclone)
Cloud sync is powered by rclone. Configure destinations from Settings → Cloud Sync → Add Destination. Supported providers include: AWS S3, Backblaze B2, Google Cloud Storage, Azure Blob, Wasabi, Cloudflare R2, and many more.
Schedules, bandwidth limits, and sync vs. copy modes are all configurable from the UI. Progress is visible in real time on the sync status page.
Database Backup
# Manual backup sudo -u postgres pg_dump dplaneos > \ /backup/dplaneos-$(date +%Y%m%d).sql # Restore sudo systemctl stop dplaned sudo -u postgres psql dplaneos < /backup/dplaneos-20260524.sql sudo systemctl start dplaned # Automate with a cron job 0 3 * * * sudo -u postgres pg_dump dplaneos | \ gzip > /mnt/tank/backups/db-$(date +%Y%m%d).sql.gz
The database state lives at /var/lib/dplaneos/pgsql/ on the /persist partition. A ZFS snapshot of the dataset containing /persist is also a valid backup approach.
A/B OTA Update System
The boot disk has two OS slots (A and B). At any time one slot is active (booted) and the other is standby. OTA updates write to the standby slot; the active slot is never touched during an update.
Boot disk layout: /boot (ESP) systemd-boot, slot A/B entries Slot A (ext4) NixOS closure (active or standby) Slot B (ext4) NixOS closure (active or standby) swap /persist (ext4) ALL durable state - NEVER touched by OTA
Health Check (post-boot)
90 seconds after booting into the new slot, the health check fires and verifies:
GET /api/system/healthreturns 200 within 10 secondszpool listexits 0 and shows no degraded pools/persistis mounted- If active SMB shares exist in the DB:
smbdmust be running
All checks pass → update committed. Any failure → bootloader reverted to previous slot, system reboots automatically.
Triggering Updates
Via the UI (Recommended)
Settings → System → Updates → Check for Updates. The UI shows current version, available version, changelog, and estimated install time. Click Install and Reboot to start the process.
Via CLI
# Pull the latest DPlaneOS flake and rebuild into the inactive slot sudo nixos-rebuild switch --flake github:4nonX/DPlaneOS#dplaneos # Or use the OTA update script directly sudo dplaneos-ota-update
Rollback & Revert
Automatic revert: happens without operator intervention if the health check fails. No action needed.
Manual rollback (after a health-check-passing update that still has issues):
sudo dplaneos-ota-update --revert
This sets the bootloader back to the previous slot and reboots. Data on /persist and ZFS data pools are never touched by a rollback.
services.dplaneos.ota.healthCheckDelay = "120s" in configuration.nix.Security Architecture
Trust Boundary
The daemon listens on 127.0.0.1:9000 only. It is not reachable from the network without a reverse proxy (nginx, Caddy, or Pangolin). TLS is terminated at the proxy. Everything behind the proxy is trusted.
Exec Allowlist
All exec.Command calls pass through internal/security/whitelist.go, which validates:
- The binary name (only known tools permitted)
- The subcommand (e.g.,
zpool addvalidated with a dedicated argument grammar) - Each argument individually against predefined regex patterns
Arguments are passed to exec.Command as a string slice, never as a shell string. There is no bash -c, no shell expansion.
Hardening Checklist
| Item | Recommendation |
|---|---|
| HTTPS | Enable via Settings → TLS. Use Let's Encrypt for public-facing, self-signed for internal. |
| Firewall | Open only ports 80, 443, 22. Close everything else. Configured in NixOS module or via UI. |
| TOTP 2FA | Enable on all admin accounts. Especially important for internet-facing deployments. |
| Audit log review | Review monthly. Look for failed logins, unexpected role changes, out-of-hours access. |
| Least privilege | Use Operator or Viewer roles for daily use. Reserve Admin for administrative tasks only. |
| DB password for LDAP bind | Use a read-only service account. The bind password is stored in PostgreSQL. |
| SSH keys | Password SSH login is disabled post-install. Keep your SSH private key secure. |
HMAC Audit Chain Verification
# Verify audit chain integrity via API curl http://127.0.0.1:9000/api/system/audit/verify-chain \ -H "X-Session-ID: your-token" # View audit log via UI # Settings -> Roles and Permissions -> Audit Log
Monitoring & Alerts
Dashboard Metrics
CPU, RAM, disk I/O, network traffic, pool health, and container status are displayed in real time via WebSocket. ZED events (scrub, resilver, faults, TRIM) surface immediately in the dashboard as they happen in the kernel.
Alert Channels
| Channel | Configuration | Notes |
|---|---|---|
| SMTP (email) | Settings → Notifications → SMTP | Requires an SMTP relay or mail server |
| Webhook | Settings → Notifications → Webhook | POST JSON to any endpoint (Slack, Discord, etc.) |
| Telegram | Settings → Notifications → Telegram | Bot token + chat ID. Critical ZFS events sent immediately. |
ZED Event Types
The ZFS Event Daemon hooks into the daemon and triggers alerts for: pool degraded, disk faulted, scrub started/completed/aborted, resilver started/completed, TRIM events, checksum errors, I/O errors, data loss events. All severity warning+ events are forwarded to configured alert channels.
Troubleshooting
Daemon Will Not Start
sudo journalctl -u dplaned -n 80 # Common causes: sudo systemctl status dplaneos-zfs-gate # ZFS gate timeout sudo systemctl status postgresql # DB not ready curl http://127.0.0.1:9000/api/system/health
Cannot Reach Web Interface
sudo systemctl status nginx
curl http://127.0.0.1:9000/api/system/health # test daemon directly
sudo journalctl -u nginx -n 20
ZFS Pools Not Visible
zpool list # check pool status zpool import -a # import any un-imported pools zpool import tank -f # force import (only if safe) sudo systemctl restart dplaned
Permission Denied (403)
# Check user's roles sudo -u postgres psql dplaneos -c \ "SELECT r.name FROM roles r \ JOIN user_roles ur ON r.id = ur.role_id \ WHERE ur.user_id = X;" # Clear permission cache sudo systemctl restart dplaned
High Memory / ZFS ARC
arc_summary # ZFS ARC breakdown # Limit ARC to 16 GB in /etc/modprobe.d/zfs.conf: # options zfs zfs_arc_max=17179869184
Build Issues (for developers)
# Use the Nix dev shell (provides Go, gcc, libzfs headers) nix develop # Offline / air-gapped build (uses vendored deps) cd daemon CGO_ENABLED=1 go build -mod=vendor \ -ldflags="-s -w -X main.Version=$(cat ../VERSION)" \ -o ../build/dplaned ./cmd/dplaned/
LDAP Admin Lockout
User ID 1 (the initial admin) always authenticates via local bcrypt, even when LDAP is enabled. Log in with the local admin credentials set during the setup wizard.
OTA Stuck in Auto-Revert Loop
Check what the health check is failing on:
cat /persist/ota/ota.log journalctl -u dplaneos-ota-health -n 50
Common causes: ZFS pools not imported fast enough (increase healthCheckDelay), daemon not starting before the check fires, PostgreSQL readiness issues.
Command Reference
Service Control
sudo systemctl status dplaned sudo systemctl restart dplaned sudo systemctl stop dplaned sudo journalctl -u dplaned -f # follow live logs sudo journalctl -u dplaned -n 100 # last 100 lines
Health & Version
curl http://127.0.0.1:9000/api/system/health cat /opt/dplaneos/VERSION
ZFS Quick Reference
zpool status # pool health overview zpool status -v tank # verbose with error counts zpool list # pool sizes and usage zfs list # all datasets zfs list -t snapshot # all snapshots zfs get all tank/data # all properties on a dataset zpool scrub tank # start a scrub zpool import -a # import all available pools zpool export tank # safely export a pool arc_summary # ARC cache statistics
Database Access
sudo -u postgres psql dplaneos # open psql shell sudo -u postgres psql dplaneos -c "\dt" # list tables sudo -u postgres psql dplaneos -c \ "SELECT name FROM roles;" # list roles sudo -u postgres pg_dump dplaneos > db.sql # backup
OTA Updates
sudo dplaneos-ota-update # apply latest update sudo dplaneos-ota-update --revert # revert to previous slot cat /persist/ota/ota.log # OTA operation log
Recovery Mode
sudo dplaneos-recovery # interactive recovery for lockout / DB issues
Key Paths
| Path | Contents |
|---|---|
| /opt/dplaneos/daemon/dplaned | Daemon binary |
| /opt/dplaneos/app/ | Web UI static files |
| /opt/dplaneos/VERSION | Current version string |
| /var/lib/dplaneos/pgsql/ | PostgreSQL data directory |
| /var/lib/dplaneos/audit.key | HMAC audit chain signing key (keep secure) |
| /var/lib/dplaneos/custom_icons/ | Custom container icons |
| /etc/dplaneos/patroni.yaml | Patroni HA configuration |
| /etc/dplaneos/snapshot-schedules.json | Snapshot schedule definitions |
| /etc/zfs/zed.d/dplaneos-notify.sh | ZED event hook |
| /var/log/dplaneos/ | Daemon logs |
| /persist/ | All durable state (bind-mounted from dedicated partition) |
| /persist/ota/ | A/B slot markers, pending-revert marker, OTA log |
| /run/dplaneos/dplaneos.sock | ZED event Unix socket |
| /run/dplaneos/fenced.sock | dplane-fenced control socket (HA only) |
Changelog
v11.6.5 (2026-05-26) - Docker Compose Editor Fix
Fixed: Compose create flow had no .env editor and the deploy call omitted the env field entirely - users could not supply environment variables when creating a new stack. Compose editor rendered blank immediately after deploying a new stack due to a race between selectStack and the stacks query refetch - fixed by awaiting the refetch before selecting. NixOS: dplaneos-patroni-init ran as postgres but writes to /etc/dplaneos/ (root-owned), preventing HA cluster bring-up - service now runs as root and chowns the config file to postgres:postgres afterwards. NixOS: ZED notify hook used bare nc which is not in the system closure - replaced with the full store path of socat so pool health events actually reach the daemon. NixOS: removed dead PGPASSFILE env var from dplaned service (Patroni uses trust auth on localhost). Removed dead inline ISO builder from flake.nix and stale v5.3.2 configuration.nix. frontendPackage module option is now explicitly required with no default; the HA failover NixOS test supplies its own empty derivation explicitly.
v11.6.4 (2026-05-24) — OTA Health Check Hardening
Fixed: OTA health check commits update when ZFS command fails — zpool list exit code now captured explicitly; non-zero exit increments checks_failed. OTA health check skips smbd gate when database is unreachable — psql failure now increments checks_failed instead of silently skipping the check.
v11.6.3 (2026-05-24) — Security & Error Propagation
Security: RBAC hierarchy enforcement silently bypassed on DB read failure — patched. Last-admin deactivation guard silently bypassed on DB read failure — patched. First-boot setup gate silent failure — patched. Audit log silent on pre-delete query failure — failures now logged.
v11.6.2 (2026-05-24) — Error Handling & Reliability
Fixed: Silent database write failures across all handler files. Missing rows.Err() checks after every iteration loop. Unchecked rows.Scan in iteration loops (could corrupt smb.conf on failure). Session revocation failures on password change now logged. Frontend: NixOS reconciliation errors now surface in UI. Network diagnostics auth fixed for "remember me" sessions. NixOS: dplaneos-sbd-init silently ignores zfs create failures — added set -eu. PostgreSQL readiness gate was a no-op — now actually checks connectivity.
v11.6.1 (2026-05-23) — UI Polish
Fixed: Undefined CSS tokens, missing root tokens, broken modal close hover states, password toggle using emoji instead of icon. Changed: Primary color saturation reduced. Outer glows removed from interactive elements. TopBar simplified. ZFS events sidebar shows parsed icon + label rows.
v11.6.0 (2026-05-22) — Infrastructure
Fixed: CGO daemon build compatibility with OpenZFS 2.3.7. OTA auto-revert after every successful update (wrong health endpoint). HA ZFS import guard infinite loop. HA cluster firewall ports not opened. Network config lost after OTA slot swap. etcd cluster state lost after OTA slot swap. OTA signing key never injectable. Patroni config never provisioned. Changed: Production daemon now uses libzfs CGO path.
Full history: CHANGELOG.md on GitHub