はじめに
前回は、.dockerignore、バージョン固定、non-root、Multi-stage buildを使ってDocker Imageを改善しました。
今回は、Containerを実際に運用するときに重要になる次の3つを試します。
- CPU・Memoryの使用量を制限する
- Docker Logが増え続けないようにローテーションする
- Docker Networkをfrontend / backendに分離する
これまで作ってきた環境では、Flask、Redis、Caddy、Uptime Kumaが動いています。
今回はこの環境を、
CPU → Flask Containerは最大0.5 CPU
Memory → Flask Containerは最大128MiB
Log → 1ファイル10MB、最大3ファイル
Network → 必要なContainer同士だけ通信
という構成へ変更します。
今回の構成
最終的な通信経路は次のようにします。
HTTPS
Windows PC ──────────────→ Caddy
│
docker-lab_frontend
│
▼
web
│
docker-lab_backend
│
▼
Redis
ポイントは、Flaskのwebだけがfrontendとbackendの両方に参加することです。
これによって、
Caddy → Flask OK
Flask → Redis OK
Caddy → Redis NG
という通信範囲を作ります。
1. CPUとMemoryを制限する
まずFlask ContainerにCPUとMemoryの制限を設定します。
compose.yamlのwebに次を追加しました。
services:
web:
build: .
cpus: "0.50"
mem_limit: 128m
cpus: "0.50"は、Containerが利用できるCPU時間を約0.5 CPU分に制限します。
mem_limit: 128mは、Memory上限を128MiBに設定します。
設定を確認します。
sudo docker compose config
Composeによって次のように解釈されました。
cpus: 0.5
mem_limit: "134217728"
134217728 byteは128MiBです。
2. Raspberry PiでMemory制限が効かなかった
設定を反映してみます。
sudo docker compose up -d --force-recreate web
すると、次のWarningが出ました。
Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded.
docker inspectでも確認しました。
sudo docker inspect docker-lab-web-1 \
--format 'NanoCpus={{.HostConfig.NanoCpus}} Memory={{.HostConfig.Memory}}'
結果は、
NanoCpus=500000000 Memory=0
でした。
CPU制限は設定されていますが、Memory制限は0です。
つまり、
CPU → 設定成功
Memory → 設定が破棄された
という状態でした。
3. cgroupを調べる
Dockerのリソース制限にはLinux Kernelのcgroupが使われます。
現在利用できるcgroup controllerを確認します。
cat /sys/fs/cgroup/cgroup.controllers
結果は、
cpuset cpu io pids
でした。
memoryがありません。
さらにKernelの起動パラメータを確認します。
cat /proc/cmdline
その中に、
cgroup_disable=memory
が入っていました。
つまり、このRaspberry PiではMemory cgroupが無効になっていました。
4. Memory cgroupを有効化する
Raspberry Pi OSではKernelの起動パラメータを/boot/firmware/cmdline.txtで設定できます。
念のためバックアップを作ります。
sudo cp /boot/firmware/cmdline.txt \
/boot/firmware/cmdline.txt.before-memory-cgroup
cmdline.txtは基本的に1行で記述されるため、その行の末尾へ次を追加しました。
cgroup_memory=1 cgroup_enable=memory
今回は次のコマンドで追加しました。
sudo sed -i '1 s/$/ cgroup_memory=1 cgroup_enable=memory/' \
/boot/firmware/cmdline.txt
その後、再起動します。
sudo reboot
再起動後に確認します。
cat /sys/fs/cgroup/cgroup.controllers
今度は、
cpuset cpu io memory pids
となりました。
memoryが追加されています。
5. Memory制限が有効になった
Flask Containerを再作成します。
cd ~/docker-lab
sudo docker compose up -d --force-recreate web
再び確認します。
sudo docker inspect docker-lab-web-1 \
--format 'NanoCpus={{.HostConfig.NanoCpus}} Memory={{.HostConfig.Memory}}'
結果は、
NanoCpus=500000000 Memory=134217728
となりました。
これで、
CPU 0.5 CPU
Memory 128MiB
の両方が有効になりました。
docker statsでも確認できます。
sudo docker stats --no-stream
Flask Containerは次のようになりました。
docker-lab-web-1 0.02% 30.71MiB / 128MiB 23.99%
以前はMemory欄が0B / 0Bになっていましたが、Memory cgroupを有効化したことで正常に取得できるようになりました。
6. CPU制限を実際に試す
設定値を見るだけではなく、本当にCPUが制限されるか試してみます。
Flask Container内でCPUを使い続けるPythonプログラムを別Processとして起動しました。
sudo docker compose exec -d web \
python -c 'while True: pass'
その状態で、
sudo docker stats --no-stream
を実行します。
結果、Flask ContainerのCPU使用率は、
50.00%
になりました。
cpus: "0.50"で設定した制限付近で頭打ちになっています。
Container内で動いているProcessも確認しました。
sudo docker top docker-lab-web-1
Flask本体に加えて、
python app.py
python -c while True: pass
が動いていました。
実験用Processを停止します。
今回はslim Image内にpkillが入っていなかったため、Host側からPIDを指定して終了しました。
sudo kill <PID>
再びdocker topを確認すると、
python app.py
だけに戻りました。
CPU制限は設定ファイル上だけではなく、実際に機能していることを確認できました。
7. Docker Logはどこに保存されるのか
次はLogです。
Flask ContainerのLogging DriverとLog Pathを確認しました。
sudo docker inspect docker-lab-web-1 \
--format 'Driver={{.HostConfig.LogConfig.Type}} Path={{.LogPath}}'
Logging Driverは、
json-file
でした。
Dockerのjson-file Logging Driverでは、Containerの標準出力・標準エラー出力がHost側のLogファイルとして保存されます。
Logを放置すると、長期間の運用でファイルが大きくなる可能性があります。
そこでLog Rotationを設定します。
8. Log Rotationを設定する
compose.yamlのwebへ次を追加しました。
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
意味は、
max-size: 10m
1つのLogファイルを最大10MB程度にする
max-file: 3
最大3ファイルまで保持する
です。
設定を反映します。
sudo docker compose up -d --force-recreate web
docker inspectで確認します。
sudo docker inspect docker-lab-web-1 \
--format '{{json .HostConfig.LogConfig}}'
結果は、
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
となりました。
9. Log Rotationを実際に発生させる
本番用Flask Containerで10MBのLogを大量発生させる必要はありません。
そこで、実験用Containerを作り、小さい上限でLog Rotationを試しました。
sudo docker run -d \
--name log-test \
--log-driver json-file \
--log-opt max-size=10k \
--log-opt max-file=3 \
busybox \
sh -c 'i=0; while true; do echo "log-test line $i xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; i=$((i+1)); done'
このContainerでは、
1ファイル最大 10KB
最大3ファイル
にしています。
Logディレクトリを確認しました。
sudo ls -lh \
/var/lib/docker/containers/$(sudo docker inspect -f '{{.Id}}' log-test)/
結果は、
9.7K ...-json.log
9.8K ...-json.log.1
9.8K ...-json.log.2
となりました。
約10KBごとにLogファイルが切り替わり、3ファイルまで保持されています。
つまりLog Rotationが実際に機能していることを確認できました。
実験用Containerは削除します。
sudo docker rm -f log-test
Log RotationはBackupや集中Log管理ではありません。
古いLogを無限に残すのではなく、HostのDiskを圧迫しないように上限を設ける仕組みとして考えるのがよさそうです。
10. Docker Networkを分離する
次はNetworkです。
これまではdocker-lab_defaultという1つのNetworkを使っていました。
確認すると、
sudo docker network inspect docker-lab_default \
--format '{{range $id, $c := .Containers}}{{$c.Name}}{{"\n"}}{{end}}'
次の4つが同じNetworkに参加していました。
caddy
uptime-kuma
docker-lab-redis-1
docker-lab-web-1
構造としては、
docker-lab_default
├── caddy
├── uptime-kuma
├── web
└── redis
です。
CaddyはFlaskへ接続する必要があります。
しかし同じNetworkにRedisも存在するため、CaddyとRedisまでNetworkを共有しています。
そこでNetworkを役割ごとに分離します。
11. frontendとbackendを作る
docker-labのcompose.yamlで、webを2つのNetworkへ参加させます。
services:
web:
networks:
- frontend
- backend
Redisはbackendだけにします。
services:
redis:
networks:
- backend
そしてNetworkを定義します。
networks:
frontend:
name: docker-lab_frontend
backend:
name: docker-lab_backend
これによって、
frontend
└── web
backend
├── web
└── redis
という構成になります。
Flaskは外側のCaddyとも内側のRedisとも通信する必要があるため、両方のNetworkへ参加します。
Redisはbackendだけです。
12. Caddyをfrontendへ接続する
Caddyは別のCompose Projectで管理しています。
Caddy側のcompose.yamlでは、外部NetworkとしてDocker LabのNetworkを参照しています。
以前は、
networks:
docker-lab:
external: true
name: docker-lab_default
でした。
これを、
networks:
docker-lab:
external: true
name: docker-lab_frontend
へ変更しました。
Caddy Service側では、
services:
caddy:
networks:
- default
- docker-lab
- uptime-kuma
となっています。
Caddyを再作成します。
cd ~/caddy
sudo docker compose up -d --force-recreate caddy
参加Networkを確認します。
sudo docker inspect caddy \
--format '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}'
結果は、
caddy_default
docker-lab_frontend
uptime-kuma_default
となりました。
Caddyはdocker-lab_backendには参加していません。
13. Caddy → Flaskを確認する
Windows PCから、
https://flask.home.arpa
へアクセスしました。
正常にFlaskページが表示され、アクセスカウンターも増加しました。
つまり、
Caddy → web
は正常です。
さらにカウンターがRedisに保存されているため、
web → Redis
も正常に通信できています。
Networkを分離しても、アプリケーションに必要な通信は維持できています。
14. Redisはbackendにしかいない
Redisの参加Networkを確認します。
sudo docker inspect docker-lab-redis-1 \
--format '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}'
結果は、
docker-lab_backend
だけでした。
一方Caddyは、
caddy_default
docker-lab_frontend
uptime-kuma_default
です。
CaddyとRedisには共通するDocker Networkがありません。
15. frontendからRedisへ接続してみる
実際に通信できないことも確認します。
Caddy Imageにredis-cliが入っているとは限らないため、docker-lab_frontendだけに参加する一時Containerを使いました。
sudo docker run --rm \
--network docker-lab_frontend \
redis:latest \
redis-cli -h redis -p 6379 ping
結果は、
Could not connect to Redis at redis:6379: Name or service not known
となりました。
これはdocker-lab_frontendではredisという名前を解決できないためです。
Redisはdocker-lab_backendにしか参加していません。
これで、
Caddy → Flask OK
Flask → Redis OK
frontend → Redis NG
という構成を実際に確認できました。
Docker Networkを分けることで、必要なContainer同士だけが直接通信できる構成にできます。
ただし、Docker Networkを分けただけであらゆる通信経路を防げる万能なFirewallになるわけではありません。
今回のポイントは、Docker Bridge Network上で「どのContainer同士に直接通信させるか」を構成として明確にしたことです。
16. Uptime Kumaもfrontendへ移動する
Uptime KumaではFlaskを、
http://web:5000
で直接監視しています。
そのためUptime Kumaもdocker-lab_frontendへ参加させます。
Uptime Kuma側のComposeでは、
services:
uptime-kuma:
networks:
- default
- docker-lab
networks:
docker-lab:
external: true
name: docker-lab_frontend
としました。
設定を反映します。
cd ~/uptime-kuma
sudo docker compose up -d --force-recreate uptime-kuma
確認します。
sudo docker inspect uptime-kuma \
--format '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}'
結果は、
docker-lab_frontend
uptime-kuma_default
となりました。
これでUptime KumaからFlaskへの直接監視も維持できます。
17. 古いNetworkを削除する
移行前のdocker-lab_defaultを確認します。
sudo docker network inspect docker-lab_default \
--format '{{range $id, $c := .Containers}}{{$c.Name}}{{"\n"}}{{end}}'
何も表示されませんでした。
つまり、もうこのNetworkを使っているContainerはいません。
削除します。
sudo docker network rm docker-lab_default
Network一覧を確認します。
sudo docker network ls | grep docker-lab
結果は、
docker-lab_backend
docker-lab_frontend
だけになりました。
古いNetworkを残さず、移行完了です。
18. 最終確認
最後にContainer一覧を確認しました。
sudo docker ps
結果、次の4つが起動しています。
uptime-kuma healthy
caddy Up
docker-lab-web-1 healthy
docker-lab-redis-1 healthy
Hostへ公開しているPortを見ると、Caddyだけが、
80
443
を公開しています。
Flask、Redis、Uptime KumaはHost側へPortを公開していません。
外部からの入口はCaddyにまとめつつ、内部ではDocker Networkを使って必要な通信だけを行う構成になりました。
最終的なdocker-labのcompose.yaml
今回の変更を含めると、Docker Lab側はおおよそ次の構成になりました。
services:
web:
build: .
cpus: "0.50"
mem_limit: 128m
environment:
APP_MESSAGE: "${APP_MESSAGE}"
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
secrets:
- redis_password
networks:
- frontend
- backend
redis:
image: redis:latest
restart: unless-stopped
command:
- sh
- -c
- 'redis-server --requirepass "$$(cat /run/secrets/redis_password)"'
healthcheck:
test:
- CMD-SHELL
- 'redis-cli -a "$$(cat /run/secrets/redis_password)" ping'
interval: 5s
timeout: 3s
retries: 5
volumes:
- redis-data:/data
secrets:
- redis_password
networks:
- backend
volumes:
redis-data:
secrets:
redis_password:
file: ./redis_password.txt
networks:
frontend:
name: docker-lab_frontend
backend:
name: docker-lab_backend
今回理解できたこと
今回の実験では、Containerを「動かす」だけではなく、「どこまで使わせるか」「どこまで通信させるか」を設定しました。
CPU制限
cpus: "0.50"
CPUを使い続けるProcessを動かしても、Container全体が約50%で頭打ちになることを確認できました。
Memory制限
mem_limit: 128m
Memory cgroupが無効だと設定そのものが破棄されることも分かりました。
docker inspectで、
Memory=134217728
となるところまで確認することが重要でした。
Log Rotation
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Logを無制限に増やすのではなく、サイズと世代数に上限を設けられます。
実験用Containerでは実際に、
-json.log
-json.log.1
-json.log.2
へローテーションするところまで確認できました。
Network分離
frontend
├── Caddy
├── Uptime Kuma
└── Flask
backend
├── Flask
└── Redis
とすることで、
Caddy → Flask OK
Flask → Redis OK
frontend → Redis NG
という通信範囲を作れました。
Containerを動かすだけでは運用にならない
Dockerを触り始めた頃は、
docker run
でContainerが起動してWebページが表示されれば、それでDockerを使えているように感じました。
しかし実際に長期間動かすことを考えると、
CPUを使い切ったらどうする?
Memoryを使い切ったらどうする?
Logが何GBにもなったらどうする?
本来通信する必要がないContainer同士まで通信できてよい?
といった問題が出てきます。
今回設定した、
Resource Limit
Log Rotation
Network Segmentation
は、そうした「動いた後」の問題に対する設定でした。
Docker ComposeはContainerの起動手順を書くだけではなく、
どれだけResourceを使えるか
どのNetworkへ参加するか
どのContainerと通信できるか
Logをどう保持するか
までコードとして残せます。
ここまで来ると、Docker Composeが単なる「Containerをまとめて起動するコマンド」ではなく、アプリケーションの実行環境そのものを定義するファイルだということがかなり見えてきました。
次回
次回はDocker環境の更新と復旧を試します。
予定している内容は、
Imageの更新
Containerの再作成
Version変更
Healthcheckによる確認
問題が起きた場合のRollback
GitとBackupの役割
Raspberry PiやSD Cardが壊れた場合の復旧
です。
これまで作ってきたDocker環境を、
「壊れないようにする」のではなく、「壊れても再構築できるようにする」
という視点で整理していきます。