Netwarden
Back to Documentation
Installationv1.0

Self-Hosting Netwarden

Run Netwarden on your own hardware as a single binary. Zero-config defaults, SQLite storage, every alerting and security feature included.

Last updated: May 14, 2026
13 min read

Self-Hosting Netwarden

Netwarden ships as a single self-contained binary you can run on a laptop, a homelab box, or a small business server. There is no external database to manage, no cloud account to create, and no telemetry phoning home. This guide walks through installing, operating, and upgrading a self-hosted instance.

If you are evaluating whether to self-host or use the SaaS, see /self-hosted for the comparison. This page is for technical users who have already decided to run their own copy.

Why self-host

Self-hosting Netwarden is a sensible choice when:

  • Your monitoring data should not leave your network. Healthcare, government, financial, and air-gapped environments often need this regardless of how good a vendor's encryption story is.
  • You want a single, predictable binary. No JVM, no Python virtualenv, no node_modules directory to babysit. Drop the binary on a host, run it, done.
  • You already operate Linux in production. The agent ships as a static binary; the platform ships as one too. Both behave like any other systemd service you would deploy.
  • You want every feature, no gates. The self-hosted plan unlocks unlimited hosts, unlimited alerts, unlimited dashboards, custom alerts, advanced metrics, dashboard sharing, full API access, 365-day data retention, and the 60-second minimum check interval. Notification limits are effectively unbounded.
  • You want to air-gap the deployment. Every required component runs locally. Optional features (mobile push, GeoIP enrichment) need an internet path or a customer-supplied data file, but the core platform does not.

The trade-offs are honest: you operate the database, you run your own backups, you mirror CVE feeds yourself if you are air-gapped, and there is no support SLA unless you buy one. Read the Limitations & roadmap section before you commit.

System requirements

Netwarden is small. The numbers below are observed in practice on a fleet running 30 hosts at the 60-second check interval.

| Resource | Minimum | Comfortable | |---|---|---| | CPU | 1 vCPU | 2 vCPU | | RAM | 256 MB | 512 MB | | Disk | 200 MB free | 2 GB free | | Network | Inbound port 3000 from your agents | Same |

Idle memory usage hovers around 150 MB. The binary itself is roughly 50 MB. The SQLite database starts empty and grows by roughly 100 MB after a few weeks of metrics for a small fleet, depending on how aggressively you ingest.

Supported platforms:

  • Linux x64
  • Linux ARM64
  • macOS (Apple Silicon and Intel)
  • Windows x64

The agent supports the same platforms. There is no separate Postgres, Redis, or message broker to operate — the entire stack is the one binary plus the SQLite file it writes alongside itself.

Quick install (one binary)

The fastest path is to download the latest binary, mark it executable, and run it.

bash
# Linux x64
curl -sSL https://get.netwarden.com/selfhosted/latest/netwarden-linux-x64 -o netwarden
chmod +x netwarden
./netwarden

For other platforms, swap the URL suffix:

  • netwarden-linux-arm64
  • netwarden-darwin-arm64
  • netwarden-darwin-x64
  • netwarden-windows-x64.exe

When the process starts you will see log lines indicating that the SQLite database was created at ./data/netwarden.db, the schema was initialized, and the HTTP server is listening on port 3000. Open a browser to http://localhost:3000/auth/setup and complete the first-run wizard.

A note on the URL: the download path above follows the open-beta convention at the time of writing. If you reach a 404 or a different layout, check https://get.netwarden.com/ for the current path before filing an issue. Releases are published as standalone signed binaries — there is nothing to install, only to download.

Docker

If you prefer to run from a container image:

bash
docker run -d \
  --name netwarden \
  --restart unless-stopped \
  -p 3000:3000 \
  -v netwarden-data:/data \
  netwardenhq/netwarden:latest

A few notes:

  • The named volume netwarden-data persists /data/netwarden.db. Without it, your database vanishes when the container is recreated.
  • --restart unless-stopped is the right policy for a long-running monitoring service. always restarts even after manual docker stop, which is rarely what you want.
  • If you put Netwarden behind a reverse proxy, terminate TLS there. The binary itself does not currently serve HTTPS.
  • To upgrade, pull the new image tag and recreate the container; the volume keeps your data intact.

System service (systemd)

