Deploying Zabbix With Docker-Compose And Portainer

Introduction

Zabbix is an open-source monitoring solution. You can monitor all kinds of things like routers, switches, and computers, just to name a few. Zabbix is a great way to learn about monitoring and it’s free! Docker-Compose is a tool that you can use to create multi-container applications in a streamlined way as opposed to deploying each container using long docker commands. Finally, Portainer is a graphical front-end for docker which makes managing and deploying containers simple.

My goal is to experiment with Zabbix as a solution to monitor my home network. I chose docker and portainer as the platforms to deploy Zabbix because of how quickly I would be able to create the instance and tear it down if I didn’t need it. Also, I am new to docker and portainer so through this experience I hope to learn more about the benefits of containers and docker networking.

Prerequisites

To deploy Zabbix with docker-compose and portainer you need the following:

  • A Linux Server – My Linux server is a virtual machine running Ubuntu 24.04 LTS in my Proxmox virtualization environment
  • Docker – In my test environment, I have Docker installed on top of Ubuntu. You can refer to Docker’s official documentation for how to do this.
  • Portainer – You also need Portainer installed. You can install this by following Portainer’s official documentation.

Deployment Steps

Setting Up The Docker-Compose File

Zabbix has its own official Github with many available docker-compose files that can fulfill just about any deployment requirement. Since I will be using a docker host installed on top of Ubuntu and I want to use PostgreSQL as my database of choice I reviewed that docker compose file.

word image 25508 1

After reviewing the docker-compose file I was impressed with the code. However, the docker-compose file was dependent on a component file which meant I could not cleanly use the code in Portainer. Also, the code is highly modular which is great however it also includes all kinds of services including multiple versions of databases and web servers. Ultimately it would require me to rewrite a lot of the code and take out things I didn’t need and I wanted to start with something a lot simpler.

word image 25508 2

I searched the internet for a docker-compose file and came across this awesome write-up: Simplified Zabbix Install: Step-by-Step with Docker and Portainer. This blog provided a great and simple docker-compose file that I could use to start tinkering with. The blog provided three docker-compose files and I chose the third one, which allows the Zabbix agent access to the host’s file system.

My docker-compose file does a couple of things differently than the one I copied from the previously mentioned blog.

  • I will be using Postgres version 16 instead of the latest version (at the time of this writing is version 17)
  • Instead of letting Docker create a bridge network, I will use the ipvlan network mode so that each container receives a unique static IP on my home network.
  • I mounted an additional volume to the Nginx web server. It is a custom nginx.conf file that changes the default listening port from 8080 to 8089. (If you want to use my docker-compose file just comment this line out. If you use it as is, Portainer will see that volume listed and say that it does not exist)
  • There are additional environmental variables so that I don’t hard code things like IP addresses and the ipvlan parent interface.
services:
  postgresql-server:
    image: postgres:16
    container_name: postgresql-server
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgresql-data:/var/lib/postgresql/data
    networks:
      zabbix_net:
        ipv4_address: ${POSTGRESQL_IP}

  zabbix-server:
    image: zabbix/zabbix-server-pgsql:latest
    container_name: zabbix-server
    restart: unless-stopped
    depends_on:
      - postgresql-server
    environment:
      DB_SERVER_HOST: postgresql-server
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    ports:
      - "10051:10551"
    volumes:
      - zabbix-server-data:/var/lib/zabbix
      - zabbix-snmptraps-data:/var/lib/zabbix/snmptraps
      - zabbix-export-data:/var/lib/zabbix/export
    networks:
      zabbix_net:
        ipv4_address: ${ZABBIX_SERVER_IP}

  zabbix-web-nginx-pgsql:
    image: zabbix/zabbix-web-nginx-pgsql:latest
    container_name: zabbix-web
    restart: unless-stopped
    depends_on:
      - postgresql-server
      - zabbix-server
    environment:
      DB_SERVER_HOST: postgresql-server
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      ZBX_SERVER_HOST: zabbix-server
      PHP_TZ: ${PHP_TZ}
    ports:
      - "${ZABBIX_FRONTEND_PORT}:8080"
    volumes:
      - zabbix-web-data:/usr/share/zabbix
      #The volume below is a custom config file that sets the listening port to 8089
      - /var/lib/docker/volumes/zabbix_zabbix-web-data/_data/nginx.conf:/etc/nginx/http.d/nginx.conf
    networks:
      zabbix_net:
        ipv4_address: ${ZABBIX_WEB_IP}

  zabbix-agent:
    image: zabbix/zabbix-agent:latest
    container_name: zabbix-agent
    restart: unless-stopped
    depends_on:
      - zabbix-server
    environment:
      ZBX_HOSTNAME: "zabbix-server"
      ZBX_SERVER_HOST: zabbix-server
      ZBX_SERVER_PORT: '10051'
      ZBX_SERVER_ACTIVE: zabbix-server
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run
    privileged: true
    networks:
      zabbix_net:
        ipv4_address: ${ZABBIX_AGENT_IP}

