はじめに
前回はUptime KumaをDocker Composeで構築し、Flaskアプリを監視しました。
その中で、
- Docker Network
- External Network
- Volume
- Restart Policy
- Raspberry Pi再起動後のContainer復旧
などを確認しました。
今回はさらに一歩進めて、
Dockerをどう運用するか
をテーマにします。
具体的には、
- Healthcheck
-
depends_onとservice_healthy - Restart Policy
- Imageの更新
- Containerの再作成
- Volumeのバックアップ
- Volume削除後の復元
を実際に試します。
DockerではContainerがUpになっていても、アプリケーションが正常とは限りません。
また、VolumeがあるからといってBackupが不要になるわけでもありません。
今回はその違いを実験しながら確認していきます。
今回の構成
今回も、これまで作ってきたFlask + Redis環境を使います。
Webブラウザ
|
| :8082
v
+---------+
| Flask |
+---------+
|
| redis:6379
v
+---------+
| Redis |
+---------+
|
v
Named Volume
redis-data
FlaskへアクセスするたびにRedisのhitsが増える構成です。
ContainerがUpなら正常なのか?
まず現在の状態を確認します。
sudo docker compose ps
Containerが正常に起動していれば、
Up
と表示されます。
しかし、ここで疑問が出ます。
ContainerがUpなら、中のアプリケーションも正常なのか?
答えは必ずしもそうではありません。
ContainerのMain Processが動いていれば、Dockerから見るとContainerはrunningです。
しかし、
Flask Processは動いている
↓
Redisへ接続できない
↓
Webアプリとしては正常に動けない
という状態もあり得ます。
そこで使うのがHealthcheckです。
まず既存のHealthcheckを確認する
Uptime Kumaは、Image側ですでにHealthcheckが設定されていました。
確認します。
sudo docker inspect uptime-kuma
Healthcheck部分だけを見る場合は、
sudo docker inspect \
-f '{{json .Config.Healthcheck}}' \
uptime-kuma
と確認できます。
一方、FlaskとRedisには最初Healthcheckがありませんでした。
sudo docker inspect \
-f '{{json .Config.Healthcheck}}' \
docker-lab-web-1
結果は、
null
でした。
Redisも同様です。
つまり、
Containerが起動しているか
は分かっても、
中のサービスが正常に応答しているか
までは確認していない状態です。
FlaskへHealthcheckを追加する
Flaskのcompose.yamlへHealthcheckを追加します。
services:
web:
build: .
ports:
- "${WEB_PORT}:5000"
depends_on:
- redis
restart: unless-stopped
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
今回はPython標準ライブラリのurllib.requestを使って、
http://localhost:5000/
へアクセスしています。
つまりDocker自身が定期的に、
FlaskはHTTPで応答しているか?
を確認します。
Healthcheckの各設定
今回設定した内容を見てみます。
interval: 30s
30秒ごとにHealthcheckを実行します。
timeout: 5s
5秒以内に結果が返らなければ失敗として扱います。
retries: 3
3回連続で失敗するとunhealthyになります。
start_period: 10s
Container起動直後はアプリケーションの準備に時間がかかる可能性があるため、10秒間の猶予を与えています。
Flaskがhealthyになった
設定を反映します。
sudo docker compose up -d
状態を確認します。
sudo docker compose ps
するとFlaskに、
healthy
が表示されました。
これで、
running
だけではなく、
HTTPで正常に応答できる
ところまでDockerが確認しています。
Healthcheckの詳細を見る
Healthcheckの結果はdocker inspectでも確認できます。
sudo docker inspect \
-f '{{json .State.Health}}' \
docker-lab-web-1
ここでは、
- Status
- FailingStreak
- 各Healthcheckの実行結果
- ExitCode
などを確認できます。
正常なら、
Status: healthy
FailingStreak: 0
ExitCode: 0
のような状態になります。
Redisを止めてみる
Healthcheckが本当に意味を持っているのか確認します。
Redisだけ停止します。
sudo docker compose stop redis
Flask Container自体はそのまま動いています。
再び確認します。
sudo docker compose ps
しばらく待つとFlaskが、
Up
のまま、
unhealthy
になりました。
ここが重要です。
Container
→ running
Flask Process
→ running
Redis
→ 停止
FlaskのHTTP応答
→ 正常に処理できない
Healthcheck
→ unhealthy
つまり、
runningとhealthyは別物
ということを実際に確認できました。
unhealthyになったら自動再起動するのか?
ここでもう一つ疑問が出ます。
Flaskには、
restart: unless-stopped
を設定しています。
それならunhealthyになったとき、自動で再起動するのでしょうか?
今回の実験では、FlaskはunhealthyになってもContainer自体は動き続けました。
つまり、
restart policy
と、
healthcheck
は役割が違います。
Restart Policyは、主にContainerのMain Processが終了した場合などに再起動を行います。
Healthcheckは、
このContainer内のサービスは正常か?
という状態をDockerへ知らせる仕組みです。
そのため、
unhealthy
=
自動的にContainerを再起動
ではありません。
Redisを再起動する
Redisを再び起動します。
sudo docker compose start redis
Redisが戻るとFlaskも再び正常に応答できるようになり、Healthcheckも、
healthy
へ戻りました。
Uptime Kuma側の監視も復旧しました。
RedisにもHealthcheckを追加する
次はRedis自身にもHealthcheckを追加します。
Redisには、
redis-cli ping
という確認方法があります。
Redisが正常なら、
PONG
と返ります。
compose.yamlへ追加します。
redis:
image: redis:latest
restart: unless-stopped
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 3s
retries: 5
volumes:
- redis-data:/data
これでDockerがRedisへ定期的にPINGを送り、応答を確認します。
depends_onだけでは準備完了を待たない
これまではFlask側で、
depends_on:
- redis
としていました。
これはRedisより後にWeb Containerを起動するための依存関係として使えます。
しかし、
Redis Containerが起動した
ことと、
Redisが接続を受け付けられる状態になった
ことは同じではありません。
そこでservice_healthyを使います。
service_healthyを使う
Flask側の設定を変更します。
depends_on:
redis:
condition: service_healthy
Redis側には先ほどHealthcheckを追加しています。
最終的には次のようになります。
services:
web:
build: .
ports:
- "${WEB_PORT}:5000"
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
redis:
image: redis:latest
restart: unless-stopped
healthcheck:
test:
- CMD
- redis-cli
- ping
interval: 5s
timeout: 3s
retries: 5
volumes:
- redis-data:/data
volumes:
redis-data:
起動順を確認する
一度停止します。
sudo docker compose down
再び起動します。
sudo docker compose up -d
すると、
Redis起動
↓
Redis Healthcheck
↓
healthy
↓
Flask起動
↓
Flask Healthcheck
↓
healthy
という流れになりました。
docker compose psでも、Redisが先にhealthyになり、その後Flaskが起動したことを確認できました。
service_healthyの注意点
ここで注意したいのは、
condition: service_healthy
がすべての起動場面を完全に制御してくれるわけではないという点です。
これは主に、
docker compose up
でComposeがサービスを起動するときの依存関係です。
一方、Raspberry Pi再起動後にDocker daemonが、
restart: unless-stopped
によってContainerを再起動する動作は、Composeがupを実行しているのと同じではありません。
つまり、
Composeによる起動制御
と、
Docker daemonによるRestart Policy
は分けて考えた方が分かりやすそうです。
Restart Policyを実機で確認する
Flask、Redis、Uptime Kumaには、
restart: unless-stopped
を設定しています。
そこでRaspberry Pi自体を再起動しました。
sudo reboot
再起動後、SSHで接続して確認します。
sudo docker ps
結果として、
docker-lab-web-1
docker-lab-redis-1
uptime-kuma
が再び起動していました。
つまり、
Raspberry Pi再起動
↓
Docker daemon起動
↓
Restart Policy
↓
Container再起動
という動きを確認できました。
自宅サーバーではかなり重要な機能です。
Docker Imageを更新する
次はImageの更新を試します。
Redisでは、
image: redis:latest
を使っています。
まず現在のImageを確認します。
sudo docker images redis
次にRegistryから最新のImageを確認します。
sudo docker compose pull redis
今回は、すでに同じImageがローカルに存在していたため、実際には新しいImageへの更新は発生しませんでした。
ここで重要なのは、
pullした
=
Containerが自動で新しくなる
ではないことです。
ImageとContainerは別物です。
ImageとContainerは別
Dockerの考え方を整理すると、
Image
↓
Containerを作る
です。
Registryから新しいImageを取得しても、すでに動いているContainerが自動的に新しいImageへ入れ替わるとは限りません。
そのため更新時には、
Image取得
↓
Container再作成
という流れを考える必要があります。
Redis Containerを再作成する
まず現在のContainer IDを確認します。
sudo docker compose ps
または、
sudo docker ps
で確認できます。
その後、Redisだけ強制的に再作成します。
sudo docker compose up -d --force-recreate redis
再びContainer IDを確認すると、RedisのContainer IDが変わっていました。
つまり、
古いRedis Container
↓
削除
↓
新しいRedis Container
になっています。
Containerを作り直してもCounterは残る
Redis Containerは新しくなりました。
しかしブラウザでFlaskへアクセスすると、これまでのCounterは残っていました。
なぜならRedisのデータはContainer内部ではなく、
volumes:
- redis-data:/data
としてNamed Volumeへ保存しているからです。
旧Redis Container
|
v
redis-data Volume
^
|
新Redis Container
Containerが変わっても、同じVolumeをMountすればデータを引き継げます。
Image更新では互換性にも注意する
ただし、
Volumeがある
↓
どんなバージョンへ更新しても安全
という意味ではありません。
Databaseなどでは、バージョンによってデータ形式や互換性が変わることがあります。
そのため実運用では、
- Release Notes
- Upgrade手順
- Databaseの互換性
- Backup
などを確認してから更新する必要があります。
今回の--force-recreateは、Container再作成とVolumeの関係を理解するための実験です。
VolumeがあればBackupはいらない?
ここまでVolumeによってデータが残ることを何度も確認しました。
では、
VolumeがあるならBackupはいらないのでは?
という疑問が出ます。
Volumeは、
Containerを削除してもデータを残す
ための仕組みです。
しかしVolumeそのものを削除すると、当然データはなくなります。
つまり、
Volume
=
永続化
Backup
=
消失したデータを復旧するためのコピー
で、役割が違います。
実際に試してみます。
RedisのBackupを作る
RedisにはRDB形式でデータを保存する仕組みがあります。
まずRedisへ明示的に保存させます。
sudo docker compose exec redis redis-cli SAVE
成功すると、
OK
と表示されます。
Redis Container内部では、
/data/dump.rdb
が作成されています。
確認します。
sudo docker compose exec redis ls -lh /data/dump.rdb
Raspberry Pi側へBackupをコピーする
Backup保存用ディレクトリを作ります。
mkdir -p ~/docker-backup
Container内のdump.rdbをホストへコピーします。
sudo docker cp \
docker-lab-redis-1:/data/dump.rdb \
~/docker-backup/dump.rdb
確認します。
ls -lh ~/docker-backup/dump.rdb
これで、
Redis Volume
とは別に、
~/docker-backup/dump.rdb
というBackupができました。
Volumeを本当に削除する
ここからはデータ削除を伴う実験です。
Backupが作成できていることを確認したうえで、Compose環境とVolumeを削除します。
sudo docker compose down -v
-vを付けるとComposeで管理しているVolumeも削除されます。
これまで何度も使った、
sudo docker compose down
とは意味が違います。
docker compose down
→ ContainerやNetworkを削除
→ Named Volumeは残る
docker compose down -v
→ ContainerやNetworkを削除
→ Named Volumeも削除
となります。
再構築するとCounterがリセットされた
再び起動します。
sudo docker compose up -d
新しいredis-data Volumeが作成されます。
ブラウザからFlaskへアクセスすると、Counterが最初からになりました。
つまり、
Volume削除
↓
Redisデータ消失
↓
新しいVolume作成
↓
Counterリセット
となりました。
これで、
Containerを削除してもVolumeがあれば残る
と、
Volume自体を削除したらデータは消える
の違いを実際に確認できました。
BackupからRedisを復元する
次は先ほど保存した、
~/docker-backup/dump.rdb
から復元します。
Redisを停止します。
sudo docker compose stop redis
現在のRedis ContainerへBackupをコピーします。
sudo docker cp \
~/docker-backup/dump.rdb \
docker-lab-redis-1:/data/dump.rdb
その後Redisを起動します。
sudo docker compose start redis
必要に応じてFlaskも再起動します。
sudo docker compose restart web
ブラウザから確認すると、削除前のCounterへ戻りました。
今回の環境では、Backup前の値である200回台のCounterが復元されました。
つまり、
Volume削除
↓
データ消失
↓
Backupからdump.rdbを戻す
↓
Redis起動
↓
Counter復元
まで実際に確認できました。
VolumeとBackupの違い
今回の実験で、この2つの違いがかなり分かりやすくなりました。
Volume
Container削除
↓
データは残る
用途は、
Containerとデータを分離する
ことです。
Backup
Volume削除
↓
データ消失
↓
Backupから復元
用途は、
元データが失われても復旧できるようにする
ことです。
つまり、
Volume != Backup
です。
同じRaspberry PiへのBackupで十分なのか?
今回は、
~/docker-backup/
へBackupを保存しました。
学習用としてはこれで十分ですが、実運用ではこれだけでは不十分です。
例えばmicroSDカード自体が故障すると、
Docker Volume
も、
~/docker-backup/
も同時に失われる可能性があります。
本当にBackupとして使うなら、
Raspberry Pi
|
+-- Docker Volume
|
+-- ローカルBackup
|
v
さらに別媒体
という構成を考える必要があります。
例えば、
- USBストレージ
- NAS
- 別PC
- クラウドストレージ
など、Raspberry Pi本体とは別の場所へコピーする方が安全です。
DatabaseのBackupはファイルコピーだけでよいのか?
今回Redisでは、
redis-cli SAVE
を実行してからdump.rdbをコピーしました。
これはRedis自身に整合性の取れたRDBファイルを作らせています。
Databaseによっては、動作中のデータファイルをそのままコピーすると整合性が取れない可能性があります。
例えば、
PostgreSQL
→ pg_dump
MySQL
→ mysqldump
など、Database自身が持つBackup機能を使うことが一般的です。
つまり、
Volumeのディレクトリをコピーすれば、どんなDatabaseでも安全にBackupできる
とは限りません。
Databaseごとの正しいBackup方法を使う必要があります。
今回の運用を整理する
ここまでの内容を整理すると、Docker運用にはいくつか違う仕組みがあります。
restart policy
|
+-- Containerが停止したときの再起動
healthcheck
|
+-- Serviceが正常か確認
depends_on + service_healthy
|
+-- Compose起動時に依存Serviceの準備を待つ
docker compose pull
|
+-- 新しいImageを取得
Container再作成
|
+-- 新しいImageや設定を反映
Volume
|
+-- Container削除後もデータを残す
Backup
|
+-- Volumeそのものを失っても復元できるようにする
それぞれ目的が違います。
「動いている」と「正常」は違う
今回特に印象に残ったのが、
running
と、
healthy
の違いです。
Redisを停止したとき、FlaskのContainer自体は動いていました。
しかしFlaskはRedisを必要としているため、アプリケーションとしては正常ではありません。
Container Process
→ running
Service
→ unhealthy
という状態があり得ます。
これはDockerを運用するときにかなり重要な考え方だと思います。
「永続化」と「Backup」も違う
もう一つ重要だったのが、
Volume
と、
Backup
の違いです。
VolumeがあればContainerを自由に作り直せます。
しかし、
docker compose down -v
のようにVolumeそのものを失えばデータは消えます。
そこでBackupが必要になります。
Container
→ Disposable
Volume
→ Persistent
Backup
→ Recoverable
と考えると分かりやすそうです。
今回理解できたこと
今回の実験では、
- Containerが
runningでもServiceが正常とは限らない - HealthcheckでServiceの状態を確認できる
-
unhealthyだけでは自動再起動しない - RedisにもHealthcheckを設定できる
-
depends_onとservice_healthyでCompose起動時の依存関係を制御できる - Restart PolicyでRaspberry Pi再起動後にContainerを復旧できる
- ImageをPullしても既存Containerが自動で置き換わるとは限らない
- Containerを再作成してもVolumeがあればデータは残る
- VolumeとBackupは別物
- BackupからVolumeのデータを復元できる
ことを確認できました。
まとめ
今回はDockerを「構築する」だけではなく、「運用する」という視点で実験しました。
特に重要だったのは、
Containerが起動していることと、サービスが正常であることは別
ということです。
Healthcheckを使うことで、
running
だけでは分からないServiceの状態を確認できました。
また、
VolumeはBackupではない
ということも実際にVolumeを削除して確認しました。
Containerを作り直せるようにするためにVolumeを使い、
Volumeそのものを失ったときに備えてBackupを取る。
この2つは別の役割です。
Dockerでは、
Containerは作り直せる
Imageも再取得できる
設定もComposeから再現できる
データはVolumeへ保存する
Volumeを失ったときはBackupから復旧する
という形に近づけるほど、環境を再構築しやすくなります。
次回はさらにセキュリティの視点を加えて、
.env- Containerへの環境変数
- Docker Compose Secrets
- RedisへのPassword設定
- Port公開
-
127.0.0.1へのBind - Firewall
- DockerのNATと
DOCKER-USER
を実際に確認します。
「設定値と秘密情報はどう分けるのか?」
「ContainerのPortはどこまで公開されているのか?」
をRaspberry Pi上で実験していきます。