For Linux servers, run Netwarden as a non-root systemd service. Create a dedicated user, install the binary into a stable path, and let systemd manage lifecycle.

bash
# 1. Create a system user with no shell and no password
sudo useradd --system --home /var/lib/netwarden --shell /usr/sbin/nologin netwarden

# 2. Lay down the install layout
sudo mkdir -p /var/lib/netwarden/data /opt/netwarden
sudo curl -sSL https://get.netwarden.com/selfhosted/latest/netwarden-linux-x64 \
  -o /opt/netwarden/netwarden
sudo chmod +x /opt/netwarden/netwarden
sudo chown -R netwarden:netwarden /var/lib/netwarden

Then drop the unit file at /etc/systemd/system/netwarden.service:

ini
[Unit]
Description=Netwarden self-hosted platform
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=netwarden
Group=netwarden
WorkingDirectory=/var/lib/netwarden
ExecStart=/opt/netwarden/netwarden
Restart=always
RestartSec=5
Environment=PORT=3000
# DATABASE_URL is optional. Default is sqlite:./data/netwarden.db
# resolved relative to WorkingDirectory.
# Environment=DATABASE_URL=sqlite:./data/netwarden.db

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/netwarden/data

[Install]
WantedBy=multi-user.target

Enable and start:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now netwarden
sudo systemctl status netwarden

Logs land in the journal:

bash
sudo journalctl -u netwarden -f

First-run setup

The first time the binary boots with an empty database, the only meaningful page is /auth/setup. Every other authenticated route redirects to it until setup is complete.

The wizard collects:

  • The administrator email address
  • An administrator password

That is it. There is no plan picker, no payment step, no team-size form. The setup endpoint creates one tenant, one admin user, and one self-hosted subscription in a single transaction, then signs you in.

Once you are inside, additional users are created from Settings → Admin → Users. You can also manage roles (admin, editor, viewer), generate password resets, and disable accounts there. All users created on a self-hosted instance share the same tenant.

If you ever need to rerun the setup wizard (e.g. you have wiped the database), simply ensure the users table is empty and reload /auth/setup — the page is gated by user count, not by a flag, so it returns whenever there are zero users.

Installing the agent on your hosts

The agent is the same binary across SaaS and self-hosted. The only difference is what URL and credentials you point it at.

  1. Sign in to your self-hosted instance.
  2. Open Settings → API Keys and generate an agent token. The token is shaped like nw_sk_… and is what the agent uses to authenticate snapshot uploads.
  3. On the host you want to monitor, install the agent:
bash
curl -sSL https://get.netwarden.com/install.sh | sudo bash -s -- \
  --api-url=http://your-netwarden-host:3000 \
  --api-key=nw_sk_xxxxxxxxxxxxxxxxxxxx

If your Netwarden instance lives behind a reverse proxy at https://netwarden.example.com, use that as the --api-url. The flag is required for self-hosted installs because the installer otherwise defaults to https://api.netwarden.com.

The installer drops a config file at /etc/netwarden/netwarden.conf, registers a systemd unit, and starts the agent. Within roughly a minute you should see the host appear in the dashboard.

For unattended installs (Ansible, Terraform user-data, Packer), the same flags work in non-interactive mode. There is no separate Tenant ID flag for self-hosted installs — the API key encodes the tenant.

Configuration env vars

Netwarden reads configuration from environment variables. With zero variables set, it picks sensible defaults; the only ones most operators ever touch are PORT and GEOLITE2_CITY_PATH.

| Variable | Default | Purpose | |---|---|---| | PORT | 3000 | HTTP port the platform binds to. | | DATABASE_URL | sqlite:./data/netwarden.db | Database connection. SQLite is the only supported scheme today. The path is resolved relative to the working directory. | | SESSION_SECRET | auto-generated and persisted on first run | Secret used to sign session cookies. If unset, the binary generates one and stores it in a config file alongside the database. | | OAUTH_TOKEN_ENCRYPTION_KEY | unset | 32+ character key required only if you enable Google or GitHub OAuth login. Optional. | | GEOLITE2_CITY_PATH | unset | Absolute path to a MaxMind GeoLite2-City .mmdb file. Enables country-aware findings on failed-login bursts. Optional. | | FIREBASE_SERVICE_ACCOUNT_JSON | unset | Inline Firebase service-account JSON. Enables mobile push notifications. Optional. | | FIREBASE_SERVICE_ACCOUNT_PATH | unset | Path to a Firebase service-account JSON file. Alternative to the inline form above. Optional. |