volumes:
  postgresql-data:
  zabbix-server-data:
  zabbix-snmptraps-data:
  zabbix-export-data:
  zabbix-web-data:

networks:
  zabbix_net:
    driver: ipvlan
    driver_opts:
      parent: ${IPVLAN_PARENT_INTERFACE}
    ipam:
      config:
        - subnet: ${IPVLAN_SUBNET}
          gateway: ${IPVLAN_GATEWAY}

Here are my environmental variables:

POSTGRES_USER=zabbix
POSTGRES_PASSWORD=<replace this text with a strong password>
POSTGRES_DB=zabbix
POSTGRESQL_IP=10.50.20.33
ZABBIX_SERVER_IP=10.50.20.32
PHP_TZ=America/New_York
ZABBIX_FRONTEND_PORT=8080
ZABBIX_WEB_IP=10.50.20.30
ZABBIX_AGENT_IP=10.50.20.31
IPVLAN_PARENT_INTERFACE=ens18
IPVLAN_SUBNET=10.50.20.0/24
IPVLAN_GATEWAY=10.50.20.1

Deploy Zabbix With Portainer

After you have finished setting up the docker-compose file now you are ready to deploy it. In Portainer, select “Stacks” and then select “Add Stack” (In my screenshot I already have it set up in Zabbix so you can disregard that)

word image 25508 3

Give your stack a name, select “Upload” and then upload your docker-compose file and environmental file. If you haven’t already make sure you save your docker-compose file and give it the name “docker-compose.yaml” and save your environmental variables file as “Zabbix.env”

Alternatively, you can use the Web Editor and copy and paste the code:

word image 25508 4

Select “Deploy the stack” at the bottom of the page and with any luck you should have a working Zabbix instance! You can reach the Zabbix web front end by going to http://<IP address of Zabbix-web>:8080.

The default credentials are:

Username:Admin
Password:zabbix

Changing The Listening Port For The Zabbix Front End

By default, the Zabbix web server listens on port 8080. If this is not a problem for you, this step can be skipped. However, if you already have something on your network listening on port 8080 you can cause a conflict in your network which will make the Zabbix web server not accessible.

To change the default listening port you need to edit the nginx configuration file and change the listening port from 8080 to whatever you would like. Be aware that if you reboot the container, then all changes you make will be lost. To make the changes persistent across reboot, you need to copy the config file from the container onto the host and list it as a mounted volume in the docker-compose file. (If you are following along, it’s the line I suggested to comment out if you wanted to skip this)

The first step to doing this is to console into the Zabbix web server container. Navigate to Containers and select the exec console button under the quick actions column. On the following screen select “Connect.”

word image 25508 5
word image 25508 6

You should see the CLI for the Zabbix web server. To see the contents of the current nginx.conf file, use the following command:

cat /etc/nginx/http.d/nginx.conf

There should be two lines in there that look like this:

