> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Install Zabbix 7.0 LTS in Docker Alongside Grafana
- URL: https://www.pinglabz.com/install-zabbix-7-0-lts-in-docker-alongside-grafana/
- Published: 2026-08-19T20:56:55.000Z
- Updated: 2026-08-19T21:28:29.000Z
- Description: Two monitoring platforms, one Ubuntu box, zero port collisions. Plus the built-in host that cannot reach its own agent, and why apt was not an option.
- Author: Jaime
- Tags: Linux, Monitoring, Tools

Grafana draws pictures. It does not go out and poll anything. If you want a system that actually walks SNMP tables, runs agent checks, evaluates triggers and sends you an alert at two in the morning, you want Zabbix underneath it. This guide puts Zabbix 7.0 LTS on an Ubuntu Server box that is already running Grafana, using the official Docker images, with both platforms coexisting on the same host and neither one stepping on the other.

Everything here came off the same lab machine used in [the Grafana Enterprise install guide](https://www.pinglabz.com/how-to-install-grafana-enterprise-on-ubuntu-server/): an Ubuntu 26.04 LTS VM at 192.168.88.157 with 15 GB RAM and 6 vCPU. Grafana 13.2.0 was already live on port 3000 before any of this started, and it stayed live throughout.

## Why Docker and not apt

This deserves an honest answer rather than a hand-wave, because the obvious move is to add Zabbix's APT repository the same way you added Grafana's.

I tried that first. On Ubuntu 26.04, whose codename is `resolute`, the official Zabbix release repository contains no compiled binaries at all:

```
j@ubnt:~$ curl -s https://repo.zabbix.com/zabbix/8.0/release/ubuntu/dists/resolute/Release | grep -E '^(Architectures|Components)'
Architectures: all
Components: main
```

`Architectures: all` with no `amd64` means architecture-independent metadata only. The `zabbix-release` package installs three sources, and the only one carrying real server packages is the unstable channel:

```
j@ubnt:~$ apt-cache policy zabbix-server-mysql
zabbix-server-mysql:
  Candidate: 2:8.0.0~beta2-1+ubuntu26.04
        500 https://repo.zabbix.com/zabbix/8.0/unstable/ubuntu resolute/main amd64 Packages
```

Beta software is not what you want under a monitoring platform. Borrowing the Ubuntu 24.04 packages does not rescue it either, because 26.04 ships PHP 8.5 while the 24.04 Zabbix frontend targets PHP 8.3:

```
j@ubnt:~$ apt-cache policy php-fpm | head -3
php-fpm:
  Installed: (none)
  Candidate: 2:8.5+99ubuntu1
```

The containers sidestep all of it. The official images bundle their own PHP and their own dependency tree, so the host OS version stops mattering. If your host is Ubuntu 22.04 or 24.04 and you would rather use apt, Zabbix's own documentation covers that path and it works fine. On a release this new, containers are the pragmatic answer.

### Pick 7.0, not 7.4 or 8.0

Zabbix runs two release tracks and the numbering does not make it obvious which is which:

7.0 LTS

**Type:** Long Term Support

**Released:** June 2024

**Full support:** to June 2027

**Limited support:** to June 2029

Use this for a platform you build on

7.4

**Type:** Standard release

**Released:** July 2025

**Limited support ends:** Q4 2026

Newer features, short runway

8.0 LTS

**Type:** next Long Term Support

**Status:** beta at time of writing

**Planned:** Q3 2026

Wait for GA

Standard releases arrive every six months and are supported for twelve. LTS releases arrive every eighteen months and are supported for five years. A 7.x number tells you nothing about which track you are on, so check the lifecycle page rather than assuming the highest number is the safest.

## Planning the ports so Grafana keeps working

Two monitoring platforms on one host is fine, but only if you decide who owns which port before you start. Here is the allocation used:

**Grafana**3000, on the host under systemd 

**Zabbix frontend**8080, deliberately not 80 

**Zabbix server**10051 

**Zabbix agent 2**10050 

**MySQL**3306, internal to the stack, not published 

Port 80 is left empty on purpose. The moment you want a real hostname and a certificate, you put one reverse proxy on 80 and 443 and have it front both 3000 and 8080\. If you had let the Zabbix frontend grab port 80 you would be unpicking that later.

MySQL is not published to the host at all. Nothing outside the compose network needs to reach it, so it only listens on the stack's internal bridge.

## Step 0: Give yourself disk before the database arrives

Check this before anything else. The Ubuntu Server installer, left on defaults, allocates only about half your volume group to the root logical volume:

```
j@ubnt:~$ sudo vgs
  VG        #PV #LV #SN Attr   VSize   VFree
  ubuntu-vg   1   1   0 wz--n- <48.00g 24.00g
```

24 GB sitting unallocated. A Zabbix database grows with hosts multiplied by items multiplied by how long you keep history, and it is the least predictable thing on the box. Claim the space now, while the filesystem is empty and the operation is trivial:

```
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
```

```
  Size of logical volume ubuntu-vg/ubuntu-lv changed from <24.00 GiB (6143 extents) to <48.00 GiB (12287 extents).
  Logical volume ubuntu-vg/ubuntu-lv successfully resized.

j@ubnt:~$ df -h /
Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv   48G  9.9G   36G  22% /
```

Both commands are online operations. Nothing unmounts, nothing reboots.

## Step 1: Install Docker

Use Docker's own repository rather than Ubuntu's `docker.io` package, because you want the compose plugin and a current engine. Same keyring pattern as the Grafana install:

```
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

. /etc/os-release
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

Sourcing `/etc/os-release` saves you hardcoding the codename. Unlike Zabbix, Docker does publish native packages for 26.04:

```
j@ubnt:~$ apt-cache policy docker-ce | head -3
docker-ce:
  Installed: (none)
  Candidate: 5:29.7.2-1~ubuntu.26.04~resolute

j@ubnt:~$ docker --version
Docker version 29.7.2, build a7dcaa6
j@ubnt:~$ docker compose version
Docker Compose version v5.5.0
```

Add yourself to the `docker` group so you are not typing sudo all day. It takes effect on your next login:

```
sudo usermod -aG docker $USER
```

## Step 2: Write the compose file

Four services: the database, the Zabbix server, the web frontend, and an agent so the box monitors itself. Create `/opt/zabbix/docker-compose.yml`:

```
name: zabbix

services:
  mysql:
    image: mysql:8.4
    container_name: zabbix-mysql
    restart: unless-stopped
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_bin
      - --log_bin_trust_function_creators=1
    environment:
      MYSQL_DATABASE: zabbix
      MYSQL_USER: zabbix
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql

  zabbix-server:
    image: zabbix/zabbix-server-mysql:7.0-ubuntu-latest
    container_name: zabbix-server
    restart: unless-stopped
    depends_on:
      - mysql
    environment:
      DB_SERVER_HOST: mysql
      MYSQL_DATABASE: zabbix
      MYSQL_USER: zabbix
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    ports:
      - '10051:10051'

  zabbix-web:
    image: zabbix/zabbix-web-nginx-mysql:7.0-ubuntu-latest
    container_name: zabbix-web
    restart: unless-stopped
    depends_on:
      - mysql
      - zabbix-server
    environment:
      ZBX_SERVER_HOST: zabbix-server
      DB_SERVER_HOST: mysql
      MYSQL_DATABASE: zabbix
      MYSQL_USER: zabbix
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      PHP_TZ: America/Los_Angeles
    ports:
      - '8080:8080'

  zabbix-agent:
    image: zabbix/zabbix-agent2:7.0-ubuntu-latest
    container_name: zabbix-agent
    restart: unless-stopped
    environment:
      ZBX_HOSTNAME: Zabbix server
      ZBX_SERVER_HOST: zabbix-server
    ports:
      - '10050:10050'

volumes:
  mysql-data:
```

Several choices in there are worth explaining rather than copying blindly.

**The three MySQL flags are not optional.** Zabbix requires `utf8mb4` with the `utf8mb4_bin` collation, and the schema import creates stored functions, which MySQL refuses under binary logging unless `log_bin_trust_function_creators` is on. Miss any of them and the schema import fails in a way that is tedious to diagnose.

**Services reach each other by service name.** `DB_SERVER_HOST: mysql` and `ZBX_SERVER_HOST: zabbix-server` work because compose puts everything on one network with DNS. This matters again in a moment, in the gotcha section.

**`restart: unless-stopped` on all four** is what makes the stack come back after a host reboot. It is the container equivalent of `systemctl enable`.

**The named `mysql-data` volume is your database.** Everything else in this stack is disposable. That volume is not.

Passwords go in `/opt/zabbix/.env` beside the compose file, so they are not sitting in a file you might paste into a ticket:

```
MYSQL_PASSWORD=YourZabbixDbPassword
MYSQL_ROOT_PASSWORD=YourRootPassword
```

```
sudo chmod 600 /opt/zabbix/.env
```

Validate before you launch. `config --quiet` prints nothing and exits zero when the file is sound:

```
cd /opt/zabbix
docker compose config --quiet && echo 'COMPOSE VALID'
```

## Step 3: Bring it up

```
cd /opt/zabbix
docker compose up -d
```

First run pulls roughly a gigabyte of images. When it settles:

```
j@ubnt:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES           STATUS                    PORTS
zabbix-web      Up 33 seconds (healthy)   0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp, 8443/tcp
zabbix-server   Up 33 seconds             0.0.0.0:10051->10051/tcp, [::]:10051->10051/tcp
zabbix-agent    Up 34 seconds             0.0.0.0:10050->10050/tcp, [::]:10050->10050/tcp, 31999/tcp
zabbix-mysql    Up 34 seconds             3306/tcp, 33060/tcp
```

Note `zabbix-mysql` shows `3306/tcp` with no `0.0.0.0->` prefix. The port is exposed inside the stack but not published to the host, which is exactly what you want.

The schema import runs by itself on first start. Watch it:

```
j@ubnt:~$ docker logs zabbix-server
2026-08-19T20:49:13Z [info]: * DB_SERVER_HOST: mysql
2026-08-19T20:49:13Z [info]: * DB_SERVER_PORT: 3306
2026-08-19T20:49:13Z [info]: * DB_SERVER_DBNAME: zabbix
2026-08-19T20:49:13Z [info]: **** MySQL server is not available. Waiting 5 seconds...
2026-08-19T20:49:18Z [info]: **** MySQL server is not available. Waiting 5 seconds...
2026-08-19T20:49:39Z [info]: ** Creating 'zabbix' user in MySQL database
2026-08-19T20:49:39Z [info]: ** Creating 'zabbix' schema in MySQL
```

Those "MySQL server is not available" lines are normal and self-resolving. `depends_on` waits for the container to start, not for mysqld to finish initializing and accept connections, so the Zabbix entrypoint polls until it can connect. It retried for about 26 seconds here. Only worry if it is still looping after a couple of minutes.

Confirm the schema landed. Zabbix 7.0 creates a little over 200 tables:

```
j@ubnt:~$ docker exec zabbix-mysql mysql -uzabbix -p'YourZabbixDbPassword' \
    -e 'select count(*) as tables_created from information_schema.tables where table_schema="zabbix";'
tables_created
203
```

## Step 4: Verify, including that Grafana is untouched

Both web interfaces should answer:

```
j@ubnt:~$ curl -s -o /dev/null -w 'zabbix web  HTTP %{http_code}\n' http://192.168.88.157:8080/
zabbix web  HTTP 200
j@ubnt:~$ curl -s -o /dev/null -w 'grafana     HTTP %{http_code}\n' http://192.168.88.157:3000/login
grafana     HTTP 200
```

The Zabbix API will tell you the exact version without opening a browser:

```
j@ubnt:~$ curl -s -X POST -H 'Content-Type: application/json-rpc' \
    -d '{"jsonrpc":"2.0","method":"apiinfo.version","params":{},"id":1}' \
    http://192.168.88.157:8080/api_jsonrpc.php
{"jsonrpc":"2.0","result":"7.0.29","id":1}
```

And the listener table shows the two platforms side by side with no overlap:

```
j@ubnt:~$ sudo ss -ltnp | grep -E '3000|8080|10050|10051'
LISTEN 0 4096   0.0.0.0:8080    0.0.0.0:*  users:(("docker-proxy",pid=7782,fd=8))
LISTEN 0 4096   0.0.0.0:10051   0.0.0.0:*  users:(("docker-proxy",pid=7622,fd=8))
LISTEN 0 4096   0.0.0.0:10050   0.0.0.0:*  users:(("docker-proxy",pid=7489,fd=8))
LISTEN 0 4096         *:3000          *:*  users:(("grafana",pid=3365,fd=86))
```

Grafana holds 3000 as a native process, Docker holds the rest through `docker-proxy`. Nothing had to move.

![Zabbix Global view dashboard reporting the server running, version 7.0.29 and one available host](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/08/zabbix-global-view.jpg)

The stock Global view dashboard once the stack settles. **Zabbix server is running: Yes** against `zabbix-server:10051`, version 7.0.29 on both server and frontend, and one host Available.

## Logging in to Zabbix

Browse to `http://<your-server-ip>:8080`, which was `http://192.168.88.157:8080` here.

**URL**http://<server-ip>:8080 

**Username**Admin 

**Password**zabbix 

**Password change**NOT forced. Do it yourself 

Two things catch people. The username is `Admin` with a **capital A** and it is case sensitive, so `admin` fails. And unlike Grafana, Zabbix does **not** force a password change at first login. It will happily leave `Admin` / `zabbix` in place forever. Change it immediately under User settings, then Profile, then Change password.

You can prove the credentials work from the command line before you ever open a browser. A successful `user.login` returns a session token:

```
j@ubnt:~$ curl -s -X POST -H 'Content-Type: application/json-rpc' \
    -d '{"jsonrpc":"2.0","method":"user.login","params":{"username":"Admin","password":"zabbix"},"id":1}' \
    http://192.168.88.157:8080/api_jsonrpc.php
{"jsonrpc":"2.0","result":"ae84e83c11d4fe2400164547852f5327","id":1}
```

![Zabbix sign in page with username and password fields](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/08/zabbix-login-page.jpg)

The Zabbix sign-in page. Username `Admin` with a capital A, password `zabbix`, and no prompt to change it.

## The gotcha: the built-in host cannot reach its agent

This one will bite you within minutes of logging in, and the error message does not point at the cause.

Zabbix ships with a host called "Zabbix server" that monitors the Zabbix installation itself, and its agent interface is preconfigured as `127.0.0.1`. That is correct for a traditional install where server and agent are on the same machine. In a compose stack the agent is a **separate container**, so `127.0.0.1` is the server container's own loopback, where nothing is listening. The log fills up:

```
429:20260819:205049.443 Zabbix agent item "system.swap.size[,free]" on host "Zabbix server" failed: first network error, wait for 15 seconds
429:20260819:205104.444 Zabbix agent item "system.users.num" on host "Zabbix server" failed: another network error, wait for 15 seconds
429:20260819:205134.448 temporarily disabling Zabbix agent checks on host "Zabbix server": interface unavailable
```

The fix is to point that interface at the agent's service name and switch it from IP to DNS. In the UI: **Data collection**, then **Hosts**, then **Zabbix server**, then the **Interfaces** section. Set "Connect to" to **DNS** and the DNS name to `zabbix-agent`, matching the service name in your compose file.

Before and after, via the API:

```
BEFORE: [{"interfaceid":"1","ip":"127.0.0.1","dns":"","useip":"1","port":"10050"}]
AFTER:  [{"interfaceid":"1","ip":"","dns":"zabbix-agent","useip":"0","port":"10050"}]
```

Zabbix re-tests a failed interface on a timer, so give it up to a minute. You are looking for this line:

```
429:20260819:205234.448 enabling Zabbix agent checks on host "Zabbix server": interface became available
```

After which real data starts landing:

```
system.cpu.load[all,avg1]          state=0 last=0.434082
system.cpu.load[all,avg5]          state=0 last=0.773926
system.cpu.load[all,avg15]         state=0 last=0.474609
system.cpu.num                     state=0 last=6
```

`state=0` means supported and collecting. Six CPUs, which matches the host, so the agent really is reporting the machine and not something imaginary.

### The other log noise, which is not a problem

You will also see a run of these and they are harmless:

```
item "Zabbix server:zabbix[process,vmware collector,avg,busy]" became not supported: No "vmware collector" processes started.
item "Zabbix server:zabbix[process,ipmi manager,avg,busy]" became not supported: No "ipmi manager" processes started.
item "Zabbix server:zabbix[process,report writer,avg,busy]" became not supported: No "report writer" processes started.
```

The bundled template monitors internal Zabbix processes, including ones that only spawn when you enable VMware collection, IPMI polling or scheduled reports. You have not enabled those, so the processes do not exist, so the items report unsupported. Ignore them, or disable those items on the template if the noise bothers you.

![Zabbix Hosts page showing the Zabbix server host with interface zabbix-agent port 10050 and a green ZBX availability tag](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/08/zabbix-hosts-agent-interface.jpg)

What it looks like once fixed. The interface column reads `zabbix-agent:10050` rather than `127.0.0.1`, and the **ZBX** availability tag is green.

![Zabbix Latest data page listing CPU system time, boot time, uptime and other collected values](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/08/zabbix-latest-data.jpg)

Latest data for the built-in host, filtered to the system items. Real values with recent check times means the agent is genuinely reachable.

## What it actually costs to run both

The interesting question when you stack two monitoring platforms on one box. With Grafana and the full Zabbix stack running:

```
j@ubnt:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi       1.9Gi       7.4Gi        19Mi       6.2Gi        13Gi

j@ubnt:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}'
NAME            MEM USAGE / LIMIT     CPU %
zabbix-web      49.46MiB / 15.11GiB   0.01%
zabbix-server   112MiB / 15.11GiB     1.02%
zabbix-agent    11.11MiB / 15.11GiB   0.51%
zabbix-mysql    733.2MiB / 15.11GiB   3.65%
```

The whole Zabbix stack is about 900 MB, and MySQL is 80 percent of that. Grafana adds roughly 100 MB. Total footprint under 1.1 GB, leaving 13 GB available on a 15 GB box. Two monitoring platforms is not the resource problem people expect it to be, at least until you are polling thousands of items.

```
j@ubnt:~$ df -h /
Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv   48G   14G   32G  30% /
```

## Day-two commands

All of these run from `/opt/zabbix`, because compose looks for the file in your working directory:

**docker compose ps**What is running and whether it is healthy 

**docker compose logs -f zabbix-server**Follow the server log, your first stop for anything odd 

**docker compose pull && docker compose up -d**Patch within 7.0\. The tag keeps you on the LTS line 

**docker compose down**Stop everything. The database volume survives 

**docker compose down -v**Also deletes the volume. This destroys your database 

The distinction between the last two is worth internalizing before you are tired and troubleshooting at midnight. `-v` is not a verbose flag here.

Because the images are pinned to `7.0-ubuntu-latest` rather than `latest`, a pull gets you patch releases within the 7.0 LTS line and will never silently jump you to 7.4 or 8.0\. Major upgrades stay a deliberate act, which is the whole point of running LTS.

## What to back up

Two things, and only two things:

- **The `zabbix_mysql-data` volume.** Every host, template, trigger and item of history you have. Compose prefixes volume names with the project name, so `mysql-data` becomes `zabbix_mysql-data` on disk.
- **The `/opt/zabbix/` directory.** Your compose file and your `.env`. Small, and it is the recipe for rebuilding the rest.

A logical dump is easier to restore across versions than copying the volume:

```
docker exec zabbix-mysql mysqldump -uroot -p'YourRootPassword' \
  --single-transaction zabbix | gzip > zabbix-$(date +%F).sql.gz
```

## What to do next

You now have polling and dashboards on the same host, which is the point of doing it this way:

- **Wire Zabbix into Grafana.** The `alexanderzobnin-zabbix-app` plugin lets Grafana query Zabbix directly, so Zabbix does collection, triggers and alerting while Grafana does the visualization. That is the payoff for running both.
- **Put a reverse proxy on 80 and 443** fronting 3000 and 8080, which is why you kept port 80 free.
- **Add real devices.** SNMP against switches and routers is what Zabbix is best at, and the bundled network device templates cover most vendors out of the box.
- **Change the Admin password** if you skipped past it earlier. Zabbix will not remind you.

## Key takeaways

- Check whether your distro actually has GA Zabbix packages before committing to apt. On Ubuntu 26.04 the release repo is `Architectures: all` and the only binaries are beta.
- Zabbix 7.0 is LTS and supported to 2029\. 7.4 is a Standard release with a twelve month life. The bigger number is not the safer one.
- Decide port ownership before you install anything. Grafana on 3000, Zabbix frontend on 8080, port 80 kept free for a future reverse proxy.
- Extend the root logical volume first. The Ubuntu installer leaves about half your volume group unallocated, and a monitoring database is the wrong thing to run out of disk on.
- MySQL needs `utf8mb4`, `utf8mb4_bin` and `log_bin_trust_function_creators=1` or the schema import fails.
- Log in as `Admin` with a capital A, password `zabbix`, and change it yourself because Zabbix will not make you.
- Repoint the built-in "Zabbix server" host's agent interface from `127.0.0.1` to the DNS name `zabbix-agent`, or every agent check on it fails.
- Both platforms together use about 1.1 GB of RAM. Running them on one host is entirely reasonable.