Unset optional variables degrade gracefully. Without GEOLITE2_CITY_PATH, geographic findings are simply not produced. Without Firebase credentials, mobile push is disabled but the rest of alerting (email, webhooks) keeps working.

GeoIP setup

Two of the fourteen security findings depend on GeoIP enrichment: failed_login_high_risk_country and failed_login_country_anomaly. The agent always reports source IPs for failed SSH logins; the platform turns those into countries only if a MaxMind GeoLite2 database is mounted.

The MaxMind database file is not redistributable — even the free tier requires a per-account license key. You ship it yourself.

  1. Sign up for a free MaxMind account at maxmind.com/en/geolite2/signup.
  2. Generate a license key in your MaxMind account portal.
  3. Download the latest GeoLite2-City.mmdb (roughly 60 MB) using the license key. The binary is updated twice a week by MaxMind; you can either download manually on a cadence or run the official geoipupdate daemon as a cron job.
  4. Drop the file somewhere on the host and point Netwarden at it:
bash
mkdir -p /var/lib/netwarden/geoip
mv ~/Downloads/GeoLite2-City.mmdb /var/lib/netwarden/geoip/
sudo chown netwarden:netwarden /var/lib/netwarden/geoip/GeoLite2-City.mmdb

# In your systemd unit, add:
# Environment=GEOLITE2_CITY_PATH=/var/lib/netwarden/geoip/GeoLite2-City.mmdb

Restart the service. On the next boot the platform logs GeoLite2 reader loaded and country fields start populating on failed-login snapshots.

For the deeper Kubernetes-flavoured walkthrough (PVC, geoipupdate CronJob, validation steps), see the engineering deployment notes in the platform repository under platform/docs/geoip.md.

CVE feed updates

Netwarden derives cve_match findings by joining a host's installed-package snapshot against advisory data pulled from upstream feeds:

  • Ubuntu USNubuntu.com/security/notices.json
  • Debian DSAsecurity-tracker.debian.org
  • Red Hat OVAL — published OVAL XML for RHEL 7/8/9
  • NVD — used to enrich severity with CVSS v3 scores

A daily cron job inside the platform process refreshes all four feeds and re-runs the matcher. There is no separate worker process to operate.

In an air-gapped environment you have two options:

  1. Mirror the feeds. Pull the same upstream URLs on a schedule from a host that does have internet access, then serve the mirrored copies inside your network. An override mechanism for the feed URLs is on the roadmap; until it ships, this requires patching the feed module endpoints.
  2. Run a periodic side-load. Connect the platform to the internet only for the duration of the daily refresh, or stage advisory data into the cve_advisories and cve_affected_packages tables directly via SQL.

Distributions outside the four feeds (SUSE, Arch, Gentoo, Alpine) do not currently produce cve_match findings. The other thirteen finding types still work on those hosts.

Backups

The entire state of a self-hosted Netwarden instance is the SQLite file at ./data/netwarden.db (or wherever DATABASE_URL points). Back up that file and you can restore the instance.

A simple nightly backup using SQLite's online backup command:

bash
# /etc/cron.daily/netwarden-backup
#!/bin/sh
set -eu
BACKUP_DIR=/var/backups/netwarden
mkdir -p "$BACKUP_DIR"
DEST="$BACKUP_DIR/netwarden-$(date +\%Y\%m\%d).db"
sqlite3 /var/lib/netwarden/data/netwarden.db ".backup '$DEST'"
gzip -f "$DEST"
find "$BACKUP_DIR" -name 'netwarden-*.db.gz' -mtime +30 -delete

.backup is safe to run while the platform is live — it produces a consistent snapshot without locking writers.

For belt-and-braces operators, snapshotting the entire data/ directory at the filesystem level is also fine as long as the underlying filesystem provides consistent snapshots (LVM, ZFS, EBS, btrfs).

Upgrading

Upgrades are a binary swap.

bash
sudo systemctl stop netwarden
sudo curl -sSL https://get.netwarden.com/selfhosted/latest/netwarden-linux-x64 \
  -o /opt/netwarden/netwarden.new
sudo chmod +x /opt/netwarden/netwarden.new
sudo mv /opt/netwarden/netwarden.new /opt/netwarden/netwarden
sudo systemctl start netwarden

