This post shows a clean way to run Zabbix Server + Zabbix Web (NGINX) + MariaDB using Docker Compose, and expose HTTPS directly from the zabbix-web container (no host reverse proxy). It also uses DNS-01 (Cloudflare example) so you don’t need to open or free up any HTTP challenge port, and it uses host bind mounts only (no named/anonymous Docker volumes).
What you’ll build
- Zabbix 7.4 stack on Docker Compose
- HTTPS served by the NGINX inside
zabbix-web - Let’s Encrypt certificates issued via Certbot DNS-01
- Ports published: 443 (HTTPS web UI) and 10051 (Zabbix Server for agents/proxies).
- All persistent data stored on host paths (bind mounts)
- Automated renew + NGINX reload inside the container (no recreate)
Lab assumptions
- Hostname:
zabbix-docker.maksonlee.com - OS: Ubuntu 24.04
- Docker Engine + Docker Compose plugin installed
- Port reachable from your browser:
- 443/tcp
- Create the working directory layout (bind mounts)
sudo mkdir -p /opt/zabbix-docker/{ssl,saml-certs,data/mariadb,data/zabbix-export,data/zabbix-snmptraps}
sudo chown -R $USER:$USER /opt/zabbix-docker
cd /opt/zabbix-docker- Install Certbot + Cloudflare DNS plugin
sudo apt update
sudo apt install -y certbot python3-certbot-dns-cloudflare- Create the Cloudflare credentials file
sudo mkdir -p /home/administrator/.secrets/certbot
sudo vi /home/administrator/.secrets/certbot/cloudflare.ini
sudo chmod 600 /home/administrator/.secrets/certbot/cloudflare.iniExample cloudflare.ini:
dns_cloudflare_api_token = CLOUDFLARE_API_TOKEN_WITH_DNS_EDIT- Issue the certificate (DNS-01)
Request the cert before starting Zabbix so the web container can boot with HTTPS enabled immediately.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /home/administrator/.secrets/certbot/cloudflare.ini \
-d zabbix-docker.maksonlee.comIf your TXT record propagation is slow, add:
--dns-cloudflare-propagation-seconds 60- Prepare the certificate files for
zabbix-web(expected filenames)
The Zabbix web NGINX image enables SSL when these files exist:
/etc/ssl/nginx/ssl.crt/etc/ssl/nginx/ssl.key/etc/ssl/nginx/dhparam.pem
Copy your issued certs into /opt/zabbix-docker/ssl:
cd /opt/zabbix-docker
sudo cp -f /etc/letsencrypt/live/zabbix-docker.maksonlee.com/fullchain.pem ./ssl/ssl.crt
sudo cp -f /etc/letsencrypt/live/zabbix-docker.maksonlee.com/privkey.pem ./ssl/ssl.key
# generate once (2048 is a practical baseline)
sudo openssl dhparam -out ./ssl/dhparam.pem 2048
sudo chmod 600 ./ssl/ssl.key
sudo chmod 644 ./ssl/ssl.crt ./ssl/dhparam.pemQuick sanity check:
ls -l /opt/zabbix-docker/sslYou should see ssl.crt, ssl.key, dhparam.pem.
- Create
.env
Create /opt/zabbix-docker/.env:
DB_NAME=zabbix
DB_USER=zabbix
DB_PASSWORD=CHANGE_ME_STRONG
DB_ROOT_PASSWORD=CHANGE_ME_STRONG_TOO
PHP_TZ=Asia/Taipei- Use this Docker Compose (HTTPS only, bind mounts only)
Create /opt/zabbix-docker/docker-compose.yml:
services:
mariadb:
image: mariadb:11.4
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_bin
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- /opt/zabbix-docker/data/mariadb:/var/lib/mysql
restart: unless-stopped
zabbix-server:
image: zabbix/zabbix-server-mysql:7.4-ubuntu-latest
environment:
DB_SERVER_HOST: mariadb
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
depends_on:
- mariadb
ports:
- "10051:10051"
volumes:
# These two paths are declared as VOLUME in the image; bind-mount them
# to prevent Docker from creating anonymous volumes.
- /opt/zabbix-docker/data/zabbix-export:/var/lib/zabbix/export
- /opt/zabbix-docker/data/zabbix-snmptraps:/var/lib/zabbix/snmptraps
restart: unless-stopped
zabbix-web:
# simplest way to avoid "Permission denied" reading ssl.key
user: "0:0"
image: zabbix/zabbix-web-nginx-mysql:7.4-ubuntu-latest
environment:
ZBX_SERVER_HOST: zabbix-server
DB_SERVER_HOST: mariadb
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
PHP_TZ: ${PHP_TZ}
depends_on:
- mariadb
- zabbix-server
ports:
# publish HTTPS only
- "443:8443"
volumes:
# Zabbix web image expects these exact filenames in this exact directory
- /opt/zabbix-docker/ssl:/etc/ssl/nginx:ro
- /opt/zabbix-docker/saml-certs:/usr/share/zabbix/ui/conf/certs:ro
restart: unless-stopped
Why you were still seeing Docker volumes
Some images declare VOLUME in their Dockerfile (for example, Zabbix server uses volumes like /var/lib/zabbix/export and /var/lib/zabbix/snmptraps). If you don’t override them, Docker will create anonymous volumes automatically even if your compose file doesn’t define any named volumes.
This compose file prevents that by bind-mounting those exact paths to host directories.
- Start the stack (HTTPS from the first boot)
cd /opt/zabbix-docker
docker compose up -d
docker compose psNow open:
https://zabbix-docker.maksonlee.com/
- Renewals with zero downtime (copy files + NGINX reload)
You do not need to recreate the container. After renewal, you just:
- copy updated cert/key into
/opt/zabbix-docker/ssl - run an NGINX reload inside
zabbix-web
Deploy script
Create /usr/local/bin/zabbix-docker-cert-deploy.sh:
sudo vi /usr/local/bin/zabbix-docker-cert-deploy.sh
sudo chmod +x /usr/local/bin/zabbix-docker-cert-deploy.shScript:
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="zabbix-docker.maksonlee.com"
DST="/opt/zabbix-docker/ssl"
LINEAGE="/etc/letsencrypt/live/${DOMAIN}"
# Safety guard: if Certbot provides RENEWED_LINEAGE and it's not our cert, do nothing.
if [[ -n "${RENEWED_LINEAGE:-}" && "${RENEWED_LINEAGE}" != "${LINEAGE}" ]]; then
exit 0
fi
cp -f "${LINEAGE}/fullchain.pem" "${DST}/ssl.crt"
cp -f "${LINEAGE}/privkey.pem" "${DST}/ssl.key"
chmod 600 "${DST}/ssl.key"
chmod 644 "${DST}/ssl.crt" "${DST}/dhparam.pem"
cd /opt/zabbix-docker
# Reload nginx inside the running container (no recreate).
docker compose exec -T zabbix-web nginx -s reload
Make sure the hook runs only for this certificate
Because you may have multiple certs on the same machine later, do per-certificate hook configuration.
Your cert name is:
sudo certbot certificatesFor your output, the cert name is zabbix-docker.maksonlee.com, so the renewal file is:
sudo vi /etc/letsencrypt/renewal/zabbix-docker.maksonlee.com.confUnder [renewalparams], add:
renew_hook = /usr/local/bin/zabbix-docker-cert-deploy.shThis ensures only that certificate’s renewal triggers the deploy script.
Test the renewal flow
A dry-run verifies the ACME flow. Note that hooks may be skipped during dry-run depending on options; check the logs if needed.
sudo certbot renew --dry-runThen verify Zabbix is still up and serving HTTPS:
docker compose ps
curl -kI https://zabbix-docker.maksonlee.com/Did this guide save you time?
Support this site