EasyZFS is a ZFS management app that lives on the NAS itself: a single
static Go binary that wraps the system commands (zpool, zfs, smartctl,
hwmon sensors), exposes a REST + SSE API and serves an embedded PWA. It runs
24/7 with a minimal footprint and deploys on a Debian LXC + systemd. No
Docker, no appliance OS, no heavy stack.
After years with commercial NAS boxes (first Synology, then QNAP), I ended up with captive ecosystems running on hardware that aged fast and couldn't be upgraded. So I moved to an HP mini server running Proxmox, unifying NAS and homelab in one box. Proxmox has no friendly RAID management UI, so I tried a TrueNAS VM. Spoiler: it was a mistake. Everything worked, but every reboot meant containers waiting on a slow TrueNAS boot, it ate RAM, needed its own maintenance, and orchestrating it with the rest of the stack became a source of stress. The obvious fix won: drop TrueNAS and import the pools straight into Proxmox. Managing ZFS by hand taught me the commands, but I kept missing a small, friendly, minimal web UI for the day to day. I couldn't find one I liked, so I built it.
Framing matters: TrueNAS and OMV are complete NAS systems (shares, apps, VMs). EasyZFS is not trying to be one. It answers a narrower question: what do you use to manage your ZFS pools? In that category, this is the picture (full analysis in docs/easyzfs-vs-plataformas.md):
| EasyZFS | TrueNAS CE | OMV 8 | |
|---|---|---|---|
| RAM needed | tens of MB | 8 GB (16 recommended) | ~1 GB |
| Runs on | any Linux, VM, 256 MB LXC | its own appliance only | dedicated Debian |
| ZFS is | the whole product | the product | a third-party plugin |
zfs rewrite (real defrag) with UI |
✅ | CLI only | ❌ |
| Pool checkpoint with UI | ✅ | CLI only | ❌ |
| RAID-Z expansion with live progress | ✅ | ✅ | ❌ |
| Disk replacement recommendations (which disk, why, safety holds) | ✅ | ❌ | ❌ |
| Push alerts to your phone, zero third parties | ✅ (Web Push) | email/webhooks | |
| Live zed events + long-op progress (SSE) | ✅ | partial | ❌ |
| SMB/NFS shares, apps, VMs | ❌ (by design) | ✅ | ✅ |
Three points close the case:
- It runs where the others cannot. A 256 MB LXC on any distro is a full EasyZFS server. TrueNAS needs 8 GB and the whole machine; OMV needs a dedicated Debian.
- Alerts that reach your pocket. Real Web Push to your phone with the app closed: quiet hours, severities, ES/EN, no third-party service in between. A "degraded disk" notification in 5 seconds beats a historical report.
- No pivot risk. EasyZFS does one thing and will keep doing it. TrueNAS changed course three times in three years and retired CORE; OMV keeps ZFS alive through an extras plugin.
The honest reverse side: no SMB/NFS/iSCSI shares, no apps or VMs, and TrueNAS wins on granular RBAC, enterprise auditing, replication maturity and community. If you need a full NAS, use TrueNAS. If you need to control ZFS on any Linux with 50 MB of RAM and one binary, EasyZFS is the only tool in its category.
- Go, single static binary: a 24/7 daemon on a small LXC: ~12 MB, minimal RSS, no runtime to maintain, an upgrade is swapping one file.
modernc.org/sqlite(pure Go): keepsCGO_ENABLED=0so the binary is fully static and needs no C toolchain on the NAS. Only 2 Go dependencies, on purpose.- Collectors + cache, never CLI from HTTP: collectors poll
zpool/smartctl/sensors into an in-memory cache and publish SSE; handlers only read the cache. The UI stays instant no matter how slow the disks are. - systemd + limited sudoers, no Docker: the service runs unprivileged
and elevates only the whitelisted binaries (
zpool,zfs,smartctl…). Docker would add a layer without adding anything here. - React + Vite embedded PWA: the UI shell (theme, i18n, settings) is shared with my other apps, so fixes land everywhere at once.
- Pools, datasets, snapshots, disks (SMART), temperatures, live over SSE
- Scheduled jobs: snapshots, scrubs, SMART tests (
easyzfs-auto-*with per-job retention) - System tasks view: systemd timers + cron (read-only)
- Physical-disk inventory filter: zvols, loop, eMMC boot partitions and other pseudo-devices are hidden; eMMC/USB without SAT report SMART as "unknown"
- Multi-user auth (argon2id, roles admin/viewer), HMAC-SHA256 session cookies
- Audit log for every mutating action; destructive ops require
{"confirm":"<name>"} - Web Push alerts (pool capacity, DEGRADED/FAULTED, scrub errors, disk temperature, SMART warnings); see Push notifications
- Demo mode (
DEMO=1): realistic mock data, all mutations return 403, so it's safe to show off - PWA: installable, dark/light theme, i18n es/en/auto
- Embedded SQLite (WAL) for users, sessions, settings, alerts, series and job history
Pools: topology, health, rebuild offer when a free disk is detected**
Disks: SMART health, temperatures and power-on hours per physical disk**
Settings: theme, accent, users, push notifications and backups**
EasyZFS is a personal project, built for my own homelab and released as free software (AGPL-3.0). It is and will always be free. I can't dedicate full time to it: it evolves at my pace, following my own needs first, and I may be slow to answer issues. With contributions or support it might grow faster, but I can't promise anything. It works, it runs 24/7 in production at my home, and every release is tested there first: if I'm not using it myself, it doesn't ship.
Requirements: Linux (x86_64 or arm64) with systemd and ZFS, root access.
Interactive installer (downloads the latest release binary for your arch and installs everything):
curl -fsSL https://raw.githubusercontent.com/gnacho/easyzfs/main/deploy/install.sh | bash # (recommended)deploy/install.sh automates everything: detects the distro, installs ZFS
- smartmontools if missing, creates the service account (or root mode with
--root-mode), writes/etc/easyzfs/envand the systemd unit, and verifies startup. Supports--binary,--source,--port,--yes(non-interactive),--uninstallandDRY_RUN=1for a no-changes rehearsal. Read it before running: it's plain shell.
bash deploy/install.sh --binary ./easyzfs --yesManual installation
install -m 0755 easyzfs /usr/local/bin/easyzfs
useradd -r -s /usr/sbin/nologin easyzfs || true
install -d -o easyzfs -g easyzfs /var/lib/easyzfs
install -m 0644 deploy/easyzfs.service /etc/systemd/system/easyzfs.service
install -m 0440 -o root -g root deploy/easyzfs.sudoers /etc/sudoers.d/easyzfs
visudo -cf /etc/sudoers.d/easyzfs # validate syntax
# /etc/easyzfs/env (chmod 600, root:easyzfs 640):
# SESSION_SECRET=<long-random-string>
# ADMIN_PASSWORD=<first boot only; if unset, one is generated and logged once>
# LISTEN_ADDR=127.0.0.1:8080 # recommended if Nginx Proxy Manager lives on the same host
# DB_PATH=/var/lib/easyzfs/app.db
# COOKIE_SECURE=1 # once NPM serves SSL (Secure cookie)
systemctl daemon-reload && systemctl enable --now easyzfs
journalctl -u easyzfs -f # first boot: note the bootstrap password if generatedThe backend needs to run a handful of binaries as root (zpool, zfs,
smartctl, lsblk, crontab (only to read root's crontab
for the Tasks view), plus udisksctl/hdparm to power down free disks).
Editing system task schedules and migrating cron entries to systemd timers
goes through a confined root helper (/usr/local/libexec/easyzfs-sysd)
that only accepts three validated operations (cron-set, timer-set,
cron-to-timer) on whitelisted files. The service never gets free write
access to /etc/cron* or /etc/systemd. executil decides automatically:
if the process does not run as root, it prepends sudo -n to every
command; as root it runs them directly. Override with EASYZFS_SUDO=0|1
(default: auto). Two deployment options:
Option A: easyzfs user + limited sudoers (recommended). The service
runs unprivileged and can only elevate those binaries. This is the bundled
easyzfs.service setup (which is why it does not set
NoNewPrivileges=yes: sudo needs the setuid bit). Install
deploy/easyzfs.sudoers:
easyzfs ALL=(root) NOPASSWD: /usr/sbin/zpool, /usr/sbin/zfs, /usr/sbin/smartctl, /usr/bin/lsblk, /usr/bin/crontab -l, /usr/sbin/hdparm -y /dev/*, /usr/bin/udisksctl power-off -b /dev/*, /usr/local/libexec/easyzfs-sysd
crontab, hdparm and udisksctl are pinned to the exact arguments the
code uses (read-only crontab listing; disk standby/power-off), so they
cannot be abused as a root code-execution path.
Option B: conscious root. Change User=easyzfs/Group=easyzfs to
User=root in the unit (or set EASYZFS_SUDO=0 with another sufficiently
privileged user). A conscious, documented choice for an appliance whose
purpose is administering the system, but it grants far more than option A.
If a proxy sits in front (NPM/Caddy), SSE already sends
X-Accel-Buffering: no; in nginx also add proxy_buffering off for
/api/events.
Nginx Proxy Manager on the same host: use
LISTEN_ADDR=127.0.0.1:8080so the backend is only reachable through NPM, andCOOKIE_SECURE=1once NPM serves SSL.
The service reads /etc/easyzfs/env:
| Var | Default | Description |
|---|---|---|
LISTEN_ADDR |
:8080 |
Listen address |
DB_PATH |
/var/lib/easyzfs/app.db |
SQLite DB path |
SESSION_SECRET |
(ephemeral) | HMAC secret for sessions (set it in production) |
ADMIN_PASSWORD |
(generated) | First admin password (bootstrap) |
DEMO |
- | 1 = demo mode (mock + mutations blocked) |
MOCK |
- | 1 = mock collectors |
COOKIE_SECURE |
- | 1 = Secure cookie (behind TLS proxy) |
EASYZFS_SUDO |
auto | 1/0 forces or disables sudo -n on zpool/zfs/smartctl/lsblk/crontab |
RETENTION_DAYS |
30 |
Series retention (daily purge 03:30) |
VAPID_PUBLIC_KEY |
- | Web Push public key (installer-generated) |
VAPID_PRIVATE_KEY |
- | Web Push private key (server only; push disabled if missing) |
VAPID_SUBJECT |
mailto:easyzfs@localhost |
VAPID contact (mailto:, required by Safari) |
Restart after changes: sudo systemctl restart easyzfs.
DEMO=1:realistic mock data (poolstank/ssd, 7 disks, a live-progressing scrub over SSE) and all mutations return 403demo_mode.MOCK=1:same mock data but mutations try to run the real commands (they will fail without ZFS). For frontend development without a ZFS host.
EasyZFS can send Web Push alerts to your phone/browser with the app closed (pool capacity, DEGRADED/FAULTED pools, scrubs with errors, disk temperature, SMART warnings). With the app open you already get them live over SSE. Push is only for closed-app devices; critical alerts always notify.
- VAPID keys: the installer generates them automatically on first
install (
easyzfs -generate-vapid) and storesVAPID_PUBLIC_KEY,VAPID_PRIVATE_KEYandVAPID_SUBJECTin/etc/easyzfs/env. Reinstalls keep the existing keys (regenerating them would invalidate every subscription). Without keys the server simply starts with push disabled. - HTTPS required: Web Push only works on secure contexts.
localhostworks as-is; for remote access put EasyZFS behind Nginx Proxy Manager with SSL. - iOS/iPadOS: push requires the PWA installed on the Home Screen (Share → "Add to Home Screen"), then enable alerts from Settings.
- Enable them per device in Settings → Push notifications.
hourly@:15 · daily@06:00 · weekly:sun@03:00 · monthly:1@02:00
(NAS local time; monthly accepts days 1-28).
main.go wiring: config → db → collectors → scheduler → hub → HTTP
internal/
config/ env → validated struct
db/ SQLite (modernc.org/sqlite, WAL, busy_timeout) + migrations
settings/ settings (single JSON row) and alert thresholds
users/ multi-user, argon2id passwords, admin bootstrap
auth/ HttpOnly cookie sessions token|HMAC-SHA256 + role middleware
collectors/ zpool / smart / sensors / schedsys / maintenance / mock (in-memory cache)
actions/ real ZFS operations (whitelists, confirm, audit_log)
scheduler/ snapshot/scrub/smart jobs with custom schedule format
alerts/ thresholds (capacity, temp, SMART, scrub) → alerts table + SSE
hub/ SSE broker (25s heartbeat, X-Accel-Buffering: no)
httpapi/ REST handlers (read cache, NEVER run CLI)
model/ API contract types
executil/ defensive exec.CommandContext with timeout (auto sudo -n if not root)
HTTP handlers read the collectors' cache; they never run system commands. Long operations (scrub, resilver, SMART tests) are launched as actions and their progress is observed via the corresponding collector, which publishes SSE events.
See docs/api-contract.md. Summary:
easyzfs_session cookie auth; errors {"error","message"}; destructive
ops need {"confirm":"<name>"} and are recorded in audit_log; under
DEMO=1 mutations return 403 demo_mode.
Requirements: Go 1.23+, Node 20+ (only if rebuilding the front).
go mod tidy # go.sum is committed; tidy only if deps change
make build # = web (vite) + CGO_ENABLED=0 go build -o easyzfs .Go dependencies (kept to 2 on purpose):
modernc.org/sqlite:pure-Go SQLite driver: enablesCGO_ENABLED=0(static binary, no C toolchain needed on the NAS).golang.org/x/crypto:argon2.IDKeyfor password hashes (stdlib has no argon2).
- EasyZFS vs TrueNAS CE vs OpenMediaVault: an honest comparison of ZFS control features.
- ZFS defrag (
zfs rewrite) and roadmap comparison. - API contract.
- Security audit release: HTTP security headers on every response
(
X-Content-Type-Options: nosniff,X-Frame-Options: DENY, CSP withdefault-src 'self', HSTS, Referrer-Policy, Permissions-Policy). The CSP still allows the passive update check againstapi.github.com. - Roadmap: new phase P3 (security & robustness audit) in
docs/auditoria-2026-08-05-webzfs-zfdash.md.
- Stack updated: Go deps to date (
modernc.org/sqlitev1.56.0,golang.org/x/cryptov0.54.0) and frontend toolchain (Vite 8, TypeScript 7, Tailwind 4). No API or behavior changes.
- SMART drill-down: click a disk row to open its full SMART detail (complete attribute table: id, value, worst, threshold, raw, status; self-test history; error log), for ATA and NVMe. Data comes from the collector cache, never
smartctlon request. - Dataset properties: full property table per dataset (
zfs get all) with edit and inherit actions. Strict backend whitelist of editable properties and values (recordsize,atime,sync,quota,mountpoint, etc.), only safe and useful ones:dedup/encryptionstay out. - New endpoints:
GET /api/disks/{dev}/smart,GET /api/disks/{dev}/smart-log,GET /api/datasets/{name}/properties,PATCH /api/datasets/{name}/properties,POST /api/datasets/{name}/properties/{prop}/inherit. See API contract.