How to Deploy Zabbix on a VPS
Updated Aug 2026
Self-host Zabbix on a VPS — the full package install, database schema import, nginx frontend and agent, with the two steps that quietly break on a fresh Ubuntu box.
- A VPS with real memory — 4 GB is a sane starting point, most of it for the database
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server, on a box separate from what you monitor
- Comfort with apt, systemd and editing config files — this is not a container install
What Zabbix is
Zabbix is enterprise infrastructure monitoring: agents on every host, plus SNMP, IPMI, JMX and agentless checks, wrapped in templates, network auto-discovery, escalation-aware alerting and long-term trend storage. It is AGPL-3.0, free to self-host with no host or user cap, and it is the open-source answer to a Datadog-shaped bill.
It is also, by a distance, the heaviest deploy on this site. Zabbix is not a container you start and forget — a working install is four moving parts:
- the Zabbix server, a C daemon that does the collecting, the trend calculation and the alerting
- a database — MySQL, MariaDB, Percona or PostgreSQL — which is where almost all the load and almost all the disk goes
- a PHP frontend served by nginx, which is the web UI you actually look at
- an agent on every host you want to watch, including this one
Upstream ships packages from its own apt repository rather than a one-line
container, and the package name encodes your database choice
(zabbix-server-mysql versus zabbix-server-pgsql), so the decision is made
at install time and is awkward to reverse. This guide takes the MySQL path,
which is what the packages in our install snippet assume.
Be honest about whether you need this. Zabbix wants a database server, a web server, a PHP-FPM pool and an agent everywhere before it draws a single graph, and its payoff — templates, auto-discovery, escalation chains, proxies for remote sites — only shows up when you are watching an estate rather than a handful of boxes. For three servers, Netdata or Uptime Kuma gets you a live view in one command, and Zabbix vs Checkmate walks through that fork properly. If you are here because you have racks, VMs, switches and an on-call rota, read on.
Server sizing
Zabbix sizing is really database sizing. The server daemon itself is modest; what grows is the volume of collected values, the history retention window and the trend tables behind it — and that lands squarely on MySQL.
Upstream's smallest published sizing example is 2 CPU cores and 8 GiB of memory for roughly a thousand monitored metrics. Read that as an example, not a floor. It is one point on a curve, published to show the shape of the scaling, and a lab install watching a few hosts runs comfortably in far less. Size against your own metric count and retention, not against that number.
- 2 GB RAM / 2 vCPU — a lab or a handful of hosts, short retention. It works; it is not where you want to be six months in.
- 4 GB RAM / 2–4 vCPU — the sane default for a small real fleet, what the catalogue lists, and the size this guide assumes.
- 8 GB+ and more cores — hundreds of hosts, thousands of items, or long retention with several concurrent frontend users.
Disk deserves its own thought: history and trends accumulate continuously, and the thing that grows is the MySQL data directory. 40–80 GB of fast NVMe is a comfortable start for a small fleet — watch it for a month before deciding whether to trim retention or buy disk. This is the workload where a host that sells generous RAM and disk per dollar earns its keep, so Hetzner and Contabo both make the shortlist.
And the placement rule that applies to every monitor: do not run Zabbix on the machine it monitors. Give it its own box, ideally on a different provider or region from the fleet it watches.
Prepare the server
Start from a fresh Ubuntu 24.04 or 26.04 install, update it, and make yourself a non-root user:
sudo apt update && sudo apt upgrade -y
sudo adduser deploy && sudo usermod -aG sudo deploy
Lock the firewall down to SSH plus the reverse proxy ports. Zabbix's own frontend port stays internal, and so does MySQL:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status verbose
One port you will need to open later, once you start adding hosts: 10051/tcp, which is where the Zabbix server receives data from agents in active mode. Leave it closed for now and open it to specific source addresses when you know which hosts are reporting in. Agents in passive mode listen on 10050 on their side, which means the agent boxes need the inbound rule, not this one.
Install the database server
This is the first thing that catches people out, and it is the reason the
install snippet on the app page starts where it does. Every Zabbix walkthrough
you will find runs sudo mysql to create the database — but a fresh Ubuntu VPS
has no MySQL server on it. sudo mysql on a bare box does not fail
interestingly; it fails with a socket error that reads like a permissions
problem, and people lose twenty minutes to it.
Install the database server first, before anything Zabbix-related, and confirm it is actually up:
sudo apt install -y mysql-server
sudo mysql -e "select version();"
If that prints a version, the socket works and sudo mysql will behave for the
rest of this guide. If it does not, stop and fix it here — every later step
depends on it.
Add the Zabbix repository
Upstream publishes a small zabbix-release .deb whose only job is to drop
their apt repository and signing key onto your system. The download URL encodes
your distribution release, and this is the second thing that breaks.
Most guides — and, until recently, our own snippet — hardcode a path like
ubuntu24.04. Install that on a 26.04 box and apt happily accepts the release
package and then fails to resolve a single Zabbix package, because the
repository it points at is built for a distribution you are not running. The
error arrives one step later than the mistake, which is what makes it annoying.
The fix is to stop hardcoding it. /etc/os-release already knows which release
you are on, so source it and interpolate ${VERSION_ID}:
. /etc/os-release && wget https://repo.zabbix.com/zabbix/7.4/release/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest+ubuntu${VERSION_ID}_all.deb
sudo dpkg -i zabbix-release_latest+ubuntu${VERSION_ID}_all.deb && sudo apt update
That is correct on any Ubuntu release upstream builds for, today and after your
next dist-upgrade. The 7.4 segment in the path is the Zabbix release line,
not your distribution — upstream's download page lists the lines they currently
publish, and swapping that segment is how you move between them later.
If apt update prints a 404 for the Zabbix repository, upstream has not built
packages for your Ubuntu release yet. That is a real answer, not a
misconfiguration: use the previous LTS, or take the container route instead.
Install the Zabbix packages
Four packages, matching the four moving parts, plus the SQL scripts that create the schema:
sudo apt install -y zabbix-server-mysql zabbix-frontend-php zabbix-nginx-conf zabbix-sql-scripts zabbix-agent
zabbix-server-mysql— the server daemon, built against MySQLzabbix-frontend-php— the PHP web UIzabbix-nginx-conf— a ready-made nginx server block for that frontendzabbix-sql-scripts— the schema, shipped gzipped, imported by hand belowzabbix-agent— the agent, so this box monitors itself from day one
Nothing works yet: the server has no database to talk to and no password to talk to it with. That is the next two sections.
Create the database and import the schema
Create the database, a dedicated user, and grant it access. Change
CHANGEME — pick a long random password and keep it somewhere, because you
need it twice in a moment:
sudo mysql -e "create database zabbix character set utf8mb4 collate utf8mb4_bin; create user zabbix@localhost identified by 'CHANGEME'; grant all privileges on zabbix.* to zabbix@localhost;"
The character set and collation are not decoration — utf8mb4 with the _bin
collation is what Zabbix expects, and a database created with your server's
defaults will produce confusing failures later.
Now import the schema. It is a gzipped SQL script that creates a couple of hundred tables, so this is the slow step — expect it to run for a minute or more on a small box, with no progress output. It will prompt for the password you just set:
zcat /usr/share/zabbix/sql-scripts/mysql/server.sql.gz | mysql --default-character-set=utf8mb4 -uzabbix -p zabbix
Verify it landed before moving on:
sudo mysql -e "select count(*) from information_schema.tables where table_schema = 'zabbix';"
A count in the low hundreds means the schema is in. Zero, or a single-digit number, means the import died partway — read the error, drop the database, and run it again rather than importing on top of a half-built schema.
If the import fails with ERROR 1418 (something about deterministic
functions and binary logging), your MySQL has binary logging on and will not
create the stored functions Zabbix ships. Allow it for the duration of the
import and turn it back off afterwards:
sudo mysql -e "set global log_bin_trust_function_creators = 1;"
# re-run the zcat | mysql import here
sudo mysql -e "set global log_bin_trust_function_creators = 0;"
Configure the server and the frontend
Two files need editing by hand. Neither has a sensible default, and skipping either leaves you with services that start cleanly and do nothing.
First, the server's database password. Open
/etc/zabbix/zabbix_server.conf, find the commented DBPassword line, and set
it to the password you gave the zabbix user:
sudo nano /etc/zabbix/zabbix_server.conf
DBName=zabbix
DBUser=zabbix
DBPassword=CHANGEME
That file now holds a credential, so tighten it:
sudo chown root:zabbix /etc/zabbix/zabbix_server.conf
sudo chmod 640 /etc/zabbix/zabbix_server.conf
Second, the nginx server block. The zabbix-nginx-conf package installs
/etc/zabbix/nginx.conf with its listen and server_name directives
commented out — deliberately, so installing the package does not hijack port 80
on a box that is already serving something. Uncomment both:
sudo nano /etc/zabbix/nginx.conf
listen 8080;
server_name zabbix.example.com;
Port 8080, not 80. The reverse proxy in the next section owns 80 and 443 and forwards to this. Leaving Zabbix on 8080 behind the proxy is what keeps the PHP frontend off the public internet.
Start everything
Three services, plus a PHP-FPM restart so it picks up the frontend's configuration:
sudo systemctl enable --now zabbix-server zabbix-agent nginx
sudo systemctl restart "php*-fpm"
The quoted glob is deliberate — the PHP-FPM unit name carries the PHP version your distribution ships, and it changes under you across releases.
Check all three came up:
systemctl is-active zabbix-server zabbix-agent nginx
sudo tail -n 40 /var/log/zabbix/zabbix_server.log
The server log is the honest one. A clean start says it connected to the
database and began housekeeping; a wrong DBPassword says so plainly and the
daemon exits rather than limping.
HTTPS + domain
Point an A record for zabbix.example.com at the server's public IP and
wait for it to resolve, then terminate TLS in front of the frontend. The
simplest path is
Automatic HTTPS with Caddy:
zabbix.example.com {
reverse_proxy 127.0.0.1:8080
}
Caddy takes 80 and 443, fetches and renews the certificate on its own, and forwards to the nginx block you just uncommented. Nothing about Zabbix's UI needs special proxy handling — no WebSocket upgrade, no long-poll timeouts.
The frontend carries session cookies and the full map of your infrastructure, so there is no argument for plain HTTP and none for exposing 8080 directly. If you need to reach it before DNS is ready, use an SSH tunnel:
ssh -L 8080:127.0.0.1:8080 deploy@SERVER_IP
First login and hardening
Load https://zabbix.example.com. On a fresh install you get the frontend
setup wizard: it checks PHP prerequisites, asks for the database connection
(host localhost, database zabbix, user zabbix, and that password again),
then the server name and your timezone. It writes its answers to a config file
in the frontend directory and hands you the login screen.
The default credentials are Admin / zabbix, and they are documented
upstream, which means every scanner on the internet knows them.
- Change the Admin password immediately, before anything else. User settings → Profile → Change password.
- Create a named Super admin account for yourself and use that day to day,
leaving
Adminas a break-glass account with a long password in your vault. - Turn on two-factor auth under Users → Authentication → MFA, or wire the frontend to your identity provider there if you run one.
- Check the guest access setting. Zabbix ships a
guestuser; confirm it is disabled unless you want anonymous read access to your infrastructure map. - Leave 8080 closed at the firewall. The Caddy block is the only door.
Add the hosts you actually want to watch
The install monitors one host: itself, via the local agent. Everything past that is the work Zabbix exists for.
On each machine you want to watch, install the agent from the same repository and point it at the server:
sudo apt install -y zabbix-agent
sudo nano /etc/zabbix/zabbix_agentd.conf
Server=MONITOR_IP
ServerActive=MONITOR_IP
Hostname=web-01
sudo systemctl enable --now zabbix-agent
sudo ufw allow from MONITOR_IP to any port 10050 proto tcp
Two directions, and the distinction matters for firewalls. Passive checks
have the server connecting out to the agent on 10050, so the agent's box
needs the inbound rule (the ufw line above). Active checks have the agent
connecting in to the server on 10051, so the monitoring box needs that rule
instead, ideally restricted to your fleet's source addresses. Active mode
scales better and is friendlier to hosts behind NAT; passive is simpler to
reason about. Most fleets end up with both.
Then, in the frontend: Data collection → Hosts → Create host. Give it the same
Hostname you set in the agent config, put it in a host group, and attach a
template — "Linux by Zabbix agent" is the one for a plain server. Templates
are the whole point: they carry the items, triggers, graphs and discovery
rules, so a host goes from empty to fully instrumented by attaching one.
Finally, wire up alerting under Alerts → Media types. Zabbix ships integrations for email, webhooks and most chat platforms. Configure a media type, attach it to your user, and build an action that fires on trigger severity. Test it deliberately — stop a service and watch the alert arrive on a real device. Until you have seen that, you have a dashboard, not monitoring.
Backups
Everything that matters is in MySQL: hosts, templates, users, history, trends,
and every configuration change you made in the UI. The frontend's generated
config file and /etc/zabbix are worth capturing too, but they are small and
easy to recreate.
sudo mysqldump --single-transaction --routines --triggers \
-uzabbix -p zabbix | gzip > ~/zabbix-$(date +%F).sql.gz
sudo tar czf ~/zabbix-etc-$(date +%F).tar.gz /etc/zabbix
--single-transaction gives a consistent dump without locking the server out,
and --routines --triggers matters here because Zabbix's schema uses both.
Two things people get wrong. History is the bulk of the dump and the least valuable part of it — if the backup is unmanageably large, dump the configuration tables and accept that a restore loses graph history; losing a month of trends is survivable, losing your host and template configuration is not. And copy it off the box: this server exists to tell you when other machines die, so it needs its own recovery story elsewhere. Test a restore once against a throwaway server, because an untested dump is a guess.
Upgrades
Point releases within a line are ordinary apt upgrades:
sudo apt update && sudo apt upgrade
sudo systemctl restart zabbix-server
The server applies any database schema changes itself on the next start, and
writes what it did to /var/log/zabbix/zabbix_server.log. Watch that log
after every upgrade — on a large history table the migration can take a long
time, and the daemon is not serving while it runs.
Moving between major release lines is a bigger operation: install a new
zabbix-release package pointing at the new line, apt update, upgrade the
packages, then let the server migrate the schema on start. Back up first, and
mean it — schema migrations are one-way, and rolling back means restoring the
dump. Read upstream's upgrade notes for the specific jump, and check your
agents are within the supported version window before you move the server.
Troubleshooting
apt install cannot find any zabbix-* packages. The release .deb you
installed points at a repository for a different Ubuntu version — the
${VERSION_ID} problem from earlier. Check what you actually got with
grep -r zabbix /etc/apt/sources.list.d/, remove the release package, and
reinstall it with the sourced-os-release command above.
sudo mysql cannot connect to the local socket. No database server is
installed, or it failed to start. sudo apt install -y mysql-server, then
sudo systemctl status mysql.
The schema import errors with ERROR 1418. Binary logging is refusing the
stored functions. Set log_bin_trust_function_creators on for the import as
shown above, then off again.
zabbix-server starts and immediately stops. Almost always the database.
sudo tail -n 50 /var/log/zabbix/zabbix_server.log names the reason directly —
a wrong DBPassword, a missing database, or a schema that never imported.
The setup wizard says the frontend cannot connect to the database. The
wizard uses its own credentials, entered in the browser, separate from
zabbix_server.conf. Retype the password rather than assuming; a pasted
trailing space is the classic version of this.
Connection refused, or nginx serves a default page. The listen and
server_name lines in /etc/zabbix/nginx.conf are still commented, or a stale
/etc/nginx/sites-enabled/default is claiming the port. sudo nginx -t then
sudo systemctl reload nginx.
The frontend loads but shows a PHP error or a blank page. PHP-FPM did not
restart after the frontend was installed. Run
sudo systemctl restart "php*-fpm" and read /var/log/nginx/error.log.
The dashboard says "Zabbix server is not running" even though it is. The
frontend reaches the server over the network, not the database. Confirm it is
listening (ss -lntp | grep 10051) and that the address and port in the
frontend's config point at it.
A host stays grey / "no data". Check the direction. For passive checks the
server must reach the agent on 10050 — test with
zabbix_get -s AGENT_IP -k agent.ping from the monitoring box. For active
checks the agent must reach the server on 10051, and its Hostname must
exactly match the host name in the frontend. Mismatched hostnames are the most
common cause by a wide margin.
Verification + next steps
You are done when you can: load https://zabbix.example.com over a valid
certificate, log in with a password that is not zabbix, see this server's own
agent reporting green data, add a second host with a template attached and
watch its metrics arrive, then deliberately break something and receive the
alert on a real device.
From there the work is templates and escalation, not installation: group your hosts, lean on the shipped templates before writing your own, set trigger severities that mean something, and build escalation chains so a 3am alert reaches a person rather than an inbox. If you want prettier dashboards on top, Zabbix is a perfectly good data source for Grafana — Deploy Grafana on a VPS covers that side. For the ranked host picks see Best VPS for Monitoring & Uptime, and the monitoring catalogue has the lighter options if this install has convinced you that you wanted one of those.