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 (/api/*) and serves an embedded PWA. Runs 24/7 with a minimal
footprint and deploys on a Debian LXC + systemd, no Docker.
🇪🇸 Versión en castellano · License: AGPL-3.0
- 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), session cookies HMAC-SHA256
- Audit log for every mutating action; destructive ops require
{"confirm":"<name>"} - Demo mode (
DEMO=1): realistic mock data, all mutations return 403 — 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
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).
One-liner (downloads the latest release binary for your arch and installs everything):
curl -fsSL https://raw.githubusercontent.com/gnacho/easyzfs/main/deploy/install.sh | bashdeploy/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/env and the systemd unit, and verifies
startup. Supports --binary, --source, --port, --yes (non-interactive),
--uninstall and DRY_RUN=1 for a no-changes rehearsal.
bash deploy/install.sh --binary ./easyzfs --yesinstall -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 — the last one 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/sbin/lsblk, /usr/bin/crontab
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 session cookie will travel over HTTPS only).
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, except critical alerts, which 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 ("Enable alerts" button; the browser permission prompt appears only after that click).
| 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) |
hourly@:15 · daily@06:00 · weekly:sun@03:00 · monthly:1@02:00
(NAS local time; monthly accepts days 1-28).