
Zabbixとは、サーバーやネットワーク機器の状態をエージェントやSNMPで集め、しきい値(トリガー)を超えたらメールなどで知らせるオープンソースの統合監視ツールです。本体のZabbix server、PHPで動く画面(フロントエンド)、PostgreSQLかMySQLのデータベース、監視対象に入れるZabbix agentの4つで構成されます。Prometheusのように時系列データベースを軸にした仕組みではなく、「ホストを登録し、テンプレートを当て、トリガーとアクションで通知する」という設定モデルが中心にあるのが特徴です。
対象は、社内サーバーを目視とExcelで見ていた情シスと、自宅ラボの2台目を見張りたい人です。執筆時点のLTSであるZabbix 7.0(検証したのは7.0.31)を公式Dockerイメージで1台目のサーバーに建て、2台目のサーバーで動くzabbix-agent2をホスト登録し、自作のアイテムとトリガーで本当に障害を起こし、Mailpit(SMTPを受けるだけのコンテナ)に届いたメールをAPIで確かめるところまで進めます。コマンドや画面に出てくる nagoyadevlab01(1台目)と nagoyadevlab02(2台目)は検証機に付けたホスト名の例なので、自分のサーバー名に読み替えてください。画面での操作は言葉で示し、同じ操作をJSON-RPC APIのcurlでも残すので、手順書として丸ごと再実行できます。
この記事でわかること
- PostgreSQL、Zabbix server、フロントエンド、agent2を2つ、Mailpitの6サービスを1つの
compose.yamlにまとめた構成と、各設定を選んだ理由 - 初期ユーザー
Adminのパスワード変更と自分用のSuper adminユーザー作成を、最初にAPIで済ませる手順 - 2台目のホスト登録、ディスク空き容量のアイテムとトリガー、Emailメディアタイプとアクションの作り方(画面の場所とAPIの両方)
- 実際に障害を起こしてから43秒でメールが届いた計測と、6コンテナ合計で約300MiBのメモリ、DBサイズ約66MBの実測値
- エージェントが再起動を繰り返す、rejected、host not found、Zabbix server is not runningという4種類の実物のエラーと直し方
Zabbixの構成要素と、7.0 LTSを選ぶ理由
中心のZabbix serverは、監視対象からデータを取り、トリガーを評価し、アクションを実行する常駐プロセスで、TCPの10051番でエージェントやプロキシからの接続を受けます。フロントエンドはnginxとPHPで動く画面で、設定はすべてデータベースに書き込み、serverは設定キャッシュを定期的にDBから読み直します。つまり画面とserverは直接会話しておらず、画面が「serverが動いているか」を確かめるのは10051番へのソケット接続だけです。この構造を知っておくと、後述する「Zabbix server is not running」の意味がすぐわかります。
監視対象に入れるZabbix agent(今回はGo実装のagent 2)には2つの動き方があります。パッシブチェックはserverが10050番のエージェントに接続して値を聞きに行く方式で、エージェント側の Server= に書いた接続元だけを受け付けます。アクティブチェックはエージェントがserverの10051番へ出向いて「自分に割り当てられた監視項目」を取りに行き、値を送りつける方式で、こちらはエージェントの Hostname= がserver側のホスト名と一致していないと項目を受け取れません。この2つの照合条件が、エージェント絡みのトラブルのほぼすべてです。
設定の単位は、値を1つ取る「アイテム」、アイテムの値に対する条件式「トリガー」、トリガーが問題になったときに何をするかの「アクション」、通知手段の「メディアタイプ」とユーザーごとの「メディア」(宛先)です。テンプレートはアイテムとトリガーの束で、ホストに当てると一式が有効になります。今回は公式テンプレート「Linux by Zabbix agent」を当てた上で、自分のアイテムとトリガーを1組だけ追加します。
バージョンは執筆時点でLTSの7.0を使います。公式のライフサイクル表では、7.0 LTSは2024年6月4日リリースでフルサポートが2027年6月30日、リミテッドサポートが2029年6月30日まで。7.4は2025年7月1日リリースの通常版で、フルサポートは次の8.0 LTSが出るまで、8.0 LTSは2026年第3四半期の予定で執筆時点では未リリースです。監視サーバーは一度建てると数年動かすので、リリース直後の通常版より、パッチが2年以上出続けるLTSを選ぶほうが更新作業の回数が読めます。Dockerイメージのタグは alpine-7.0-latest のように「ベースOS-系列-latest」で固定し、系列を書かない latest(執筆時点では7.4)は使いません。ライセンスは7.0からAGPLv3で、それ以前の版はGPLv2です。
判断表:Zabbix・Prometheus+Grafana・Uptime Kuma・SaaS監視
選ぶ基準は「何を見たいか」より「誰が設定を持ち続けるか」です。SNMPしか話せないスイッチやUPSがあるか、監視項目を増やすのが情シス1人なのか、通知先を部署や深刻度で振り分けたいか、で答えが変わります。下の表は執筆時点の各ツールの設計思想を、少人数の情シスの目線で並べたものです。Prometheus+Grafanaは別の記事で同じように手を動かしているので、迷ったら両方の手順を見比べてください。
| 観点 | Zabbix 7.0 | Prometheus + Grafana | Uptime Kuma | SaaS監視(Datadogなど) |
|---|---|---|---|---|
| データの集め方 | エージェント(パッシブ/アクティブ)、SNMP、IPMI、HTTP、外部スクリプト。serverが取りに行く | 各対象にexporterを置き、PrometheusがHTTPでスクレイプする | HTTP/TCP/pingなどの外形監視のみ。対象に何も入れない | ベンダーのエージェントを入れ、クラウドに送る |
| ネットワーク機器(SNMP) | 標準機能。機種別テンプレートが同梱 | snmp_exporterを別途構成する | 対象外 | 製品による |
| しきい値と通知 | トリガー式とアクションで、深刻度・ホストグループ・時間帯で宛先を振り分ける | Alertmanagerのルーティング設定をYAMLで書く | 監視ごとに通知先を選ぶ。条件分岐はほぼない | 画面で作る。多くは有料プランの機能 |
| 設定の持ち方 | DBに保存。画面かAPIで変更、テンプレートで再利用 | YAMLとPromQL。Gitで管理しやすい | 画面のみ | 画面とAPI。エクスポートは製品次第 |
| 学習コスト | アイテム・トリガー・アクションのモデルを覚える必要がある。覚えれば画面だけで完結 | PromQLとexporterの選定。ダッシュボードは自作 | ほぼゼロ | 低いが、請求体系の理解が要る |
| 日本語の情報 | 日本語の解説記事や書籍が多く、国内に商用サポートや構築を請け負う事業者もある | 英語中心。コミュニティは大きい | UIは日本語化されている | 製品による |
| 向く環境 | 物理サーバー、VM、ネットワーク機器が混ざるオフィス。通知の振り分けが必要 | コンテナやKubernetesが主役。メトリクスを自分で描きたい | 「サービスが生きているか」だけ知りたい少数のURL | 運用人員がいない、費用より工数を減らしたい |
Zabbixが過剰なのは、見たい対象がWebサービス数個の死活だけで、SNMP機器もなく、通知先も1つの場合です。その条件ならUptime Kumaで足ります。逆に、スイッチやUPSの死活をSNMPで取りたい、深刻度によって通知先を分けたい、監視項目をテンプレートで100台に配りたい、のどれかが当てはまるならZabbixの設定モデルは覚える価値があります。ログの検索や相関分析は監視の担当ではなくSIEMの領域なので、Zabbixに寄せないほうが運用が楽です。
compose.yaml:6つのサービスと各設定の理由
作業ディレクトリは ~/zabbix とし、以下の2ファイルを置きます。Composeの書き方そのもの(.env の展開、depends_on、ボリューム)はDocker Compose入門に譲り、ここではZabbix固有の判断だけ説明します。
name: zabbix-server-monitoring-docker
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: zabbix
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: zabbix
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U zabbix -d zabbix"]
interval: 10s
timeout: 5s
retries: 12
restart: unless-stopped
zabbix-server:
image: zabbix/zabbix-server-pgsql:alpine-7.0-latest
hostname: nagoyadevlab01
environment:
DB_SERVER_HOST: postgres
POSTGRES_USER: zabbix
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: zabbix
TZ: Asia/Tokyo
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
agent-lab01:
image: zabbix/zabbix-agent2:alpine-7.0-latest
network_mode: "service:zabbix-server"
environment:
ZBX_HOSTNAME: nagoyadevlab01
ZBX_SERVER_HOST: 127.0.0.1
depends_on:
- zabbix-server
restart: unless-stopped
zabbix-web:
image: zabbix/zabbix-web-nginx-pgsql:alpine-7.0-latest
environment:
DB_SERVER_HOST: postgres
POSTGRES_USER: zabbix
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: zabbix
ZBX_SERVER_HOST: zabbix-server
ZBX_SERVER_NAME: nagoyadevlab01
PHP_TZ: Asia/Tokyo
ports:
- "127.0.0.1:8080:8080"
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
agent-lab02:
image: zabbix/zabbix-agent2:alpine-7.0-latest
hostname: nagoyadevlab02
environment:
ZBX_HOSTNAME: nagoyadevlab02
ZBX_SERVER_HOST: zabbix-server
tmpfs:
- /srv/data:size=64m
restart: unless-stopped
mailpit:
image: axllent/mailpit:v1.31
ports:
- "127.0.0.1:8025:8025"
restart: unless-stopped
volumes:
pgdata:
POSTGRES_PASSWORD=zbx-lab-pg-9f3a2c
postgres:Zabbix 7.0が対応するPostgreSQLは執筆時点の要件表で13.0から18.xまでなので、その中で枯れている16を選びました。healthcheck に pg_isready を入れ、serverとフロントエンドは condition: service_healthy でDBが応答するまで起動を待ちます。これを省くと、初回起動でserverがDBより先に立ち上がって接続に失敗し、再起動で復帰するまでの数十秒間、ログが「接続できない」で埋まります。DBのパスワードは .env に置き、compose.yamlには書きません。
zabbix-server:公式イメージのエントリポイントは、POSTGRES_DB のデータベースに dbversion テーブルが無ければスキーマと初期データを自動で投入します。だからSQLファイルを流す手順はありません。DB_SERVER_HOST の既定値は postgres-server なので、サービス名 postgres に合わせて明示します。TZ: Asia/Tokyo はserverのログとメール本文の時刻({EVENT.TIME})に効きます。10051番はホストに公開していません。エージェントは同じComposeネットワーク内からサービス名で到達できるからで、公開するのは本当に別マシンのエージェントを足すときだけです。
agent-lab01:Zabbixの初期データには「Zabbix server」というホストが最初から入っていて、127.0.0.1:10050 のエージェントを監視する設定になっています。コンテナ構成ではserverコンテナの127.0.0.1に誰もいないので、放っておくとこのホストは赤い「到達不能」のまま、テンプレートのトリガーが問題として上がり続けます。そこでagent2を network_mode: "service:zabbix-server" で起動し、serverコンテナとネットワーク名前空間を共有させます。名前空間を共有するとホスト名もserverと同じ(例では nagoyadevlab01)になります。このとき hostname: は書けません。docker run --network container:… に --hostname を足して試すと、Dockerが「conflicting options: hostname and the network mode」で拒否しました。ZBX_SERVER_HOST: 127.0.0.1 は「serverは127.0.0.1から接続してくる」という意味で、この値をagent-lab02にコピーしてはいけません(つまずきポイントで実物を見せます)。この設計には代償が1つあります。serverコンテナを止めて起動し直す、または作り直すと、agent-lab01が入っていたネットワーク名前空間は消え、agent-lab01には lo しか残りません。serverに手を入れたあとは docker compose up -d --force-recreate agent-lab01 でサイドカーも作り直します。忘れたときに何が起きるかも、つまずきポイントに載せました。
zabbix-web:ZBX_SERVER_HOST は画面がserverの生存確認に使う宛先で、既定値の zabbix-server はサービス名と一致しているものの、意図を残すために明示しています。ZBX_SERVER_NAME は画面の右上とブラウザのタブに出る名前です。ポートは 127.0.0.1:8080 にだけ束縛します。"8080:8080" と書くと0.0.0.0で待ち受け、初期パスワードのままの監視サーバーがLANやインターネットに露出します。外から使うなら、リバースプロキシかVPNを前に置いてください。
agent-lab02:これが「2台目のサーバー」役です。hostname: でコンテナのホスト名(例では nagoyadevlab02)を決め、ZBX_HOSTNAME にも同じ値を書きます(イメージの既定値はコンテナのホスト名なので省略もできますが、server側のホスト名と同じ文字列だと目で確かめられるよう明示しました)。ZBX_SERVER_HOST: zabbix-server は、パッシブチェックの許可元とアクティブチェックの宛先の両方に展開されます。/srv/data に64MBのtmpfsを載せているのは、後で「ディスクの空きが減った」という本物の障害を、ホストのディスクを汚さずに起こすためです。
mailpit:SMTPを1025番で受け、届いたメールを8025番のWeb画面とREST APIで見せるだけのコンテナです。既定ではメッセージは終了時に消える一時ファイルに保存され、保持は500通までなので、検証にはちょうどよく、本番の通知先には使えません。本番ではここを社内のSMTPリレーやMicrosoft 365のSMTP送信に置き換えます。
Composeのネットワークでは、サービス名だけでなく hostname: に書いた名前も同じIPに解決されます。agent-lab02から引くと、hostname: に書いた名前(nagoyadevlab01)と zabbix-server はどちらもserverコンテナのIPでした。
docker compose exec agent-lab02 getent hosts nagoyadevlab01 zabbix-server
172.18.0.5 nagoyadevlab01 nagoyadevlab01
172.18.0.5 zabbix-server zabbix-server
起動から最初のログインまで
起動は docker compose up -d だけです。イメージの取得が終わると、postgresがhealthyになるのを待ってからserverとフロントエンドが立ち上がります。
mkdir -p ~/zabbix && cd ~/zabbix
docker compose up -d
Network zabbix-server-monitoring-docker_default Creating
Network zabbix-server-monitoring-docker_default Created
Volume zabbix-server-monitoring-docker_pgdata Creating
Volume zabbix-server-monitoring-docker_pgdata Created
Container zabbix-server-monitoring-docker-postgres-1 Healthy
Container zabbix-server-monitoring-docker-zabbix-web-1 Started
Container zabbix-server-monitoring-docker-zabbix-server-1 Started
Container zabbix-server-monitoring-docker-agent-lab01-1 Started
スキーマ投入の後、serverのログに「Starting Zabbix Server」と版番号が出ます。記事中の版番号はすべてこのログと -V の出力から取っています。
docker compose logs zabbix-server | grep -m1 "Starting Zabbix Server"
zabbix-server-1 | Starting Zabbix Server. Zabbix 7.0.31 (revision e7da53e).
docker compose exec agent-lab02 zabbix_agent2 -V | head -1
zabbix_agent2 (Zabbix) 7.0.31
docker compose ps --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
agent-lab01 Up About a minute
agent-lab02 Up About a minute
mailpit Up About a minute (healthy)
postgres Up About a minute (healthy)
zabbix-server Up About a minute
zabbix-web Up About a minute (healthy)ブラウザで http://127.0.0.1:8080/ を開くとログイン画面になります。初期ユーザーは Admin、パスワードは zabbix です(公式マニュアルのフロントエンド設置の章に書かれている値です)。この記事ではAPIも使うので、curlで同じ確認をしておきます。APIのエンドポイントは api_jsonrpc.php、リクエストはJSON-RPC 2.0で、Content-Typeに application/json-rpc を付けます。
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
200
毎回curlのヘッダーを書くのは面倒なので、リクエスト本文のJSONファイルを渡すと送信してくれる小さなシェルスクリプトを置きます。環境変数 TOKEN があれば Authorization: Bearer ヘッダーを付けます。7.0のAPIではこのヘッダー方式が推奨で、本文に auth を入れる旧方式は非推奨です。user.login だけはヘッダーを付けると「The "user.login" method must be called without authorization header.」で拒否されるので、スクリプト側で外しています。
#!/bin/sh
# 使い方: ./zbx.sh request.json
# 環境変数 TOKEN があれば Authorization ヘッダーを付ける(user.login だけはヘッダーなしで送る)
if [ -n "$TOKEN" ] && ! grep -q '"user.login"' "$1"; then
curl -s -H 'Content-Type: application/json-rpc' -H "Authorization: Bearer $TOKEN" \
-d @"$1" http://127.0.0.1:8080/api_jsonrpc.php
else
curl -s -H 'Content-Type: application/json-rpc' -d @"$1" http://127.0.0.1:8080/api_jsonrpc.php
fi
echo
{"jsonrpc":"2.0","method":"apiinfo.version","params":{},"id":1}
chmod +x zbx.sh
./zbx.sh apiinfo.json
{"jsonrpc":"2.0","result":"7.0.31","id":1}
apiinfo.version は認証なしで呼べる唯一に近いメソッドで、画面が開けない状況でもフロントエンドとDBの疎通を確かめる道具になります。以降のJSONファイルも、すべて ~/zabbix にエディタで保存して ./zbx.sh ファイル名 で送る形です。jqは応答の整形に使うので、入っていなければパッケージで入れてください。
最初のセキュリティ設定:管理ユーザー作成とAdminのパスワード変更
ホストを登録する前に、初期パスワードのままの Admin を片付けます。手順は「自分用のSuper adminを作る → そのユーザーでログインし直す → Adminのパスワードを変える」の順です。この順にするのは、Adminを先にいじって締め出されても戻れる経路を残すためと、APIの仕様として、自分自身のパスワード変更には current_passwd が要る一方、Super adminが他人のパスワードを変えるときは不要だからです。
画面では、執筆時点のメニューで「Users → Users → Create user」を開き、Username に自分で決めた管理用のユーザー名、Groups に「Zabbix administrators」、Password を入力し、「Permissions」タブでRoleを「Super admin role」にします。メニュー名はバージョンで変わるので、見つからなければ左メニューの検索を使ってください。この記事の画面とコマンド例に出てくる ryoma は検証用に付けた名前で、決まった名前ではありません。JSONの username や login-ryoma.json のファイル名も含めて、自分のユーザー名に読み替えてください。APIでは、まずAdminでログインしてトークンを取り、ロールとユーザーグループのIDを引いてから user.create を呼びます。トークンは export TOKEN=… で環境変数に入れます。export を付けない TOKEN=… は今のシェルだけの変数で、子プロセスとして動くzbx.shからは見えないため、以降の呼び出しがすべて「Not authorized.」で返ってきます。
{"jsonrpc":"2.0","method":"user.login","params":{"username":"Admin","password":"zabbix"},"id":1}
{"jsonrpc":"2.0","method":"role.get","params":{"output":["roleid","name"]},"id":1}
{"jsonrpc":"2.0","method":"usergroup.get","params":{"output":["usrgrpid","name"]},"id":1}
./zbx.sh login-admin.json
{"jsonrpc":"2.0","result":"9fed0286decc7ba15e00042e7b36f939","id":1}
export TOKEN=$(./zbx.sh login-admin.json | jq -r .result)
./zbx.sh lookup-ids.json | jq -c ".result[]"
{"roleid":"1","name":"User role"}
{"roleid":"2","name":"Admin role"}
{"roleid":"3","name":"Super admin role"}
{"roleid":"4","name":"Guest role"}
./zbx.sh usergroup-get.json | jq -c ".result[]"
{"usrgrpid":"7","name":"Zabbix administrators"}
{"usrgrpid":"8","name":"Guests"}
{"usrgrpid":"9","name":"Disabled"}新規インストールならロール3がSuper admin、グループ7がZabbix administratorsですが、アップグレードを重ねた環境では違うことがあるので、IDは必ず自分の応答から拾ってください。user.create の必須項目は username、passwd、roleid で、名前と姓は画面の表示名(「Ryoma T」)になります。
{"jsonrpc":"2.0","method":"user.create","params":{
"username":"ryoma","name":"Ryoma","surname":"T",
"passwd":"Lab01-Sakura-2026!",
"roleid":"3",
"usrgrps":[{"usrgrpid":"7"}]
},"id":1}
{"jsonrpc":"2.0","method":"user.login","params":{"username":"ryoma","password":"Lab01-Sakura-2026!"},"id":1}
{"jsonrpc":"2.0","method":"user.update","params":{"userid":"1","passwd":"Ops-Console-2026!"},"id":1}
./zbx.sh user-create.json
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}
./zbx.sh login-ryoma.json
{"jsonrpc":"2.0","result":"6491d5b63ddbdffbfd61bb13e2924b7e","id":1}
export TOKEN=$(./zbx.sh login-ryoma.json | jq -r .result)
./zbx.sh admin-password.json
{"jsonrpc":"2.0","result":{"userids":["1"]},"id":1}
./zbx.sh login-admin.json
{"jsonrpc":"2.0","error":{"code":-32500,"message":"Application error.","data":"Incorrect user name or password or account is temporarily blocked."},"id":1}最後の行は「古いパスワード zabbix ではもう入れない」ことの確認です。ここから先のAPI呼び出しはすべて、いま作った管理ユーザー(例ではryoma)のトークンで行い、Adminは金庫にしまいます。パスワードは記事用の使い捨て値なので、自分の値に置き換えてください。
7.0の既定のパスワードポリシーは最小8文字で、さらにユーザー名・名・姓を含む文字列を拒否します。検証中、姓を「T」にしたら「Monitor」を含むパスワードが「must not contain user's name, surname or username」で弾かれ、Adminのほうは「Admin」を含む文字列が同じ理由で弾かれました。1文字の姓は実質「その文字を含むパスワード禁止」になるので、表示名の付け方には注意してください。
2台目のサーバーをホストとして登録する
エージェント側の準備は、compose.yamlの環境変数で済んでいます。イメージが実際に生成した設定を見ると、ZBX_SERVER_HOST が Server(パッシブの許可元)と ServerActive(アクティブの宛先)の両方に展開され、ZBX_HOSTNAME が Hostname になっています。実機のLinuxに入れるときは、この3行を /etc/zabbix/zabbix_agent2.conf に書くのと同じことです。
docker compose exec agent-lab02 grep -E "^(Server|ServerActive|Hostname)=" /etc/zabbix/zabbix_agent2.conf
Server=zabbix-server
ServerActive=zabbix-server
Hostname=nagoyadevlab02
画面では、執筆時点のメニューで「Data collection → Hosts → Create host」を開き、Host name に監視対象のホスト名(例では nagoyadevlab02)、Templates に「Linux by Zabbix agent」、Host groups に「Linux servers」を選び、Interfaces で「Add → Agent」を押して DNS name にserverから名前解決できるそのホストの名前(例では同じ nagoyadevlab02)、Connect to を「DNS」、Port を 10050 にします。Host name はエージェントの Hostname= と1文字も違えてはいけません。使える文字は英数字、スペース、ドット、ハイフン、アンダースコアです。
APIでは、ホストグループとテンプレートのIDを引いてから host.create を呼びます。必須は host と groups で、インターフェースの type: 1 がエージェント、useip: 0 がDNS名で接続する指定です。
{"jsonrpc":"2.0","method":"hostgroup.get","params":{"output":["groupid","name"],"filter":{"name":["Linux servers","Zabbix servers"]}},"id":1}
{"jsonrpc":"2.0","method":"template.get","params":{"output":["templateid","host"],"filter":{"host":["Linux by Zabbix agent"]}},"id":1}
{"jsonrpc":"2.0","method":"host.create","params":{
"host":"nagoyadevlab02",
"groups":[{"groupid":"2"}],
"templates":[{"templateid":"10001"}],
"interfaces":[{"type":1,"main":1,"useip":0,"ip":"","dns":"nagoyadevlab02","port":"10050"}]
},"id":1}
{"jsonrpc":"2.0","method":"host.get","params":{"output":["hostid","host","status"],"filter":{"host":["nagoyadevlab02"]},"selectInterfaces":["interfaceid","dns","useip","port","available","error"]},"id":1}
./zbx.sh hostgroup-get.json | jq -c ".result[]"
{"groupid":"2","name":"Linux servers"}
{"groupid":"4","name":"Zabbix servers"}
./zbx.sh template-get.json | jq -c ".result[]"
{"templateid":"10001","host":"Linux by Zabbix agent"}
./zbx.sh host-create.json
{"jsonrpc":"2.0","result":{"hostids":["10683"]},"id":1}
./zbx.sh host-get-lab02.json | jq -c ".result[]"
{"hostid":"10683","host":"nagoyadevlab02","status":"0","interfaces":[{"interfaceid":"31","dns":"nagoyadevlab02","useip":"0","port":"10050","available":"1","error":""}]}
available は0が未確認、1が到達可、2が到達不能で、画面の「Hosts」一覧にある「ZBX」の四角い印の色(緑・赤・灰)と同じものです。登録直後は0で、serverが設定キャッシュを読み直してテンプレートのアイテムを最初に取りに行った時点で1になります。検証では1分待たずに1になりました。hostid(10683)と interfaceid(31)は次の節で使うので控えておきます。
初期データの「Zabbix server」ホストも、名前を実機のホスト名(例では nagoyadevlab01)に合わせて変えておきます。agent-lab01がserverと同じ名前空間で動いているので、127.0.0.1:10050のインターフェースはそのまま使えます。
{"jsonrpc":"2.0","method":"host.get","params":{"output":["hostid","host","name","status"],"selectInterfaces":["interfaceid","ip","dns","useip","port","available","error"]},"id":1}
{"jsonrpc":"2.0","method":"host.update","params":{"hostid":"10084","host":"nagoyadevlab01","name":"nagoyadevlab01"},"id":1}
./zbx.sh host-rename-server.json
{"jsonrpc":"2.0","result":{"hostids":["10084"]},"id":1}
./zbx.sh host-get-server.json | jq -c ".result[]"
{"hostid":"10683","host":"nagoyadevlab02","name":"nagoyadevlab02","status":"0","interfaces":[{"interfaceid":"31","ip":"","dns":"nagoyadevlab02","useip":"0","port":"10050","available":"1","error":""}]}
{"hostid":"10084","host":"nagoyadevlab01","name":"nagoyadevlab01","status":"0","interfaces":[{"interfaceid":"1","ip":"127.0.0.1","dns":"","useip":"1","port":"10050","available":"1","error":""}]}
「Zabbix server」ホストのIDは新規インストールでは10084でしたが、これも自分の環境の host.get の応答で確かめてから host.update に入れてください。名前を変えると、agent-lab01の ZBX_HOSTNAME(例では nagoyadevlab01)と一致するので、後でこのホストにアクティブチェックのアイテムを足しても「host not found」になりません。
自分のアイテムとトリガーを作る
テンプレート「Linux by Zabbix agent」にもディスカバリ経由でファイルシステムの容量を見るアイテムとトリガーが含まれますが、初めてのうちは、しきい値と間隔を自分で決めた1組を作るほうが動きを追いやすいです。アイテムキーは vfs.fs.size[/srv/data,pfree] で、pfree は空き容量の百分率です(他に total、free、used、pused が選べます)。取得間隔 delay は1分にしました。既定でよく使われる間隔より短いのは、この記事で「障害からメールまでの時間」を測るためで、本番で数百台に1分間隔を配るとDBの書き込みが増えるので、テンプレート側の間隔に揃えるのが普通です。
画面では「Data collection → Hosts → 登録したホスト(例では nagoyadevlab02)の行の Items → Create item」(項目名は執筆時点)で、Name、Type「Zabbix agent」、Key、Type of information「Numeric (float)」、Units %、Update interval 1m を入れます。APIの item.create では type: 0 がZabbix agent(パッシブ)、value_type: 0 が浮動小数で、パッシブのアイテムには interfaceid が必須です。
{"jsonrpc":"2.0","method":"item.create","params":{
"hostid":"10683","interfaceid":"31",
"name":"/srv/data の空き容量 (%)",
"key_":"vfs.fs.size[/srv/data,pfree]",
"type":0,"value_type":0,"delay":"1m","units":"%"
},"id":1}
{"jsonrpc":"2.0","method":"item.get","params":{"output":["itemid","name","key_","lastvalue","lastclock","units"],"hostids":"10683","filter":{"key_":"vfs.fs.size[/srv/data,pfree]"}},"id":1}
./zbx.sh item-create.json
{"jsonrpc":"2.0","result":{"itemids":["50862"]},"id":1}
./zbx.sh item-get.json | jq -c ".result[]"
{"itemid":"50862","name":"/srv/data の空き容量 (%)","key_":"vfs.fs.size[/srv/data,pfree]","units":"%","lastclock":"1790493642","lastvalue":"100"}
lastclock が0でなくなり、lastvalue が100(空のtmpfsなので空き100)になれば値が届いています。画面なら「Monitoring → Latest data」でホストを絞ると同じ値が見えます。次にトリガーです。式は last(/ホスト名/キー)<20 の形で、last() は最新値を返す履歴関数、深刻度 priority: 2 はWarningです(0 Not classified、1 Information、2 Warning、3 Average、4 High、5 Disaster)。画面では「Data collection → Hosts → Triggers → Create trigger」で、Expression の「Add」ボタンからアイテムと関数を選ぶと同じ式が組み立てられます。
{"jsonrpc":"2.0","method":"trigger.create","params":{
"description":"nagoyadevlab02: /srv/data の空き容量が20%未満",
"expression":"last(/nagoyadevlab02/vfs.fs.size[/srv/data,pfree])<20",
"priority":2
},"id":1}
./zbx.sh trigger-create.json
{"jsonrpc":"2.0","result":{"triggerids":["25286"]},"id":1}
本番でこの式を使うなら、last() ではなく min(/host/key,5m)<20 のように一定期間の最小値で判定して、一時的なファイル生成で通知が飛ぶのを防ぎます。今回は「値が入った次の評価で必ず反応する」ほうが計測に向くので last() にしました。
メール通知:Mailpit・メディアタイプ・ユーザーメディア・アクション
通知は「メディアタイプ(送り方)→ ユーザーのメディア(宛先)→ アクション(いつ誰に送るか)」の3段で成り立ち、どれか1つ欠けてもメールは出ません。初期データのEmailメディアタイプは、SMTPサーバーが mail.example.com のまま、しかも無効(status: 1)です。まずここを確かめます。
{"jsonrpc":"2.0","method":"mediatype.get","params":{"output":["mediatypeid","name","type","status","smtp_server","smtp_port","smtp_email","smtp_security","smtp_authentication","message_format"],"filter":{"name":["Email"]}},"id":1}
./zbx.sh mediatype-get.json | jq -c ".result[]"
{"mediatypeid":"1","name":"Email","type":"0","status":"1","smtp_server":"mail.example.com","smtp_port":"25","smtp_email":"[email protected]","smtp_security":"0","smtp_authentication":"0","message_format":"0"}
画面では「Alerts → Media types → Email」を開き、SMTP server に mailpit、SMTP server port に 1025、Email に送信元アドレス、Connection security「None」、Authentication「None」、Message format「Plain text」にして、一覧で「Enabled」に切り替えます。APIの mediatype.update は同じ項目名で、status: 0 が有効化です。Mailpitは同じComposeネットワークにいるので、宛先はサービス名で届きます。
{"jsonrpc":"2.0","method":"mediatype.update","params":{
"mediatypeid":"1",
"smtp_server":"mailpit","smtp_port":"1025",
"smtp_email":"[email protected]",
"smtp_security":0,"smtp_authentication":0,
"message_format":0,
"status":0
},"id":1}
{"jsonrpc":"2.0","method":"mediatype.get","params":{"output":["name"],"mediatypeids":"1","selectMessageTemplates":["eventsource","recovery","subject"]},"id":1}
./zbx.sh mediatype-update.json
{"jsonrpc":"2.0","result":{"mediatypeids":["1"]},"id":1}
./zbx.sh mediatype-templates.json | jq -c ".result[0].message_templates[]"
{"eventsource":"0","recovery":"0","subject":"Problem: {EVENT.NAME}"}
{"eventsource":"0","recovery":"1","subject":"Resolved in {EVENT.DURATION}: {EVENT.NAME}"}
Emailメディアタイプには件名と本文のメッセージテンプレートが最初から入っていて、アクション側で「Default message」を選ぶとこれが使われます。件名の「Problem: 〜」「Resolved in 〜」は後でMailpitに届く実物と一致します。次に、作成した管理ユーザーに宛先を付けます。画面では「Users → Users → 対象のユーザー(例ではryoma) → Media タブ → Add」で Type「Email」、Send to にアドレスを入れます。APIでは user.update の medias で、severity: 63 が全深刻度(1、2、4、8、16、32の合計)、period が通知を許す曜日と時間帯、active: 0 が有効です。
{"jsonrpc":"2.0","method":"user.update","params":{
"userid":"3",
"medias":[{"mediatypeid":"1","sendto":["[email protected]"],"active":0,"severity":63,"period":"1-7,00:00-24:00"}]
},"id":1}
最後にアクションです。画面では「Alerts → Actions → Trigger actions → Create action」で、Conditions に「Trigger severity is greater than or equals Warning」を足し、Operations タブの Operations で「Send message to users」に通知先のユーザー(例ではryoma)、「Send only to: Email」を選び、Recovery operations にも同じ操作を足します。APIの action.create では eventsource: 0 がトリガーイベント、conditiontype: 4 がトリガーの深刻度、operator: 5 が「以上」、operationtype: 0 がメッセージ送信です。esc_period はエスカレーションの1段の長さで、60秒以上を指定します。
{"jsonrpc":"2.0","method":"action.create","params":{
"name":"障害を ryoma にメールで通知",
"eventsource":0,"status":0,"esc_period":"1h",
"filter":{"evaltype":0,"conditions":[{"conditiontype":4,"operator":5,"value":"2"}]},
"operations":[{"operationtype":0,"esc_step_from":1,"esc_step_to":1,
"opmessage":{"default_msg":1,"mediatypeid":"1"},
"opmessage_usr":[{"userid":"3"}]}],
"recovery_operations":[{"operationtype":0,
"opmessage":{"default_msg":1,"mediatypeid":"1"},
"opmessage_usr":[{"userid":"3"}]}]
},"id":1}
./zbx.sh user-media.json
{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}
./zbx.sh action-create.json
{"jsonrpc":"2.0","result":{"actionids":["7"]},"id":1}
curl -s http://127.0.0.1:8025/api/v1/messages | jq .total
0
curl -s http://127.0.0.1:8025/api/v1/info | jq -r .Version
v1.31.2
Mailpitのバージョンは執筆時点で v1.31.2 でした。compose.yaml の v1.31 は浮動タグなので、取得した時期によってパッチ番号は変わります。Mailpitの受信箱はまだ0通です。アクションは宛先と条件を自分で決めた1本だけにして、初期データに入っているアクションには触りませんでした。
「アクションを作ったのにメールが来ない」の原因は、経験上ほぼこの3つです。Emailメディアタイプが無効のまま(初期状態)、ユーザーにメディアが無い、アクションの条件の深刻度がトリガーより高い。画面の「Reports → Action log」(執筆時点のメニュー名)に送信結果と失敗理由が残るので、Mailpitを見る前にそこを開いてください。
障害を起こして、メールが届くまでを計測する
2台目(nagoyadevlab02)の /srv/data(64MBのtmpfs)に56MiBのファイルを書くと、空きは12.5%になり、トリガーの「20%未満」を満たします。時刻を前後に打ち、Mailpitの受信数が1になるまで1秒ごとに見張るコマンドで、書き込みからメール到着までの時間を測りました。
date +%T; docker compose exec agent-lab02 dd if=/dev/zero of=/srv/data/fill bs=1M count=56; until [ "$(curl -s http://127.0.0.1:8025/api/v1/messages | jq .total)" -ge 1 ]; do sleep 1; done; date +%T
07:20:59
56+0 records in
56+0 records out
58720256 bytes (59 MB, 56 MiB) copied, 0.0328698 s, 1.8 GB/s
07:21:42
docker compose exec agent-lab02 df -h /srv/data
Filesystem Size Used Avail Use% Mounted on
tmpfs 64M 56M 8.0M 88% /srv/data
書き込みが07:20:59、メール到着が07:21:42で、43秒でした。内訳は「次の1分間隔の取得が来るまでの待ち」がほとんどで、値が届いてからトリガー評価、アクション、SMTP送信までは同じ秒に収まっています。Zabbix側の記録で確かめると、問題イベントとアラートの clock は同じ値です。
{"jsonrpc":"2.0","method":"problem.get","params":{"output":["eventid","clock","name","severity"],"hostids":"10683"},"id":1}
{"jsonrpc":"2.0","method":"alert.get","params":{"output":["alertid","clock","sendto","subject","status","error"],"sortfield":"alertid"},"id":1}
./zbx.sh item-get.json | jq -c ".result[]"
{"itemid":"50862","name":"/srv/data の空き容量 (%)","key_":"vfs.fs.size[/srv/data,pfree]","units":"%","lastclock":"1790493702","lastvalue":"12.5"}
./zbx.sh problem-get.json | jq -c ".result[]"
{"eventid":"39","clock":"1790493702","name":"nagoyadevlab02: /srv/data の空き容量が20%未満","severity":"2"}
./zbx.sh alert-get.json | jq -c ".result[]"
{"alertid":"1","clock":"1790493702","sendto":"[email protected]","subject":"Problem: nagoyadevlab02: /srv/data の空き容量が20%未満","status":"0","error":""}
画面では「Monitoring → Problems」にこの行が出て、Actions列の印が送信状況を示します。届いた側の証拠はMailpitのAPIで取ります。/api/v1/messages が一覧、/api/v1/message/{ID} が本文で、Web画面(http://127.0.0.1:8025/)でも同じものが読めます。
curl -s http://127.0.0.1:8025/api/v1/messages | jq '.messages[] | {From: .From.Address, To: .To[0].Address, Subject, Created}'
{
"From": "[email protected]",
"To": "[email protected]",
"Subject": "Problem: nagoyadevlab02: /srv/data の空き容量が20%未満",
"Created": "2026-09-27T07:21:42.616Z"
}
ID=$(curl -s http://127.0.0.1:8025/api/v1/messages | jq -r ".messages[0].ID"); curl -s http://127.0.0.1:8025/api/v1/message/$ID | jq -r .Text
Problem started at 16:21:42 on 2026.09.27
Problem name: nagoyadevlab02: /srv/data の空き容量が20%未満
Host: nagoyadevlab02
Severity: Warning
Operational data: 12.5 %
Original problem ID: 39
本文の時刻が16:21:42なのは、serverコンテナに TZ: Asia/Tokyo を入れたからで、Mailpitの Created はUTCです。次に復旧です。ファイルを消して、受信数が2になるまで待ちます。
date +%T; docker compose exec agent-lab02 rm /srv/data/fill; until [ "$(curl -s http://127.0.0.1:8025/api/v1/messages | jq .total)" -ge 2 ]; do sleep 1; done; date +%T
07:21:43
07:22:43
curl -s http://127.0.0.1:8025/api/v1/messages | jq '.messages[] | {Subject, Created}'
{
"Subject": "Resolved in 1m 0s: nagoyadevlab02: /srv/data の空き容量が20%未満",
"Created": "2026-09-27T07:22:42.586Z"
}
{
"Subject": "Problem: nagoyadevlab02: /srv/data の空き容量が20%未満",
"Created": "2026-09-27T07:21:42.616Z"
}
./zbx.sh problem-get.json | jq -c ".result"
[]
./zbx.sh alert-get.json | jq -c ".result[]"
{"alertid":"1","clock":"1790493702","sendto":"[email protected]","subject":"Problem: nagoyadevlab02: /srv/data の空き容量が20%未満","status":"1","error":""}
{"alertid":"2","clock":"1790493762","sendto":"[email protected]","subject":"Resolved in 1m 0s: nagoyadevlab02: /srv/data の空き容量が20%未満","status":"1","error":""}
削除から復旧メールまでは60秒で、これは次の取得がちょうど1分後に来たからです。alert.get の status が1になっていれば「送信済み」で、SMTPに拒否されたときはここが2(失敗)になり error に理由が入ります。問題が消えた後の problem.get は空の配列です。ここまでで、ホスト登録・アイテム・トリガー・アクション・メールの一連が、記事の環境で一度も手で触らずに通ったことになります。
メモリとDBサイズの実測
起動直後と、ホスト2台を18分監視した後の docker stats です。起動直後はserverが47.63MiB、フロントエンドが38.57MiB、PostgreSQLが152.6MiBで、DBのサイズは初期データ投入直後で64MBでした(この時点ではエージェント2つが再起動ループ中だったので表には0Bで出ています。理由はつまずきポイントで扱います)。
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}'
NAME MEM USAGE / LIMIT CPU %
zabbix-server-monitoring-docker-agent-lab02-1 9.312MiB / 7.75GiB 0.36%
zabbix-server-monitoring-docker-agent-lab01-1 9.832MiB / 7.75GiB 0.05%
zabbix-server-monitoring-docker-zabbix-server-1 53.41MiB / 7.75GiB 1.33%
zabbix-server-monitoring-docker-zabbix-web-1 57.84MiB / 7.75GiB 0.00%
zabbix-server-monitoring-docker-postgres-1 161.7MiB / 7.75GiB 1.65%
zabbix-server-monitoring-docker-mailpit-1 10.2MiB / 7.75GiB 0.00%
docker compose exec postgres psql -U zabbix -d zabbix -c "SELECT pg_size_pretty(pg_database_size('zabbix'));"
pg_size_pretty
----------------
66 MB
(1 row)
6コンテナの合計は約300MiBで、ホスト2台分なら小さなVM1台で足ります。zabbix-serverの数字が小さいのは、設定キャッシュ(CacheSize)がserver本体の既定値32Mのままだからです。公式イメージのREADMEには ZBX_CACHESIZE=8M と書かれていますが、環境変数を渡さない限りコンテナは何も書き込まず、下の設定ファイルのとおり32Mで動いています。ホストが数百台になるとキャッシュ不足のエラーがログに出るので、そのときに ZBX_CACHESIZE で増やします。この環境ではDBが18分で64MBから66MBに増えました(LLDが作るアイテム数はホストごとに違うので、この値は目安です)。増分のほとんどは履歴(history)とトレンドで、1分間隔のアイテムを増やすほど伸びます。保持期間は「Administration → Housekeeping」(執筆時点のメニュー名)で決め、ボリューム pgdata の置き場所のディスクを見張る対象に入れておくのが、監視サーバー自身が止まらないための最低限です。
docker compose exec zabbix-server grep -n "CacheSize=" /etc/zabbix/zabbix_server.conf
417:# VMwareCacheSize=8M
485:# CacheSize=32M
510:# HistoryCacheSize=16M
519:# HistoryIndexCacheSize=4M
528:# TrendCacheSize=4M
537:# TrendFunctionCacheSize=4M
547:# ValueCacheSize=8Mつまずきポイント
エージェントのコンテナがRestartingを繰り返す
最初に書いたcompose.yamlでは、イメージのREADMEにある ZBX_ACTIVESERVERS も律儀に zabbix-server:10051 と書いていました。すると docker compose ps でエージェント2つだけが「Restarting」を繰り返します。
docker compose ps
zabbix-server-monitoring-docker-agent-lab01-1 zabbix/zabbix-agent2:alpine-7.0-latest "/usr/bin/docker-ent…" agent-lab01 45 seconds ago Restarting (1) 3 seconds ago
zabbix-server-monitoring-docker-agent-lab02-1 zabbix/zabbix-agent2:alpine-7.0-latest "/usr/bin/docker-ent…" agent-lab02 45 seconds ago Restarting (1) 14 seconds ago
docker compose logs agent-lab02 | grep -m1 "Cannot validate"
agent-lab02-1 | zabbix_agent2 [1]: ERROR: Cannot validate configuration: failed to validate "ServerActive" configuration parameter: address "zabbix-server:10051" specified more than once.
イメージは ZBX_SERVER_HOST の値をアクティブチェックの宛先にも使い、ZBX_ACTIVESERVERS はそれに追加する扱いなので、同じ宛先が2回並んで ServerActive=zabbix-server,zabbix-server:10051 になり、agent2が設定検証で止まります。直し方は ZBX_ACTIVESERVERS の行を消して docker compose up -d です(変更のあったサービスだけ作り直されます)。ZBX_ACTIVESERVERS を書くのは、パッシブの許可元とアクティブの宛先が別(プロキシ経由など)のときだけです。
画面の下に「Zabbix server is not running」が出る
ログインはできるのに、画面下部に赤く「Zabbix server is not running: the information displayed may not be current.」と出る状態です。原因は「フロントエンドが ZBX_SERVER_HOST:10051 に接続できない」の1つだけで、serverコンテナがまだ起動中か落ちている、あるいは ZBX_SERVER_HOST がサービス名と違う、のどちらかです。この文言はページのHTMLには含まれず、ブラウザ側のJavaScriptが10秒ごとに jsrpc.php へ問い合わせて表示しています。だからcurlで再現するときは、そのRPCを直接叩きます。
{"jsonrpc":"2.0","method":"zabbix.status","params":{"nocache":true},"id":1}
docker compose exec zabbix-web grep -n "delay:" /usr/share/zabbix/js/servercheck.js
27: delay: 10000, // 10 seconds
curl -s -c cj -o /dev/null -w '%{http_code}\n' --data-urlencode 'name=ryoma' --data-urlencode 'password=Lab01-Sakura-2026!' --data-urlencode 'enter=Sign in' http://127.0.0.1:8080/index.php
302
docker compose stop zabbix-server
Container zabbix-server-monitoring-docker-zabbix-server-1 Stopping
Container zabbix-server-monitoring-docker-zabbix-server-1 Stopped
curl -s -b cj -H 'Content-Type: application/json' -d @status-check.json 'http://127.0.0.1:8080/jsrpc.php?output=json-rpc'
{"jsonrpc":"2.0","result":{"result":false,"message":"Zabbix server is not running: the information displayed may not be current."},"id":1}
docker compose up -d
Container zabbix-server-monitoring-docker-zabbix-server-1 Starting
Container zabbix-server-monitoring-docker-zabbix-server-1 Started
docker compose up -d --force-recreate agent-lab01
Container zabbix-server-monitoring-docker-agent-lab01-1 Recreate
Container zabbix-server-monitoring-docker-agent-lab01-1 Recreated
Container zabbix-server-monitoring-docker-agent-lab01-1 Started
sleep 15
curl -s -b cj -H 'Content-Type: application/json' -d @status-check.json 'http://127.0.0.1:8080/jsrpc.php?output=json-rpc'
{"jsonrpc":"2.0","result":{"result":true,"message":""},"id":1}1つ目のcurlは画面のログインフォームに管理ユーザー(例ではryoma)でPOSTしてセッションCookieを cj に保存する操作で、302が返れば成功です。止めた直後は result: false と文言が返ります。docker compose up -d で戻したら、続けて docker compose up -d --force-recreate agent-lab01 でサイドカーも作り直します。serverコンテナは止めて起動し直しただけでもネットワーク名前空間が新しくなり、古い名前空間を共有していたagent-lab01には lo しか残らないので、作り直しを忘れると次の取得でserver自身のホスト(例では nagoyadevlab01)が「Cannot establish TCP connection to [[127.0.0.1]:10050]: [111] Connection refused」の到達不能になり、そのまま放っておくとテンプレートのトリガー「Zabbix agent is not available」がメールを送ってきます。画面側のserver確認は起動直後はまだ false で、10秒ほど待つと true に変わります。ZBX_SERVER_HOST の綴りを間違えた場合も応答は同じなので、まず docker compose ps でserverが「Up」か、次にcompose.yamlの zabbix-web の ZBX_SERVER_HOST がサービス名と一致しているか、の順で見ます。
エージェントのログに「rejected, allowed hosts」か「host not found」
サイドカーのagent-lab01に書いた ZBX_SERVER_HOST: 127.0.0.1 をagent-lab02にそのままコピーすると、serverからの接続はコンテナのIP(この環境では172.18.0.5)から来るので、エージェントが許可元と一致しないとして拒否します。server側から見ると、接続した瞬間に切られるので「Connection reset by peer」になり、ホストは到達不能(available: 2)です。
agent-lab02:
image: zabbix/zabbix-agent2:alpine-7.0-latest
hostname: nagoyadevlab02
environment:
ZBX_HOSTNAME: nagoyadevlab02
ZBX_SERVER_HOST: 127.0.0.1
docker compose up -d agent-lab02
docker compose logs --no-log-prefix agent-lab02 | grep -m1 rejected
2026/09/27 07:35:02.521564 connection from "172.18.0.5" rejected, allowed hosts: "127.0.0.1"
./zbx.sh host-get-lab02.json | jq -c ".result[0].interfaces[0]"
{"interfaceid":"31","dns":"nagoyadevlab02","useip":"0","port":"10050","available":"2","error":"Get value from agent failed: cannot read response: cannot read from socket: [104] Connection reset by peer"}
実機では、Server= に書くのは「serverが接続してくる送信元IP」です。serverがNATやプロキシの向こうにいると、思っているIPと違う送信元になるので、まさにこの「rejected」に出ている送信元をそのまま Server= に書けば通ります。もう1つは Hostname= のずれです。たとえばエージェント側だけをFQDN(検証では nagoyadevlab02.local)にした状態では、server側に登録した短いホスト名(nagoyadevlab02)と一致せず、起動直後のログに「host not found」が出ます。
docker compose logs --no-log-prefix agent-lab02 | grep -m2 -E "hostname:|not found"
2026/09/27 07:26:12.141740 Zabbix Agent2 hostname: [nagoyadevlab02.local]
2026/09/27 07:26:14.004272 [101] no active checks on server [zabbix-server:10051]: host [nagoyadevlab02.local] not found
この状態でもパッシブチェックは動くので、テンプレートの値は取れているのに「Zabbix agent (active)」型のアイテムだけ値が来ない、という中途半端な症状になります。直し方はどちらか一方に合わせるだけで、エージェント側の ZBX_HOSTNAME(実機なら Hostname=)を直して docker compose up -d agent-lab02、または画面の Host name を変えます。復旧後は次の取得で available が1に戻ります。
ZBXの印が赤く「Cannot establish TCP connection」
ポートを間違えた(10050と10051を取り違えた)、ファイアウォールで10050が閉じている、エージェントが起動していない、のどれでも同じ赤い印になります。インターフェースのポートを10051に変えて再現しました。
{"jsonrpc":"2.0","method":"hostinterface.update","params":{"interfaceid":"31","port":"10051"},"id":1}
./zbx.sh interface-port-wrong.json
{"jsonrpc":"2.0","result":{"interfaceids":["31"]},"id":1}
./zbx.sh host-get-lab02.json | jq -c ".result[0].interfaces[0]"
{"interfaceid":"31","dns":"nagoyadevlab02","useip":"0","port":"10051","available":"2","error":"Get value from agent failed: Cannot establish TCP connection to [[nagoyadevlab02]:10051]: [111] Connection refused"}
エラー文の末尾で切り分けます。「[111] Connection refused」は相手に届いたが誰も待ち受けていない(ポート違い、エージェント停止)、タイムアウト系ならファイアウォールか経路、「cannot resolve」ならDNS名の間違いです。実機にファイアウォールを入れているなら、serverのIPからの10050番だけを許可します。ポートを10050に戻すと、次の取得で緑に戻りました。
動作確認
一連の設定が終わった状態で、6つのコンテナが全部「Up」、2台のホストが到達可、Mailpitに2通、フロントエンドのserver確認が true であれば完成です。ログインしたダッシュボードのタイトルには ZBX_SERVER_NAME の値が出ます。
docker compose ps --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
agent-lab01 Up 20 seconds
agent-lab02 Up 5 minutes
mailpit Up 5 minutes (healthy)
postgres Up 5 minutes (healthy)
zabbix-server Up 21 seconds
zabbix-web Up 5 minutes (healthy)
./zbx.sh host-get-server.json | jq -c ".result[] | {host, available: .interfaces[0].available, error: .interfaces[0].error}"
{"host":"nagoyadevlab02","available":"1","error":""}
{"host":"nagoyadevlab01","available":"1","error":""}
curl -s http://127.0.0.1:8025/api/v1/messages | jq .total
2
curl -s -b cj 'http://127.0.0.1:8080/zabbix.php?action=dashboard.view' | grep -o '<title>[^<]*</title>'
<title>nagoyadevlab01: Dashboard</title>ブラウザでは「Monitoring → Hosts」で両方のホストのZBXの印が緑、「Monitoring → Problems」が空(もう一度 dd を実行すれば1分以内にWarningの行が現れます)、「Reports → Action log」に2件の送信済みが並んでいれば、画面側も同じ状態です。
後片付け
検証を終えて消すときは、ボリュームごと落としてイメージも消します。-v を付けないとDBのボリューム pgdata が残り、次に up したときに古い設定とAdminのパスワードがそのまま復活します。
docker compose down -v
Container zabbix-server-monitoring-docker-postgres-1 Removed
Network zabbix-server-monitoring-docker_default Removed
Volume zabbix-server-monitoring-docker_pgdata Removed
docker rmi zabbix/zabbix-server-pgsql:alpine-7.0-latest zabbix/zabbix-web-nginx-pgsql:alpine-7.0-latest zabbix/zabbix-agent2:alpine-7.0-latest axllent/mailpit:v1.31 postgres:16-alpine
よくある質問
Zabbixの構築費用はいくらですか?
ソフトウェア自体はAGPLv3で公開されていて、ライセンス費用はかかりません。実際の費用は、動かすサーバー(この記事の構成ならメモリ約300MiBで動く小さなVM1台)と、テンプレートやアクションを設計する人の工数です。開発元や国内の事業者が有償サポートや構築サービスを提供しており、料金は公式サイトと各社の案内で確認してください。
WindowsでZabbixサーバを構築するにはどうしたらいいですか?
Zabbix server自体はWindowsに対応しておらず、執筆時点の要件表でもWindows列のserverは「−」です。Windowsで動くのはエージェントだけなので、serverはLinuxのVMかこの記事のようなDockerで建て、Windowsマシンにはagent 2を入れてホスト登録する、という分担になります。WindowsのDocker Desktop上でこの記事のcompose.yamlを試す手もありますが(この記事では検証していません)、常時稼働の監視サーバーには向きません。
Zabbixの欠点は何ですか?
アイテム・トリガー・アクション・メディアの4段を理解するまで、通知が出ない理由が追いにくいこと、履歴でDBが太るので保持期間とディスクの計画が要ること、画面の項目が多く初めての人には威圧的なことです。エージェントの Server= と Hostname= の照合も、この記事で見せたとおり最初の壁になります。裏返すと、覚えてしまえば通知の振り分けやSNMP機器の監視を追加設定なしでこなせるのが利点です。
Zabbixとは何ですか?
サーバー、仮想マシン、ネットワーク機器、クラウドサービスの状態を1か所に集めて、条件を満たしたら通知する統合監視のソフトウェアです。監視対象にエージェントを入れる方式とSNMPやHTTPで外から見る方式の両方を持ち、設定はすべてDBに保存され、画面とJSON-RPC APIから同じように操作できます。この記事のように、2台のホストと1本の通知経路から始めて、テンプレートで対象を増やしていく使い方が典型です。
まとめ
公式イメージの7.0 LTSを6サービスのcompose.yamlで建て、2台目のagent2をホスト登録し、空き容量のアイテムとトリガーで本物の障害を起こして、43秒後にMailpitへ届いたメールまでを一続きに確かめました。合計メモリは約300MiB、DBは18分で約66MB(この環境の値)です。設定は画面でもAPIでも同じ結果になるので、JSONファイルを残しておけば次の環境でも同じ手順を再生できます。次の一歩は、Mailpitを社内のSMTPリレーに置き換え、本当に見張りたい2台目のサーバーにagent 2を入れて、この記事の host-create.json のDNS名だけ書き換えて登録することです。
次に読む
- PrometheusとGrafanaで自宅サーバーを監視する — exporter方式の監視を同じ手順の深さで試し、判断表を自分の環境で埋める
- Docker Compose入門 — この記事で省いた
.env、depends_on、ボリュームの基礎 - Linuxサーバーの基本ハードニング — 実機にエージェントを入れるときの、10050番だけを開けるファイアウォール設定