server {
     listen 8080;
     listen {::}:8080;

By default, containers don’t have the nano text editor so I chose to use the “sed” command to edit the file directly and change these two lines:

sed -i 's/listen 8080;/listen 8089;/' /etc/nginx/http.d/nginx.conf
sed -i 's/listen \[::\]:8080;/listen [::]:8089;/' /etc/nginx/http.d/nginx.conf

Verify that the changes have been made to the configuration file by using the “cat” command from earlier:

word image 25508 7

Next use these commands to test the configuration file and then reload the web server:

nginx -t
nginx -s reload

If you did not get any errors you should be able to reach our Zabbix front end using the IP address and the port you specified; in my case port 8089.

word image 25508 8

For this final step, we need to make sure the changes we made to the nginx.conf file, persist when the container reboots. Access your docker host via SSH or however, you access your docker host and use these commands:

Sudo -i
docker cp zabbix-web:/etc/nginx/http.d/nginx.conf /var/lib/docker/volumes/zabbix_zabbix-web-data/_data/nginx.conf

If you have been following this guide, what you want to do at this point is uncomment that line from earlier or include the custom config as a mounted volume in the docker-compose file.

  zabbix-web-nginx-pgsql:
    image: zabbix/zabbix-web-nginx-pgsql:latest
    container_name: zabbix-web
    restart: unless-stopped
    depends_on:
      - postgresql-server
      - zabbix-server
    environment:
      DB_SERVER_HOST: postgresql-server
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      ZBX_SERVER_HOST: zabbix-server
      PHP_TZ: ${PHP_TZ}
    ports:
      - "${ZABBIX_FRONTEND_PORT}:8080"
    volumes:
      - zabbix-web-data:/usr/share/zabbix
      #The volume below is a custom config file that sets the listening port to 8089
      - /var/lib/docker/volumes/zabbix_zabbix-web-data/_data/nginx.conf:/etc/nginx/http.d/nginx.conf
    networks:
      zabbix_net:
        ipv4_address: ${ZABBIX_WEB_IP}

The redeploy the stack in Portainer. Your changes should now still be there after redeploying!

Configuring The Zabbix Server

Resetting The Admin Password

If you didn’t change the listening port for the Zabbix server, the first thing you want to do after deploying Zabbix is change the password for the Admin account. To do this select “Users” in the left navigation pane and then select “Users” again in the sub menu. Then select the Admin account:

word image 25508 9

Next, select “Change Password”

word image 25508 10

Finally enter the current password and the new password twice. Then select “Update”

word image 25508 11

Changing The Time Zone

Make sure that the time zone is set correctly depending on your location. To do this select “Administration” in the left navigation pane and then select General>GUI. Finally, change the default time zone to your time zone.

word image 25508 12

Configuring The Zabbix Server Host

By default, the Zabbix server is added to the lists of monitored hosts. For Zabbix to start collecting data on the Zabbix server you need to make one change to the host configuration. Start by navigating to “Data Collection” in the left navigation pane and then select “Hosts.” Then select “Zabbix server” in the list of hosts. (In my screenshot I have already added additional hosts; you can disregard) Take note that by default, the interface for the Zabbix server is set to 127.0.0.1:10050 and will be listed as red in the availability column.

word image 25508 13

In the host configuration screen, we need to add “zabbix-agent” as a DNS name for the interface. Then select “Update.” Docker will automatically resolve that DNS name.

word image 25508 14

It takes 60 seconds for Zabbix to poll the passive check items (more on this later). After 60 seconds you can refresh your browser and confirm that the interface turns green in the availability column.

word image 25508 15

You could also force Zabbix to poll the passive check items. Select “items:”

word image 25508 16

Then select the top checkbox so that all the passive check items get selected:

word image 25508 17

Finally, select “Execute Now” go back to the lists of hosts, and find the Zabbix server host turn green in the availability column (may have to refresh the browser). Be aware that this only works for passive checks.

word image 25508 18

Installing The Zabbix Agent On The Docker Host

The Zabbix agent works in two different ways: active checks and passive checks. Passive checks are when the Zabbix server initiates the connection with a Zabbix agent to collect data. Active checks are when the client machine initiates the connection with a Zabbix server so it can collect its data.

For passive checks to work properly on a docker host, you need to configure the networking in a specific way. By design, Docker does not allow any container-to-host communication. There is a great explanation in this blog for more information:

Using docker macvlan networks

Here is the workaround at a high level for ensuring bi-directional communication between the Zabbix server and the docker

  • Create a dedicated ipvlan interface on the host
  • Assign it an IP address on my home network
  • Configure a routing policy so that all traffic coming from the host’s new IP address will use the new ipvlan interface.
  • Create a startup script that creates the ipvlan interface and routing policy on boot-up

Creating A New Virtual ipvlan Interface

To create a new ipvlan interface SSH into the docker host and use the following command. In the below command <ens18> is the parent interface and <ens18-zabbix> is the name of the interface:

sudo ip link add link ens18 name ens18-zabbix type ipvlan mode l2

Use the following command to give the new interface an IP address:

sudo ip addr add 10.50.20.9/24 dev ens18-zabbix

Finally make sure the interface is up:

sudo ip link set ens18-zabbix up

At this point, you should be able to ping the new IP address. Try to ping it from somewhere on your network.

Creating A Routing Policy

Because I have two interfaces on the host that are on the same subnet, this will create conflicts in the main routing table. This is why we need to create a custom routing table that bypasses the main routing table for destinations in the 10.50.20.0/24 network.

First I need to tell my docker host that all traffic originating from 10.50.20.9 will use a new routing table (table 1o0). I use a /32 subnet mask to tell the routing table that this only applies to this IP address:

sudo ip rule add from 10.50.20.9/32 table 100

Next, I need to add a route to the new custom route table 100. This route will send packets destined for the 10.50.20.0/24 network out to ens18-zabbix

sudo ip route add 10.50.20.0/24 dev ens18-zabbix table 100

Creating A Startup Script

To create a startup script I created a file called setup-ipvlan.service at this file path:

sudo nano /etc/systemd/system/setup-ipvlan.service  

Copy the code into the file and then save it:

[Unit]
Description=Set up ipvlan interface ens18-zabbix and routing rules
After=network.target
  
[Service]
Type=oneshot
ExecStart=/sbin/ip link add link ens18 name ens18-zabbix type ipvlan mode l2
ExecStartPost=/sbin/ip addr add 10.50.20.9/24 dev ens18-zabbix
ExecStartPost=/sbin/ip link set ens18-zabbix up
ExecStartPost=/sbin/ip rule add from 10.50.20.9/32 table 100
ExecStartPost=/sbin/ip route add 10.50.20.0/24 dev ens18-zabbix table 100
RemainAfterExit=true
  
[Install]
WantedBy=multi-user.target

Reloaded system to recognize the new service:

 sudo systemctl daemon-reload

Enable the service:

sudo systemctl enable setup-ipvlan.service

Start the service immediately:

sudo systemctl start setup-ipvlan.service

Check if the service is working:

sudo systemctl status setup-ipvlan.service

Reboot the docker host and check if the changes persist:

ip link show ens18-zabbix
ip rule show
ip route show table 100

Install And Configure The Zabbix Agent

To install the Zabbix agent on the docker host we can do it on the command line. The code below downloads the Zabbix agent package from the repository installs it, and then enables it.

sudo -s
wget https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest+ubuntu24.04_all.deb
dpkg -i zabbix-release_latest+ubuntu24.04_all.deb
apt update
apt install zabbix-agent

To configure the Zabbix agent you need to use a text editor to configure the Zabbix_agentd.conf file. Use this command:

nano /etc/zabbix/zabbix_agentd.conf

In the configuration file you must set three values:

Server=
Hostname=
ServerActive=

The value for “Server” should be the IP address of the Zabbix server. Setting this value will allow passive checks to work:

word image 25508 19

Scroll down and set the same value for “ServerActive” for active checks.

You also need to set the value for Hostname:

word image 25508 20

After you have finished configuring the agent save the file and then reload the agent:

systemctl restart zabbix-agent
systemctl status zabbix-agent

Configuring The Docker Host In Zabbix

To configure the docker host in Zabbix, select Data collection>Hosts, then select “Create host” in the top right. Below is the configuration of the docker host:

word image 25508 21

The hostname is case-sensitive. The hostname must match exactly with what is specified in the Zabbix agent configuration. For passive checks select the Linux by Zabbix Agent template. A monitored host must have a Host group set – for this host, it is linked to the Linux servers host group. For the interface, I specified the docker hosts ipvlan interface – 10.50.20.9. Selecting add will finish the host setup.

Troubleshooting And Lessons Learned

  • Docker Networking – When I was trying to identify the problem with why my docker containers could not reach the host, I used a few different network modes and learned about what each network mode does and what their limitations are.
  • Linux Server Administration – When I was trying to figure out how to create a new virtual interface that would allow my containers to talk to my docker host, I learned that netplan is what the Ubuntu server uses for configuring network interfaces and it did not recognize the ipvlan interface I created. I needed to create a startup script to get past this limitation.

Conclusion

In this guide, I walked through deploying a Zabbix server instance in which the Zabbix server has privileged access to the docker host so that I can monitor it. I learned a lot about Linux, docker networking, and basic Zabbix setup and configuration.

Scroll to Top