How to Deploy Netdata on a VPS
Updated Aug 2026
verified on Ubuntu 26.04 · Aug 2026Self-host Netdata on a VPS — per-second metrics for a whole machine in one command, plus the two things the default install gets wrong: an open dashboard on 19999 and retention nobody plans for.
- Any VPS — 256 MB of free RAM is enough, and it rides along on an existing box
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server, if you want the dashboard reachable remotely
- Docker Engine installed (see the base guide below)
- Caddy installed, if you want the dashboard behind HTTPS (see the base guide below)
What Netdata is
Netdata is a per-host monitoring agent: install it on a machine and it immediately collects thousands of metrics about that machine — CPU per core, memory, disk I/O, network interfaces, systemd units, containers, and whatever services it auto-detects — and renders them as per-second charts with no configuration and no dashboards to build. It is a C agent under GPL-3.0, it runs in a couple of hundred megabytes, and it is the fastest path on this site from an empty server to a live health view.
The per-second resolution is the actual selling point, and it is easy to undersell. Most monitoring samples every fifteen or sixty seconds, which means a spike that lasts four seconds does not exist as far as your graphs are concerned. Netdata sees it. When you are chasing a stall, a burst of I/O, or a process that briefly eats a core, that resolution is the difference between diagnosing the problem and guessing at it.
Be clear about the scope, because it is where Netdata differs from everything near it. Netdata's home is one host. It is not a fleet dashboard, not a query engine, and not a place to store years of history and ask questions of it. It can stream to a parent node and it can forward to a backend, but out of the box you are getting one deep, gorgeous, real-time view of one machine.
That makes the comparison with Grafana less of a rivalry than it looks — Netdata is the agent, Grafana (usually with Prometheus behind it) is the fleet-wide dashboard, and plenty of setups run both. Grafana vs Netdata walks through how they fit together. If what you want is up/down alerting across many services rather than deep metrics on one, Uptime Kuma is a different tool for a different job, and the two coexist happily.
Server sizing — the lightest thing here
Netdata is designed to ride along on a machine that is already doing something else, and it succeeds at that. The catalogue lists 256 MB of RAM, and in practice the agent's own footprint is small enough that the honest advice is: you do not size a box for Netdata, you install Netdata on the box you have.
Two caveats worth knowing before you assume it is free:
- The database costs RAM and disk in proportion to retention. More on that below, because it is the thing people underestimate.
- Per-second collection is per-second work. On a very small shared-CPU instance you will see the agent as a visible slice of a modest CPU budget. It is not a problem on any normal box; it is worth a look on the very cheapest tier if you are already close to the line.
A dedicated monitoring box is not required. If you want one — because you are also running dashboards, or because you want the monitoring to survive the monitored machine dying — the same reasoning as any monitoring host applies: somewhere reliable, ideally a different provider or region from the fleet you are watching. A Hetzner box or a small Kamatera instance has room to spare for the agent plus a dashboard layer on top.
Prepare the server
This guide assumes Docker Engine is installed along with a non-root deploy
user and a ufw firewall. If not, work through
Docker & Compose on Ubuntu first.
The firewall step matters more here than in most guides, so do it before you install rather than after:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status verbose
Do not open 19999. That is Netdata's dashboard port, and the next section explains why leaving it open is the mistake this guide exists to prevent.
Install Netdata
The official image, with host networking and the capabilities the agent needs to see the system properly:
docker run -d --name=netdata --pid=host --network=host \
-v netdataconfig:/etc/netdata -v netdatalib:/var/lib/netdata -v netdatacache:/var/cache/netdata \
--cap-add SYS_PTRACE --cap-add SYS_ADMIN --security-opt apparmor=unconfined \
--restart unless-stopped netdata/netdata
Every flag in there is doing something, and it is worth knowing what, because this is a deliberately privileged container:
--network=hostputs the agent on the host's network stack so it can read real interface statistics rather than the container's. It also means the dashboard binds directly to the host's port 19999 — see the warning below.--pid=hostlets it see the host's processes, which is where most of the interesting per-application metrics come from.SYS_PTRACE,SYS_ADMIN, and the AppArmor exception are what let it inspect other processes and mount points. This is a container with real visibility into the host. That is the deal you are making for zero-config monitoring; if that trade is unacceptable for a given machine, install the agent natively instead and confine it with systemd.- The three named volumes are configuration (
/etc/netdata), the metrics database (/var/lib/netdata), and cache. The middle one is the only one you would miss.
Give it a few seconds and check it came up:
docker logs netdata --tail 30
curl -s localhost:19999/api/v1/info | head -c 300
Close port 19999 — do this before anything else
Here is the honest part, and it is the reason this guide is not two paragraphs long.
The default install exposes an unauthenticated dashboard on port 19999. The open-source agent has no login screen. Anyone who can reach that port gets your hostname, your process list, your running services, your network interfaces, your disk layout, your container names, and a per-second view of what the machine is doing. That is a detailed reconnaissance report, served to the internet, by default. Scanners find port 19999 quickly.
It is worse than it looks with --network=host, because docker run -p does
nothing in host network mode. The usual 127.0.0.1:PORT:PORT trick that
keeps a container private is silently ignored here — the agent binds where its
own configuration tells it to, and the default is every interface. If your
mental model is "I did not publish the port so it is not exposed", that model
is wrong for this container.
Pick one of two fixes. Both are fine; do at least one.
Option A — block it at the firewall. Fastest, and it is what ufw is for:
sudo ufw deny 19999
sudo ufw status verbose
Option B — bind the agent to loopback. Cleaner, because the port is then
not listening publicly at all. Edit netdata.conf with the agent's own helper,
which fetches the current defaults and writes to the right place:
Netdata ships an edit-config helper. It opens an editor, so run it in your own
terminal rather than pasting it into a script:
docker exec -it netdata bash
cd /etc/netdata && ./edit-config netdata.conf
Set the bind address in the [web] section:
[web]
bind to = 127.0.0.1
Then restart the container:
docker restart netdata
Verify from outside the box, not from the server itself — a check from localhost proves nothing:
# give it a few seconds to come up, then confirm it answers locally
until curl -fsS -m 5 http://localhost:19999/ >/dev/null 2>&1; do sleep 3; done
curl -fsS -m 5 -o /dev/null -w 'netdata answered: HTTP %{http_code}\n' http://localhost:19999/
A refused connection or a timeout is the correct answer.
HTTPS + domain
With 19999 closed, put the dashboard behind a reverse proxy that terminates TLS and adds the authentication the agent does not have. The simplest path is Automatic HTTPS with Caddy.
Point an A record for netdata.example.com at the server's IP, wait for it
to resolve, then:
netdata.example.com {
basic_auth {
admin <bcrypt hash>
}
reverse_proxy 127.0.0.1:19999
}
Generate the hash with Caddy itself and paste the result in place of the placeholder:
caddy hash-password
(Older Caddy releases spell the directive basicauth; if your config fails to
load, that is the first thing to check.)
The authentication line is not optional. Proxying an unauthenticated dashboard to a public hostname over HTTPS gives you an encrypted connection to a wide-open dashboard — you have secured the transport and left the door open. Basic auth is the minimum. If you already run an identity provider, forward auth through that instead; if you would rather not expose it at all, skip the proxy and reach the dashboard over an SSH tunnel:
ssh -L 19999:127.0.0.1:19999 deploy@SERVER_IP
Then open http://localhost:19999 on your laptop. For a single-admin box this
is a perfectly good permanent answer, and it is the one with the smallest
attack surface.
One more thing you will meet on first load: the dashboard offers to connect the agent to Netdata Cloud, the vendor's hosted layer for viewing many agents in one place. You can decline and run entirely locally — the agent is fully functional without it, and this guide assumes you do. Connecting is a deliberate choice with its own data-sharing implications, not a step in the install.
Retention — the thing people underestimate
This is the second honest section, and it is where a "zero-config" install quietly disappoints people three weeks in.
Netdata's default retention is tuned to be light on the machine, not to answer questions about last month. It stores metrics in tiers: full per-second resolution for the recent window, then progressively coarser aggregates for longer periods. That design is exactly right for what Netdata is for — you look at now in extreme detail, and at the recent past in outline. But it means the answer to "what did the disk look like during the incident six weeks ago" is often that the data is no longer there at the resolution you want.
You have three options, and the right one depends on what you actually need:
- Accept it. For a lot of servers, per-second detail on the last while plus coarse history is genuinely all you want. This is the default for a reason.
- Extend it. Retention is configured in
netdata.confunder the[db]section, where you can raise the disk budget for each tier. More disk means more history; the trade is straightforward and you can measure it. Check what you are currently keeping in the dashboard's own retention view before guessing at numbers. - Ship it somewhere else. Netdata can export metrics to a long-term backend such as Prometheus, which is what you do when you want years of history, cross-host queries, and dashboards over a fleet. This is the point where Netdata becomes the collector in a bigger stack rather than the whole stack — Deploy Grafana on a VPS covers the other end of that pipe, and Prometheus is the usual middle.
Decide which of the three you are doing now, while nothing depends on the answer. The failure mode is discovering during an incident that the window you needed had already rolled off.
Alerts
Netdata ships with a large set of health alarms already active — it will notice a disk filling, a load spike, or a service dying without you configuring anything. What it does not have out of the box is anywhere to send them.
Notification methods live in health_alarm_notify.conf, edited the same way as
the main config:
Netdata ships an edit-config helper. It opens an editor, so run it in your own
terminal rather than pasting it into a script:
docker exec -it netdata bash
cd /etc/netdata && ./edit-config health_alarm_notify.conf
The file is long, heavily commented, and covers email, chat webhooks, push services and more. Fill in at least two independent channels — the useful failure is the one where your primary channel is also broken — and use one that reaches a phone. If you want that channel self-hosted too, ntfy is the small option.
Custom alarms go in /etc/netdata/health.d/ as individual files; the shipped
ones in the read-only stock directory are the reference to copy from. Restart
the container after editing, then test deliberately — fill a disk on a
throwaway box, or trigger an alarm you can safely trigger, and confirm the
message arrives on a real device. An untested notification path is a guess.
Backups
Netdata is unusual on this site in that its data is mostly not worth backing up. The metrics database is a rolling window that regenerates itself the moment the agent starts; if you lose it, you lose history you had already decided was disposable when you set retention.
What is worth keeping is the configuration — your bind address, retention settings, notification config and any custom alarms:
docker run --rm -v netdataconfig:/data -v $(pwd):/backup alpine \
tar czf /backup/netdata-config-$(date +%F).tar.gz -C /data .
Copy that off the box. Rebuilding from it is a docker run plus a tar
extraction into a fresh volume. If you do want the metrics to survive — for
instance because this agent is the only record of a machine's history — include
netdatalib in the same archive, and stop the container first so the database
is not mid-write.
Upgrades
docker pull netdata/netdata
docker rm -f netdata
# re-run the docker run command from the install section
Because this is a plain docker run rather than a compose file, upgrading
means recreating the container — which is why the named volumes matter. Keep
the run command in a file somewhere (or convert it to a small compose file) so
"re-run the install command" is a copy-paste rather than an archaeology
exercise.
Configuration lives in the netdataconfig volume and survives the recreate. If
a new version ships changed defaults, the edit-config helper shows the
current stock file alongside your overrides, which is the reason to edit
through it rather than writing config files directly.
Monitoring more than one machine
Once you have one agent you will want the rest of the fleet, and there are two shapes:
- An agent per host, each with its own dashboard. Simple, and each machine keeps its own data. It does not scale as a habit — nobody opens fifteen dashboards.
- Streaming to a parent. Agents on your other machines forward their
metrics to a designated parent agent, which holds the data and serves the
dashboards for all of them. The children can then keep almost no local
retention, which is nice on small boxes. Configuration is a shared API key in
stream.confon both ends.
The parent is the natural place to spend your retention budget, and the natural thing to put behind the authenticated proxy above. Beyond that scale — many hosts, long history, arbitrary queries — you are into Prometheus-and-Grafana territory, and Netdata's exporters are how you get there without throwing away the agents.
Troubleshooting
The dashboard is reachable from the internet even though I did not publish
the port. --network=host ignores -p. Use the ufw deny rule or the
loopback bind from the section above, then verify from outside the box.
Charts are missing, or containers and processes do not appear. A capability
was dropped from the run command. --pid=host, SYS_PTRACE, SYS_ADMIN and
the AppArmor exception are each responsible for a chunk of what the agent can
see. Compare your running container against the install command.
The proxy serves the page but charts stay empty. The browser loads the dashboard shell from one origin and then fetches data from the agent's API. If the proxy only forwards part of the path, or a trailing-slash rewrite mangles the API calls, you get a working-looking page with no data. Proxy the whole hostname to the agent rather than mounting it under a subpath, which is the setup that causes this almost every time.
History does not go back as far as I expected. That is retention doing what it was configured to do. See the retention section — check the dashboard's own retention view for what you are actually keeping, then raise the disk budget or export to a long-term backend.
Disk usage keeps climbing after raising retention. Working as intended: the tiers grow to fill the budget you gave them. Set the budget deliberately rather than to a number that sounded generous, and watch it for a week.
Alerts fire in the dashboard but nothing reaches me.
health_alarm_notify.conf has not been filled in, or the channel is
misconfigured. Test the channel directly rather than waiting for a real alarm.
The agent uses more CPU than expected on a small instance. Per-second collection across every auto-detected plugin is real work. Disable collectors you do not need rather than accepting the load — the dashboard lists what is running, and each plugin can be turned off in the config.
Verification + next steps
You are done when you can: load the dashboard over HTTPS with authentication in front of it (or through an SSH tunnel), confirm that port 19999 refuses connections from outside the box, see live per-second charts for CPU, memory, disk and network, and trigger an alarm that reaches you on a real device.
From there: decide your retention story before you need it, add agents to the rest of your machines and stream them to a parent, and pair it with a fleet-level dashboard when one host stops being enough — Deploy Grafana on a VPS is the next step, and the observability catalogue has the neighbours. For the ranked host picks, see Best VPS for Monitoring & Uptime.