01 At a glance #
ZenPlus ships as a self-contained appliance. There is no cluster to design, no package matrix to resolve and no follow-up configuration script — the installer provisions every component, verifies that it actually came up, and prints exactly what to do next.
$ curl -fsSL https://zentryc.com/install.sh | sudo bash
Safe to re-run at any time — the installer is idempotent and preserves data, credentials and licences.
Each of the 16 steps prints a single line and earns its checkmark only after its post-conditions actually hold — the binary exists, the database answers, the API returns healthy. The installer never reports success over a broken install: it stops at the first failure of a critical step, shows the last 25 lines of that step's output, and exits non-zero.
02 Requirements #
A dedicated virtual machine or physical server is recommended. The appliance runs its own PostgreSQL, Redis and ClickHouse instances, so a host that already runs those services will need manual cleanup first.
| Resource | Minimum | Recommended | Why it matters |
|---|---|---|---|
| Operating system | Ubuntu 20.04 LTS | Ubuntu 22.04 or 24.04 LTS | Server edition; other distributions are rejected at preflight |
| Architecture | x86_64 / amd64 | x86_64 / amd64 | 32-bit and ARM hosts are rejected |
| CPU | 2 cores | 4 cores | The Go poller is highly concurrent; ClickHouse scales with cores |
| RAM | 4 GB | 8 GB or more | ClickHouse plus the build toolchain will thrash below this |
| Disk | 20 GB free | 100 GB or more | Metric history grows with device count and retention |
| Access | root / sudo | root / sudo | Creates system users, services and firewall-adjacent settings |
2.1 · Outbound network access
Installation requires outbound HTTPS. The installer checks reachability before it writes anything and stops with a clear message if any host is unreachable — so a firewall problem costs you seconds, not ten minutes of a failed build.
| Destination | Port | Purpose | Needed |
|---|---|---|---|
github.com, raw.githubusercontent.com | 443 | Application source | Install & updates |
go.dev, dl.google.com | 443 | Go toolchain | Install |
deb.nodesource.com | 443 | Node.js (dashboard build) | Install |
download.docker.com, Docker Hub | 443 | Docker engine + ClickHouse image | Install & image upgrades |
archive.ubuntu.com, pypi.org, registry.npmjs.org | 443 / 80 | System, Python and Node packages | Install & updates |
zentryc.com | 443 | Licence registration and OTA updates | Continuously, once registered |
Export http_proxy and https_proxy before running the installer. If the proxy
terminates TLS, it must also allow streaming responses from zentryc.com — the OTA
agent uses chunked HTTPS for resumable downloads, and buffering proxies break it.
2.2 · Inbound (LAN) ports
| Port | Listener | Exposure |
|---|---|---|
| 80 / 443 | nginx → dashboard, API and agent traffic | Your LAN — the only ports users need |
| 8000 | zenplus-api (FastAPI) | Internal, proxied by nginx |
| 5432 / 6379 / 8123 / 9000 | PostgreSQL / Redis / ClickHouse | Loopback only |
No inbound port from the public internet is required. Registration and OTA updates are outbound-only.
2.3 · The preflight gate
The first step rejects unsuitable hosts in seconds rather than failing halfway through a build. It checks:
- OS family and release — Ubuntu 20.04 or newer; anything else is a hard stop, Debian raises an "untested" warning.
- CPU architecture — must be
amd64/x86_64. - Memory and free disk — against the minimums above, on the filesystem that will hold
/opt/zenplus. - Port conflicts — ports 80, 8000, 5432, 6379, 8123 and 9000 are inspected; unknown listeners raise a warning, ZenPlus's own services do not.
- Outbound HTTPS — a live reachability probe of every download host.
03 Run the installer #
On a fresh Ubuntu server, as root or with sudo:
curl -fsSL https://zentryc.com/install.sh | sudo bash
Equivalent, straight from the source repository:
curl -fsSL https://raw.githubusercontent.com/khuram2025/zen-mon/main/install.sh | sudo bash
Installation takes 8–15 minutes on a typical server — most of that is compiling the Go poller and building the React dashboard. Prefer to read before you run? Download first, inspect, then execute:
curl -fsSL https://zentryc.com/install.sh -o /tmp/zenplus-install.sh
less /tmp/zenplus-install.sh
sudo bash /tmp/zenplus-install.sh
3.1 · What you will see
The console stays a clean checklist. Every step reports one verified line, with the time it took:
╔══════════════════════════════════════════════════════════╗
║ ZenPlus — Network Monitoring Appliance ║
║ Automated Installer ║
╚══════════════════════════════════════════════════════════╝
Host mon-01 (10.20.30.40)
Source zen-mon.git branch main
Target /opt/zenplus
Log /var/log/zenplus-install.log
This takes 8–15 minutes on a fresh server. Safe to re-run.
[01/16] Validating host ✓ (2s)
[02/16] Installing system packages ✓ (3m14s)
[03/16] Creating service account ✓ (0s)
[04/16] Fetching application source ✓ (18s)
[05/16] Generating configuration ✓ (0s)
[06/16] Building application ✓ (5m02s)
[07/16] Initialising databases ✓ (1m11s)
[08/16] Provisioning trial licence ✓ (0s)
[09/16] Configuring services ✓ (12s)
[10/16] Installing TLS/security tools ✓ (1s)
[11/16] Configuring OTA updater ✓ (3s)
[12/16] Installing support tooling ✓ (2s)
[13/16] Installing management CLI ✓ (0s)
[14/16] Finalising installation ✓ (1s)
[15/16] Verifying appliance health ✓ (14s)
[16/16] Collecting system report ✓ (1s)
Installation summary
──────────────────────────────────────────────────────────
✓ 16 steps · 0 warnings · completed in 10m21s
──────────────────────────────────────────────────────────
✓ ZenPlus 1.11.0 is installed and running.
Access
Dashboard http://10.20.30.40
API docs http://10.20.30.40/docs
Sign in admin / admin123 (change this immediately)
Licence
Trial active — expires 2026-09-10
50 devices · 20 service checks · 5 users
Everything each step prints is captured in /var/log/zenplus-install.log, with
===== [n/16] Step title ===== markers. The console stays readable; the log holds the full detail.
3.2 · If a step fails
The installer stops at the first failure of a critical step, prints the last 25 lines of that step's output, and exits non-zero. Non-critical components — the TLS helper, OTA updater and support tooling — degrade to a warning and the install continues, leaving a working appliance with one feature to repair later.
Fix the reported cause and re-run the same command. The installer is idempotent: existing credentials, data directories, a changed admin password and any active subscription are all preserved.
04 The 16 steps, explained #
The steps run in a deliberate order — the application is built before the databases are initialised, because both the admin-hash repair and the trial seed need the Python virtual environment that the build step creates. Grouped by what they accomplish:
Solid chips are critical — a failure aborts the install. Dashed chips are non-critical: a failure warns and the install continues.
| # | Step | What it does | Critical |
|---|---|---|---|
| 01 | Validating host | Preflight: OS, architecture, RAM, disk, port conflicts and outbound reachability — before anything is written | Yes |
| 02 | Installing system packages | apt packages plus Docker, the Go toolchain and Node.js; enables non-root ICMP | Yes |
| 03 | Creating service account | Creates the unprivileged zenplus system user and its directories | Yes |
| 04 | Fetching application source | Clones or fast-forwards the repository into /opt/zenplus, preserving .env, data and backups | Yes |
| 05 | Generating configuration | Generates PostgreSQL, ClickHouse, Redis, JWT and SNMP-encryption secrets into .env (mode 0640). Existing values are kept on re-run. | Yes |
| 06 | Building application | Compiles the Go poller (with cap_net_raw), creates the Python venv, and builds the React dashboard | Yes |
| 07 | Initialising databases | Creates the PostgreSQL role and database, runs every schema migration, starts ClickHouse in Docker and applies its migrations | Yes |
| 08 | Provisioning trial licence | Seeds a 30-day trial with its entitlements — skipped if a subscription already exists | Yes |
| 09 | Configuring services | Writes the systemd units, the dependency-gate service and the nginx site, then enables and starts them | Yes |
| 10 | Installing TLS/security tools | Installs the certificate store and the privileged helper behind Settings → Security | No |
| 11 | Configuring OTA updater | Installs the updater service and timer, the polkit rule, the scoped sudoers entries and the default agent config | No |
| 12 | Installing support tooling | Installs the tech-support bundle generator used by Zentryc support | No |
| 13 | Installing management CLI | Drops the zenplus CLI at /usr/local/bin/zenplus | Yes |
| 14 | Finalising installation | Stamps the semantic version into .version and fixes ownership across the tree | Yes |
| 15 | Verifying appliance health | Polls the API health endpoint, confirms nginx serves the dashboard, and checks every service and container | Yes |
| 16 | Collecting system report | Records a post-install snapshot into the log for later support bundles | No |
Step 14 writes a semantic version — not a git commit — to the first line of
/opt/zenplus/.version. The OTA server matches releases on that value, so a commit hash there
would silently break update matching. Confirm it with head -1 /opt/zenplus/.version.
05 First login #
When the installer finishes it prints the access details for the appliance:
Go to Settings → General → Profile and set a real password before the appliance sees production traffic. Re-running the installer will not reset a password you have changed — it only repairs the stored hash if it is missing or corrupt.
06 Trial licence and registration #
A 30-day trial is provisioned automatically during installation. There is no activation step, no sales call and no feature gate — the appliance is fully usable the moment it boots.
Every product capability is available during the trial — SNMP, NetFlow, NCM, APM, discovery, agents, sensors and reporting. Nothing is withheld. See pricing for the node bundles available after the trial.
6.1 · What a licence key unlocks
A licence key is required for exactly one thing: receiving over-the-air updates. Until the appliance is registered, update checks return an error and the Licenses tab shows "Appliance Not Registered". Monitoring is entirely unaffected.
Open Settings → General → Licenses
Signed in as an administrator on the appliance dashboard.
Paste the licence key and press Register
The key is supplied with your subscription. The appliance registers itself with zentryc.com over outbound HTTPS.
Confirm from the shell
Run sudo zenplus status — the registration state is printed alongside service health.
Re-running the installer never resets, shortens or extends an existing subscription.
07 Enable HTTPS #
A fresh appliance serves plain HTTP. For anything beyond a lab, enable TLS before adding devices or agents — one action secures the dashboard, the API and all agent and sensor traffic at once.
Go to Settings → General → Security and pick the path that matches your environment:
Self-signed certificate
Generated on the appliance in one click. Download the certificate and distribute it to clients — browser trust store, or Active Directory GPO to Trusted Root Certification Authorities — to avoid warnings.
FastestCSR signed by your CA
Generate a CSR on the appliance and have your enterprise CA sign it, then paste the issued certificate back. With AD Certificate Services, use the Web Server template.
RecommendedUpload an existing certificate
Bring your own PEM certificate and key, or a PKCS#12 / PFX bundle issued by your existing PKI or a public CA.
Bring your ownThen enable HTTPS and, optionally, the HTTP→HTTPS redirect, HSTS, and a TLS 1.2+/1.3-only floor. See the Zentryc security model for how this fits the wider platform.
Changing to HTTPS changes the browser origin, so you will be asked to sign in again. Agents and sensors
keep the controller URL they enrolled with — re-point existing ones at https:// and
install the certificate into their host trust store.
08 Verify the installation #
Step 15 already asserted appliance health before the installer exited. To re-check at any time:
sudo zenplus status
Every service below should report active:
zenplus-wait-deps
zenplus-api
zenplus-poller
zenplus-updater.timer
nginx
postgresql
redis-server
ClickHouse runs in Docker and should report healthy:
docker ps --filter name=zenplus-clickhouse
zenplus-clickhouse Up 3 minutes (healthy)
A direct API health check bypasses nginx entirely:
curl -fsS http://127.0.0.1:8000/api/v1/system/health
{"status":"ok","service":"zenplus-api"}
And confirm the installed version — it must be a semantic version:
head -1 /opt/zenplus/.version
zenplus-wait-deps exists because PostgreSQL, Redis and ClickHouse boot asynchronously.
Without an explicit gate, the API and poller would race the databases on every reboot. The gate blocks
until all three answer health checks, then releases the API and poller together.
09 What gets installed #
Everything lands on one host. nginx is the only component listening to your network; the databases bind to loopback, the poller reaches out to your devices, and the updater reaches out to zentryc.com. Nothing calls in.
9.1 · Filesystem and services
| Component | Location |
|---|---|
| Application | /opt/zenplus |
| Configuration & secrets | /opt/zenplus/.env — mode 0640, owned by zenplus |
| Version marker | /opt/zenplus/.version |
| OTA agent configuration | /opt/zenplus/updater/config/agent.conf |
| PostgreSQL | System service, database zenplus |
| ClickHouse | Docker container zenplus-clickhouse |
| Redis | System service, password-protected |
| Web server | nginx → /etc/nginx/conf.d/zenplus.conf |
| TLS certificates | /etc/zenplus/tls — root-owned, private key 0600 |
| Services | zenplus-api, zenplus-poller, zenplus-updater.timer, zenplus-wait-deps |
| Management CLI | /usr/local/bin/zenplus |
| Install log | /var/log/zenplus-install.log |
9.2 · Privilege model
Nothing in ZenPlus runs as root beyond what the operating system itself requires:
- The API and poller run as the unprivileged
zenplussystem user. - The poller sends ICMP echoes via the
cap_net_rawfile capability — a capability on the binary, not a root process. - Dashboard-triggered update actions flow through a scoped sudoers file at
/etc/sudoers.d/zenplus-updaterthat enumerates the exactsystemctlinvocations permitted — nothing else — plus a polkit rule for the equivalent D-Bus calls. - Generated secrets live only in
/opt/zenplus/.envat mode 0640; TLS private keys live in/etc/zenplus/tlsat 0600, root-owned.
10 Managing the appliance #
The zenplus CLI is the day-to-day interface for anything that is not in the dashboard:
sudo zenplus status # service and registration status
sudo zenplus update # apply the latest release
sudo zenplus restart # restart the application services
sudo zenplus start # start everything, including databases
sudo zenplus stop # stop the application services
sudo zenplus backup # on-demand PostgreSQL dump
sudo zenplus logs api # tail a log — api | poller | updater
| Command | What it does | When to use it |
|---|---|---|
status | Prints service state, container health and OTA registration | First stop for any "is it healthy?" question |
update | Pulls, migrates, rebuilds, reloads and restarts | Applying a release manually, ahead of the OTA timer |
restart | Restarts the API, poller and nginx | After a configuration change |
backup | Dumps PostgreSQL into /opt/zenplus/backups | Before a risky change or a manual upgrade |
logs | Follows the journal for a chosen unit | Diagnosing a service that will not settle |
10.1 · Files worth knowing
/var/log/zenplus-install.log # complete installer output, per step
/opt/zenplus/.env # generated secrets and connection strings
/opt/zenplus/.version # semantic version + install timestamp
/opt/zenplus/updater/logs/update.log # OTA agent activity
/etc/nginx/conf.d/zenplus.conf # web server configuration
11 Staying up to date #
There are two update paths, and they lead to the same place.
Once the appliance is registered, zenplus-updater.timer checks zentryc.com for a newer
release, downloads it over resumable HTTPS, applies it, and reports the outcome back. Update
history is visible in the dashboard.
sudo zenplus updatePulls the current branch, runs any pending migrations, rebuilds the poller and dashboard, reloads nginx and restarts the services. Useful for air-gapped-adjacent environments or to apply a fix immediately.
An unregistered appliance monitors perfectly but cannot receive OTA updates — update checks return "not registered". Paste the licence key in Settings → General → Licenses to enable the channel. See §06.
12 Troubleshooting #
Where to look first: /var/log/zenplus-install.log holds the complete output of
every step, delimited by ===== [n/16] Step title ===== markers. Then:
sudo zenplus status
sudo systemctl status zenplus-api --no-pager
sudo journalctl -u zenplus-api -n 120 --no-pager -l
sudo journalctl -u zenplus-poller -n 120 --no-pager -l
docker ps
! Preflight rejects the host
http_proxy and https_proxy before running the installer, then re-run.! Login returns HTTP 500
passlib UnknownHashError.$, since an unescaped $2b$ is expanded by the shell, which is what corrupts the hash in the first place:sudo -u postgres psql -d zenplus -c \
"UPDATE users SET password_hash = '\$2b\$12\$vjHI8XBgL.dCyn.sgl41VufIFkQGcEzjt78GJdB66AwG9e9MZasai', is_active = true WHERE username = 'admin';"
That restores the password to admin123. Change it immediately afterwards.
! Poller fails with status=203/EXEC
zenplus-poller refuses to start; systemd reports exit code 203./opt/zenplus safe, build with -buildvcs=false, and fail the install if the binary is missing rather than continuing. To repair an existing appliance:sudo bash -lc 'set -e
export PATH=/usr/local/go/bin:/usr/local/bin:$PATH
git config --global --add safe.directory /opt/zenplus || true
mkdir -p /opt/zenplus/bin
cd /opt/zenplus/poller
go mod download
CGO_ENABLED=0 go build -buildvcs=false -o /opt/zenplus/bin/zenplus-poller ./cmd/poller
chown zenplus:zenplus /opt/zenplus/bin/zenplus-poller
chmod 0755 /opt/zenplus/bin/zenplus-poller
setcap cap_net_raw+ep /opt/zenplus/bin/zenplus-poller || true
systemctl restart zenplus-poller'
! Settings → Security is unavailable
sudo bash /opt/zenplus/scripts/setup-security.sh
sudo systemctl restart zenplus-api
! Update checks report "not registered"
sudo zenplus status.! Dashboard loads but returns 502
curl -fsS http://127.0.0.1:8000/api/v1/system/health
sudo systemctl status zenplus-wait-deps --no-pager
docker ps --filter name=zenplus-clickhouse
sudo journalctl -u zenplus-api -n 120 --no-pager -l
Still stuck? Contact Zentryc support and attach
/var/log/zenplus-install.log — it contains everything the installer saw, step by step.
13 Advanced install options #
The installer reads a handful of environment variables, so you can change its behaviour without editing the
script. Prefix them to the bash invocation:
curl -fsSL https://zentryc.com/install.sh | sudo ZENPLUS_BRANCH=release-1.11 bash
| Variable | Default | Effect |
|---|---|---|
ZENPLUS_BRANCH | main | Install a specific branch or release line |
ZENPLUS_REPO | Zentryc repository | Install from a fork or an internal mirror |
ZENPLUS_HOME | /opt/zenplus | Install to a different filesystem — useful when /opt is small |
INSTALL_LOG | /var/log/zenplus-install.log | Write the install log elsewhere |
TRIAL_DAYS | 30 | Length of the seeded trial licence |
MIN_RAM_MB / MIN_DISK_GB | 3500 / 20 | Preflight thresholds — lower only for lab builds |
MIN_RAM_MB and MIN_DISK_GB exist so lab and CI builds can proceed on small
hosts. They do not make ZenPlus run well below the documented minimums — ClickHouse and the build
toolchain will thrash. Do not lower them on a production appliance.
13.1 · Which version does a fresh install get?
install.sh clones the main branch and installs whatever version
that branch carries. Cutting a release fast-forwards main to the released commit, so a fresh
install always lands on the current release.
14 Publishing the installer Operators #
This section is for the team that runs zentryc.com, not for customers. The public one-liner is served as a static file and must not require login, API authentication or an access proxy.
In the server { server_name zentryc.com; … } block, before any generic application route:
location = /install.sh {
root /var/www/zentryc-public;
default_type text/plain;
add_header Cache-Control "public, max-age=300";
add_header X-Content-Type-Options "nosniff" always;
try_files /install.sh =404;
}
Publish or refresh the file:
sudo mkdir -p /var/www/zentryc-public
sudo curl -fsSL https://raw.githubusercontent.com/khuram2025/zen-mon/main/install.sh \
-o /var/www/zentryc-public/install.sh
sudo chmod 0644 /var/www/zentryc-public/install.sh
sudo nginx -t && sudo systemctl reload nginx
Verify from another machine:
curl -fsSIL https://zentryc.com/install.sh # expect HTTP 200, text/plain
curl -fsSL https://zentryc.com/install.sh | head -1 # expect #!/usr/bin/env bash
The served file is a manual snapshot, so it drifts from the repository every time the installer changes. Refresh it as part of every release, and confirm the served copy targets the intended branch:
curl -fsSL https://zentryc.com/install.sh | grep ZENPLUS_BRANCH=