Schema migrations run automatically on first boot of the new binary. The migrations are SQLite-to-SQLite and additive — no destructive column drops, no incompatible renames. That said, always take a fresh backup before upgrading. SQLite is fast enough that this costs you seconds, not minutes.

If a migration fails, the platform refuses to start and logs the failing statement. Roll back by stopping the service, restoring the pre-upgrade backup, and replacing the new binary with the previous one.

Limitations & roadmap

Honest list. None of these block a small self-hosted deployment, but they are real:

  • SQLite only today. The SaaS deployment runs on PostgreSQL with TimescaleDB. A Postgres mode for self-hosted instances is on the roadmap so customers with very large fleets can scale beyond what SQLite comfortably handles.
  • No clustering. The single binary is a single process. There is no built-in active-passive failover, no horizontal sharding. For most homelab and small-IT deployments this is fine; if you need HA, plan for periodic backup/restore drills.
  • Mobile push needs Firebase + internet. Push notifications to the iOS and Android apps are delivered via Firebase Cloud Messaging. If you cannot supply Firebase credentials or your platform has no internet egress, push is unavailable. Email and webhook delivery are unaffected.
  • GeoIP is not bundled. MaxMind's license forbids us from shipping the .mmdb file. You supply it.
  • CVE coverage is Ubuntu, Debian, and Red Hat-family only. SUSE, Arch, Gentoo, and Alpine package matching is not implemented.
  • Community tier is single-tenant. SSO, SAML, and per-team RBAC beyond the three built-in roles are not in the Community build.
  • Pro tier is private beta. A paid self-hosted Pro tier with extended support, SSO, and additional features is in development. If that fits your shop, get in touch.

Troubleshooting

A handful of issues account for almost every support ticket on self-hosted instances. Run through these before opening one.

Port 3000 is already in use.

Set PORT to something free before starting the binary, or stop whatever else is bound to 3000. With systemd, edit the unit file and reload.

bash
PORT=8080 ./netwarden

Database is locked.

SQLite supports exactly one writer at a time. This error usually means you started the binary twice from different working directories, both pointing at the same database file. Confirm with ps -ef | grep netwarden and lsof | grep netwarden.db. The fix is to stop the duplicate process; the database is fine.

GeoIP enrichment isn't producing country fields.

Check the platform log on startup. You want to see GeoLite2 reader loaded with the path you configured. If you see GEOLITE2_CITY_PATH unset; geo enrichment disabled, the env var did not propagate (most commonly in systemd — Environment= lines inside [Service], not [Unit]). If you see GeoLite2 mmdb file not found, the path is wrong or the file is unreadable by the netwarden user.

The first-run setup wizard keeps reappearing after I create an account.

The wizard is gated by the user count: zero users means setup is required. If you are bouncing the platform and re-arriving at /auth/setup, your form submission did not actually persist. Check the platform logs for the POST to /api/auth/setup and look for transaction errors. The most common cause is a read-only data/ directory.

An agent reports connection refused against the platform.

Three things to check, in order:

  1. Confirm the agent's --api-url matches your platform's listening address. The default agent installer points at https://api.netwarden.com; for self-hosted you must override it.
  2. Confirm the host firewall allows inbound on PORT. On Ubuntu that is sudo ufw allow 3000/tcp.
  3. Confirm the API key is valid and not revoked. Check Settings → API Keys in the dashboard.

The dashboard loads but no metrics are populating.

Look at agent-side logs first (sudo journalctl -u netwarden -n 200). Successful uploads log a status line per submission. If the agent is silent, it has not started; if it is logging 401s, the API key is wrong; if it is logging 403s, the host has been quarantined.

Where to get help

  • Community Discord — the fastest channel for "I am stuck" questions. Other operators are usually around within an hour or two.
  • GitHub Issues — for bugs, regressions, and concrete reproductions. Include the platform version (visible at /admin/about), the OS, and the relevant log lines.
  • Pro support — if you need a contracted SLA, escalation paths, and engineering touchpoints, the Pro tier is the right channel. Contact us at [email protected].

Self-hosted Netwarden is meant to be boring, predictable, and yours. If something about your deployment is none of those things, that is a bug — please tell us.

Was this page helpful?

Help us improve our documentation

Edit on GitHubReport an Issue