最近在社区里总能看到一类问题把我逗乐了:一边是新手问“zabbix server必须装到麒麟系统服务器版本吗”,一边是踩坑老手在问“docker安装mysql失败怎么办”“docker网络不通怎么排查”。说实话,用Docker搭Zabbix这件事,难点从来不在那几个docker run命令,而在于把数据库、Server、Web前端、Agent这几号角色放在同一个网络里,还要让它们安心把数据写进你指定的数据卷。这篇内容就是基于我自己的实际部署经历,从镜像选型、compose编排、高频故障排查,到上线后的告警日常,一次性讲清楚。适合正在选型的运维同学,也适合那些已经装完但每天被“问题已解决”“告警怎么手动消除”折磨的一线值班人员。
1. 为什么把Zabbix装进容器:先聊动机再谈方案
1.1 从“必须装到麒麟系统吗”这个热搜说起
我先把结论扔在前面:不需要。Zabbix官方镜像跑在哪个Linux发行版的宿主机上,跟镜像内部是什么系统没有直接关系。你宿主机用麒麟、用CentOS、用Ubuntu、用Debian都行,前提就两条:内核版本能跑当代Docker,端口不被占用。为什么网上那么多人纠结系统问题?因为传统二进制安装的Zabbix,本质上是一整套本地依赖环境:PHP版本、Apache/Nginx、数据库客户端、字体库、时区配置全部要跟发行版自带的包管理配合。CentOS 7默认的PHP 5.4带不动新版Zabbix前端,Ubuntu 22.04的PHP 8.1又时不时冒出graph字体缺失的问题。这些坑叠加起来,就会让人产生“是不是必须换系统才能装”的错觉。
容器化之后,这套依赖全被锁进镜像里了。Zabbix Server镜像内部自带编译好的PHP环境,前端Web镜像内置Nginx,数据库镜像更是开箱即用。宿主机要做的就是保证Docker守护进程稳定、磁盘空间够、网络策略不封端口。换句话讲,你的注意力应该从“发行版包怎么装”彻底转移到“镜像版本怎么选、环境变量怎么配、数据卷怎么挂”上。
1.2 容器对监控这类基础设施的真正价值
我见过很多团队把Zabbix容器化当成分散式部署的银弹,其实它的核心价值不是“跑得快”,而是“可预期”。拿最常见的升级场景对比一下:二进制升级Zabbix 5.0到6.4,你要先备份数据库、停服务、编辑一堆配置文件、跑升级脚本,任何一个依赖不对就原地崩溃;容器化的升级路径简单粗暴——先备份数据卷目录,然后docker compose pull拉新版本镜像,再docker compose up -d替换容器。数据库schema升级由Zabbix Server启动时自动执行,前端版本由镜像tag锁定,整个过程几乎没有人工介入的缝隙。
还有一个很容易被忽略的点:容器化之后监控数据的备份变得极其直观。以前你要想备份Zabbix的历史监控数据,得先搞清楚数据库在哪里、用哪种备份策略,搞得像个考古项目。现在只要把PostgreSQL数据卷挂载到宿主机目录,备份就是tar整个目录的事,恢复也一样。这种“数据与程序分离”的思维,用一次就会爱上。
1.3 什么场景下我不建议上容器
也不是所有场景都适合立刻容器化。第一,宿主机内核太老,比如CentOS 6这种,连新版Docker都装不上,硬上只会换来一堆兼容性报错。第二,你需要在监控采集链路上做非常底层的网络定制,比如自定义复杂iptables规则来配合多网卡分流,容器网络会引入一层NAT,排查问题时会多一个变量。第三,资源极度紧张的小机器,内存低于2G还要在同一台机器上跑数据库加Server加Web,那无论是不是容器都会很吃力,不如直接用Agent单机模式做轻量采集。
所以我的观点很明确:容器化是更优实践,但它不是魔法。真正决定成败的,永远是三个前置决策——数据卷挂载对不对、自定义网络建没建、镜像tag是不是同一版本线。
2. 镜像组合与网络规划:部署前容易被忽略的两个决策点
2.1 数据库镜像选MySQL还是PostgreSQL
Zabbix官方对MySQL和PostgreSQL都提供完整的镜像组合,并没有哪个“不支持”的说法,但实际部署的体感差别还是挺大的。我先给一张选型对照表:
| 对比维度 | PostgreSQL方案 | MySQL方案 |
|---|---|---|
| 镜像名 | zabbix-server-pgsql、zabbix-web-nginx-pgsql | zabbix-server-mysql、zabbix-web-nginx-mysql |
| 内存占用 | 相对更省,空闲时能压到几百MB | 基线稍高,需要额外tuning |
| 历史数据表维护 | 大表清理和分区维护更顺手 | 也能用,但要花精力调参数 |
| 团队熟悉度 | 看团队积累 | 团队若已有MySQL体系则上手更快 |
| 适合场景 | 全新部署、预算有限 | 已有MySQL运维体系、数据要进统一审计 |
我给新项目的建议是:除非你们团队已经有明确的MySQL绑定需求,否则全新部署优先选PostgreSQL。原因也很朴实:Zabbix的历史数据表写放大很夸张,PG在这类时序读写场景下的锁竞争更小,默认配置下内存也更可控。如果你在某个机房看到一台2G内存的小机器上跑Zabbix全套容器,pgsql方案大概率比mysql方案活得舒服。
镜像tag方面,别碰latest,一定要锁定版本线。Zabbix从6.4开始有明确的LTS版本,目前常用的长期支持版是7.0线,镜像tag类似alpine-7.0-latest。server、web、agent三个镜像尽量保持同一个版本线,否则会出现“前端提示Server version mismatch”这种看着像网络故障、实际是版本不一致的玄学问题。
2.2 自定义bridge网络与host模式的生死差异
Docker网络模式里,bridge、host、none三兄弟中,host模式看起来最省事,实际上最坑。host模式下容器直接复用宿主机网络栈,端口不带映射直接暴露,听着方便,但容器之间没法通过服务名解析地址,所有配置都得写localhost或者具体IP。一旦以后要拆成web与server分离部署,那些硬编码地址会让你怀疑人生。
自定义bridge网络才是我推荐的方案。在这个模式下,每个容器有独立IP,同网络内的容器可以直接用服务名互相访问。拿生活打比方:bridge网络就像一个小区,每栋楼有门牌号,你喊“张三”就能找到人;host模式是所有人挤在一层楼,距离确实近,但谁是谁全靠大喊大叫,管理和隔离都很乱。
实际操作中还有个小坑:如果你手动执行过docker network create zbx-net,那么在compose文件里引用这个网络时要标记external: true,否则compose会先尝试创建,结果报“network not found”。我更推荐直接在compose文件里声明网络,让compose自己管理生命周期,这样团队换人接手时看一份文件就能还原全部结构。
2.3 数据卷先于容器存在:一次“容器重删后配置全没”的反思
说个我自己翻车的真事。有段时间为了在Zabbix前端显示中文,我把一个中文字体文件copy进容器里覆盖了默认字体,当时界面马上就正常了,我还挺得意。结果过了几天觉得容器日志太多,手贱执行了docker compose up -d --force-recreate,再打开页面,中文全变成方块。原因很简单:所有落在容器可写层的改动,容器一重建就全部归零。后来我把字体文件放到宿主机的/mnt/zabbix/fonts目录,再挂载进容器,重启多少次都不会丢。
这只是一个小字体文件,丢了顶多重配一次,但数据库目录不挂载就是事故了。Zabbix几年的历史监控数据全在PG数据卷里,容器一删,数据灰飞烟灭。我的规范是:容器层不保存任何有状态数据,数据库盘、配置目录、字体目录、报警脚本全部用bind mount指到宿主机固定路径,比如/mnt/zabbix/pgdata。这里还要特别提醒一句:docker volume prune这个清理命令会把未被任何容器使用的具名卷直接干掉,如果你用的是compose自动创建的具名卷,误删后想恢复只能靠备份。用bind mount到固定目录则直观得多,备份就是tar -czf backup.tar.gz /mnt/zabbix。
3. 可直接落地的compose编排:数据库、Server、Web一次起齐
3.1 完整docker-compose.yml参考
下面这份编排是我目前生产环境在用的简化版,改成你的IP、密码和路径就能直接跑。我用的是Zabbix 7.0长期支持版搭配PostgreSQL 16镜像,Web前端用官方自带的Nginx版本。
version: "3.8" networks: zbx-net: driver: bridge services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: zabbix POSTGRES_PASSWORD: zabbix_pwd POSTGRES_DB: zabbix TZ: Asia/Shanghai volumes: - /mnt/zabbix/pgdata:/var/lib/postgresql/data networks: - zbx-net healthcheck: test: ["CMD-SHELL", "pg_isready -U zabbix -d zabbix"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped zabbix-server: image: zabbix/zabbix-server-pgsql:alpine-7.0-latest environment: DB_SERVER_HOST: postgres POSTGRES_USER: zabbix POSTGRES_PASSWORD: zabbix_pwd POSTGRES_DB: zabbix TZ: Asia/Shanghai ports: - "10051:10051" volumes: - /etc/localtime:/etc/localtime:ro depends_on: postgres: condition: service_healthy networks: - zbx-net restart: unless-stopped zabbix-web: image: zabbix/zabbix-web-nginx-pgsql:alpine-7.0-latest environment: ZBX_SERVER_HOST: zabbix-server DB_SERVER_HOST: postgres POSTGRES_USER: zabbix POSTGRES_PASSWORD: zabbix_pwd POSTGRES_DB: zabbix PHP_TZ: Asia/Shanghai ports: - "8080:8080" - "8443:8443" depends_on: - zabbix-server networks: - zbx-net restart: unless-stopped zabbix-agent: image: zabbix/zabbix-agent:alpine-7.0-latest container_name: zabbix-agent environment: ZBX_HOSTNAME: "Zabbix server" TZ: Asia/Shanghai ports: - "10050:10050" networks: - zbx-net restart: unless-stopped启动命令就两行:
docker compose up -d docker compose ps等几秒后,浏览器访问http://宿主机IP:8080,默认账号Admin,默认密码zabbix。第一次登录后第一件事就是改密码,这个没得商量。
3.2 逐项解读环境变量:它们各自在等谁
很多新手看到一堆环境变量就头大,其实这些变量回答的都是一件事:你在这个容器里要连谁。POSTGRES_USER、POSTGRES_PASSWORD、POSTGRES_DB是PostgreSQL容器首次初始化时创建账号和库用的,三个容器里的这三组值必须完全一致,否则就会报数据库连接失败。
DB_SERVER_HOST: postgres是Zabbix Server容器里最重要的配置,它告诉Server程序:数据库的主机名就叫postgres。这个“postgres”不是IP,而是我们在同一网络里定义的容器服务名。正因为Server和数据库在同一个zbx-net网络里,Docker的DNS解析会把“postgres”自动解析成数据库容器的IP。同理,Web容器里的ZBX_SERVER_HOST: zabbix-server也是靠服务名找到Zabbix Server。理解了这一点,你就不会再问“为什么我的容器里localhost不通”这种问题了——bridge网络下面,localhost永远是容器自己,找同伙必须喊服务名。
depends_on配合condition: service_healthy是另一个关键点。PostgreSQL容器启动不代表数据库已经完成初始化,如果没有健康检查,Zabbix Server很可能在数据库还没就绪时就开始连接,结果就是一连串的“Connection refused”。有了pg_isready健康检查,Compose会等数据库真正可以被连接时才启动Server容器。注意这个写法需要新版Docker Compose v2,如果你还在用老版docker-compose,就把condition那段删掉,靠restart: unless-stopped让Server自动重试。
3.3 镜像下载慢与“docker compose安装”这类前置问题
镜像下载慢是大陆机房永恒的话题,不一定是你网速的问题,很可能是Docker Hub的出口本身就抽风。通用解法是给Docker配置镜像加速器:编辑/etc/docker/daemon.json,加入registry-mirrors字段,填一个你常用云厂商提供的镜像加速地址,然后重启Docker服务。配置完可以用docker info查看Registry Mirrors是否生效。
如果你的环境连外网都不通,那就得走离线镜像路线:在一台能联网的机器上docker pull所需的镜像,然后docker save打成tar包,再docker load导入目标机器。这个方法看着笨,但在等保机房和隔离网环境里是最可靠的。
还有一类问题是“docker compose安装”。新版Docker安装包默认带docker compose插件,但有些老教程只让你装docker-ce,顺手把docker-compose-plugin也装上,否则执行docker compose up时会提示command not found。装了插件后可以用docker compose version确认。如果你非要用老版docker-compose二进制也不是不行,但新版插件无论是语法支持还是命令输出都比老版舒服太多,能用新就上新。
4. 从启动报错到网络不通:容器化Zabbix的高频故障排查链路
4.1 docker API权限:permission denied的完整解决路径
刚装好Docker就翻车的第一道坎,十有八九是这个报错:permission denied while trying to connect to the docker api。这个报错的根源非常直白——你的当前用户不在docker组里,而Docker CLI需要读取/var/run/docker.sock这个socket文件。查看这个socket的权限你可以发现,它只对root用户和docker组开放了读写权限。
解决办法是把自己的账号加进docker组:
sudo usermod -aG docker yourusername注意这条命令执行后要重新登录终端,或者执行newgrp docker切换当前组,否则当前会话依然继承旧的权限列表。有些桌面环境更省事,但原理一样。这里要敲个警钟:能操作Docker等价于能在宿主机上为所欲为,因为容器是可以挂载宿主机目录的。所以生产环境里别随便给无关账号开docker组权限,使用sudo反而更安全,因为至少多一层审计。
如果你是Windows或macOS用户,报错往往不是socket权限,而是Docker Desktop启动失败,提示类似virtualization support not detected。这是系统虚拟化没开或者WSL 2组件缺失。Windows要在BIOS里打开CPU虚拟化,并安装WSL 2内核更新包;macOS老款Intel机器要确认HyperKit框架没被其他安全软件拦截。总之先把Docker Desktop的状态图标点绿,再做后面的容器操作。
4.2 数据库起不来的两种根因与日志定位法
容器编排完,最常出现的是PostgreSQL容器一直在Restarting,网页一打开就是“Database connection failed”。遇到这个情况别急着怀疑密码,先按下面的排查链路走一遍。
第一步,docker compose ps -a看哪个容器状态异常。第二步,docker logs --tail 100 postgres看数据库容器日志。日志能告诉你绝大多数真相。我实际处理过的最典型案例有两个。
根因A:POSTGRES_DB配置不一致。数据库初始化时建的库叫zabbix,但Server容器里POSTGRES_DB写成了zabbix_test,两者不匹配,数据库日志直接报database "zabbix_test" does not exist。这种问题在compose文件里一眼就能对比出来,但新手往往会先跑去检查网络。
根因B:宿主机挂载目录的权限不对。PostgreSQL容器内的进程用户UID是999,如果你手动创建了/mnt/zabbix/pgdata且owner是root,容器启动时初始化和写入都会失败,日志里会出现Permission denied。解决方法是chown -R 999:999 /mnt/zabbix/pgdata,但如果这个目录里已经有残留的partial数据,建议先清空目录再重新初始化,否则PG会一直报文件已存在相关错误。
还有一类更隐蔽的根因是磁盘满。Zabbix历史数据表吃空间速度远超你的直觉,尤其默认保留周期没调过的情况下,一块20G的数据盘撑不了几个月。日志出现No space left on device时,先df -h看挂载点,然后去前端调整Housekeeper保留策略,或者把历史数据表分区方案配上。
4.3 容器间“互相找不到”的真正排查顺序
如果数据库正常,但打开Web页面提示“Unable to connect to Zabbix server [zabbix-server]:10051”,这就是典型的容器间网络互通问题。我说一下我自己的排查顺序,基本能定位90%的场景。
先确认三个容器是不是都在同一个自定义网络里。执行docker inspect zabbix-server --format '{{json .NetworkSettings.Networks}}',看返回的网络名是不是zbx-net。如果某个服务忘了写networks段,它会被塞进默认的bridge网络,跟zbx-net里的容器互不可见。这个坑在compose文件里特别容易漏,因为compose不强制要求每个服务都声明网络。
接着验证DNS解析。正常来说,在Server容器里执行getent hosts postgres,应该返回一个172.x开头的IP。如果能解析出来,说明网络层面没问题,问题大概率在Zabbix Server服务本身,这时再看docker logs zabbix-server的日志。如果解析失败,就去docker network inspect zbx-net看容器的IP归属,确认是不是有容器掉线。
最后再强调一个容易混淆的概念:端口映射和容器间通信是两码事。容器A没发布10051端口到宿主机,不影响容器A和容器B在同一个自定义网络里互相访问10051端口。只有宿主机外部或跨主机访问才依赖ports映射。所以排查Web到Server的连接问题时,先看容器网络,别一上来就改防火墙。
4.4 时间、语言与Web端访问异常:容易被忽略的隐藏细节
有一类问题不属于启动故障,但上线后一定会碰到,就是时区。容器默认使用UTC时间,不处理的话你会发现监控趋势图的横轴永远比北京时间慢8小时,告警时间也怪怪的。在compose里给Server、Web、Agent三个容器都加上TZ: Asia/Shanghai,再挂载/etc/localtime:/etc/localtime:ro,一套操作下来才算是“入乡随俗”。
再一个是中文字体。Zabbix前端界面可以切中文,但镜像里不包含中文字体库,仪表盘上的中文或者图形里的中文标签会显示成方块。两个解决方案,一个是把宿主机里的中文字体文件挂载到容器里,然后在前端界面“字体”配置里指定该字体文件名字;第二个更省事,直接给Web容器挂载一个包含fonts-noto-cjk的字体目录。字体问题是小问题,但因为是视觉层面的,用户感知特别强,建议新部署时直接配好。
如果Web页面打开慢,还要看一眼Nginx容器日志里有没有大量失败解析记录。前端与Server分离部署后,Web容器去解析Server时如果走的是外部域名或公网IP,DNS解析抖动会直接拖垮页面响应。后面会聊到分离部署,这里先记住:能走内网IP就绝不走公网域名。
5. 纳入主机与处理告警:上线后真正会用的日常操作
5.1 新主机监控三步:agent容器、主机配置、连通性测试
部署完成只是开始,真正的高频操作是把新机器纳入监控。我习惯的做法是三步走。第一步,在被监控机器上起一个agent容器。日常建议用host网络模式,这样agent共享宿主机网络栈,Zabbix Server直接连被监控机的IP就能通信,省去容器网络路由的麻烦:
docker run -d --name zabbix-agent --network host \ -e ZBX_HOSTNAME=web-server-01 \ -e ZBX_SERVER_HOST=192.168.1.10 \ zabbix/zabbix-agent:alpine-7.0-latest第二步,在Zabbix Web界面里“数据采集→主机”创建主机,主机名称必须和容器里的ZBX_HOSTNAME完全一致,然后填IP地址,端口默认10050。第三步,配置模板并检查可用性。最常用的模板是“Linux by Zabbix agent”,应用到主机后,过一两分钟再去主机列表看,可用性变成绿色就说明链路通。
这里有一个非常容易误判的地方:被动模式下Zabbix Server要去连Agent的10050端口,主动模式下Agent反而会主动连Server的10051端口。你如果只想让监听的机器主动往外连,可以选“主动模式”模板,但无论哪种模式,两边端口在防火墙和安全组里都要放通对应方向。我之前把一台数据库主机加入监控,半天都是灰色“Zabbix agent is not running”,最后发现是agent容器用了bridge模式,Server拿到的是NAT地址,路由怎么都搭不上。改成--network host后立刻解决。
5.2 “问题已解决”是什么意思:手动消除告警的正确姿势
很多人搜“zabbix监控系统显示的主机问题已经解决”“告警如何手动消除”,本质上是对Zabbix问题事件机制不熟。我先铺垫一个概念:当某个监控项从异常恢复到正常值,Zabbix会自动把这条触发器问题标记为“已解决(RESOLVED)”,此时问题列表里那行会变为绿色,不再触发动作。你不需要做任何手动操作。但如果你在值班系统里看到告警邮件早已发送,最终需要在Zabbix里留下处理闭环,那就要手工确认一次。
具体操作路径:进入“监测→问题”,勾选要处理的问题,点击底部“更新”,把状态改为“已解决”,并填写备注,比如“凌晨2点的磁盘告警,已扩容并观察30分钟”。这相当于你在告警事件上盖了一个“我处理过”的章,后续审计和复盘都有据可查。团队协作时还可以给问题打“确认”标记,表示已有人接手,避免多个人同时盯着同一个报警重复处理。
比手动消除更科学的做法是提前使用维护窗口。比如夜里要升级数据库、割接网络,可以提前在“配置→维护”里创建一段维护周期,把涉及的主机加进去。维护期间这些主机的告警会被主动抑制,不会半夜震你手机。维护结束后Zabbix会继续正常监控。这套机制想清楚了,“告警如何手动消除”就不再是操作问题,而是流程设计问题。
5.3 生产环境下的Web与Zabbix分离部署经验
当单机容量吃紧,或者出于安全要求想把Web前端和Zabbix Server拆到不同机器时,容器化的灵活性就体现出来了。核心思路是:不再依赖同一个bridge网络内的服务名解析,而是通过宿主机IP显式互联。
分离部署至少要把端口和服务拆开:Zabbix Server机器上只保留Server容器和PostgreSQL容器,Web容器跑在另一台机器上。Web容器里的ZBX_SERVER_HOST要改成Zabbix Server所在机器的IP,而不是服务名;DB_SERVER_HOST改成数据库所在机器的IP。Zabbix Web前端是直连数据库读数据的,所以数据库端口也要对Web容器开放。
跨主机通信的端口放行策略务必收敛:数据库端口(PostgreSQL默认5432)只放行给Zabbix Server和Web前端所在机器的IP;Server端口10051放行给Web前端和所有主动模式的Agent;Agent端口10050放行给Zabbix Server。云环境里安全组别用0.0.0.0/0,加个白名单不费事。
如果还想再进一步,给Web前端套一层反向代理做域名和HTTPS,那我建议用Nginx或Caddy。有个细节容易被忽略:Zabbix 6+的某些页面交互会用到WebSocket,反向代理配置里需要给对应location加上Upgrade和Connection头,否则前端操作偶尔会卡住。前端页面的静态资源也可以让反向代理加一层缓存,体感会好很多。
最后分享几句个人体会
我用Docker搭Zabbix也有两年多了,最大的感受是,真正让人省心的不是“docker compose up -d”那一瞬间的丝滑,而是数据卷和编排文件带来的可预期性。我现在每次升级前的固定动作就是先tar备份整个/mnt/zabbix目录,然后docker compose pull拉新镜像,再docker compose up -d。如果哪次升级后Web页面提示连不上Server,我从来不会去翻网络图,而是老老实实检查三个环境变量:DB_SERVER_HOST、ZBX_SERVER_HOST、POSTGRES_PASSWORD,十次有八次问题就出在这三个值上。最后再分享一个小习惯:给每个容器都配上restart: unless-stopped,但遇到需要变更配置时,一定先docker compose down再改再起,不要让服务在异常状态下反复重启,否则日志会刷得你怀疑人生。希望这篇分享对正在部署Zabbix的你有点帮助,也欢迎大家把自己踩过的更离奇的坑发在评论区,我顺手记进排查笔记。