Run Zabbix 7.4 on Docker Compose with HTTPS (No Reverse Proxy, DNS-01 Certbot, Bind Mounts Only)

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

  1. Create the working directory layout (bind mounts)
sudo mkdir -p /opt/zabbix-docker/{ssl,data/mariadb,data/zabbix-export,data/zabbix-snmptraps}
sudo chown -R $USER:$USER /opt/zabbix-docker
cd /opt/zabbix-docker

  1. Install Certbot + Cloudflare DNS plugin
sudo apt update
sudo apt install -y certbot python3-certbot-dns-cloudflare

  1. 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.ini

Example cloudflare.ini:

dns_cloudflare_api_token = CLOUDFLARE_API_TOKEN_WITH_DNS_EDIT

  1. 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.com

If your TXT record propagation is slow, add:

--dns-cloudflare-propagation-seconds 60

  1. 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.pem

Quick sanity check:

ls -l /opt/zabbix-docker/ssl

You should see ssl.crt, ssl.key, dhparam.pem.


  1. 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

  1. 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
    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.


  1. Start the stack (HTTPS from the first boot)
cd /opt/zabbix-docker
docker compose up -d
docker compose ps

Now open:

  • https://zabbix-docker.maksonlee.com/

  1. 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.sh

Script:

#!/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 certificates

For your output, the cert name is zabbix-docker.maksonlee.com, so the renewal file is:

sudo vi /etc/letsencrypt/renewal/zabbix-docker.maksonlee.com.conf

Under [renewalparams], add:

renew_hook = /usr/local/bin/zabbix-docker-cert-deploy.sh

This 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-run

Then 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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top