問題:サービスを停止せずにコンテナを移行する
先月、VPSをより高性能なサーバーに移行する必要がありました。実行中のコンテナはバックグラウンドで処理を行うジョブで、すでに6時間稼働しており、大きなバッチジョブの途中でした。コンテナを停止すると、それまでの進捗がすべて失われ、最初からやり直しになります。かといってそのままにしておくと移行ができません。
これは多くの人が頭を抱えた経験のある状況です:Dockerコンテナには文字通り「一時停止して移動する」仕組みが存在しません。よく使われる一般的な方法は:
- コンテナを停止 → データをバックアップ → 新サーバーでRestore → 再起動(ダウンタイムあり)
- 永続ボリュームを使用して再起動(インメモリ状態は失われる)
- 複雑なレプリケーションを使用(リソースが必要で、常に実現可能とは限らない)
しかし、あまり知られていない方法があります:CRIU(Checkpoint/Restore In Userspace)——プロセスの全状態をファイルにダンプし、別のマシンに転送して、何事もなかったかのように実行を継続できます。
原因:コンテナを「ホット」に移行できない理由
コンテナの本質は、Linuxのnamespacesとcgroupsで隔離されたプロセスグループです。実行中のコンテナには以下が含まれます:
- メモリ状態:ヒープ、スタック、処理中の変数
- ファイルディスクリプタ:開いているソケット、パイプ、ファイルハンドル
- ネットワーク接続:確立済みのTCP接続
- プロセス状態:CPUレジスタ、シグナルハンドラ、タイマー
これらはすべて現在のサーバーのRAMにのみ存在します。DockerイメージはファイルシステムのスナップショットのみSaveし、ランタイムの状態は保存しません。そのため、docker commitして新しいマシンでdocker pullしても、実行中のインメモリ状態には何の助けにもなりません。
解決策
方法1:Blue-greenデプロイメント(CRIUを使わない)
アプリケーションがステートレスか、データベースから状態を同期できる場合、blue-greenデプロイメントははるかにシンプルな選択肢です:
- 移行先サーバーで新しいコンテナを起動
- NginxやロードバランサーでトラフィックをSwitchする
- 確認が取れたら古いコンテナを停止
ただし、コンテナが重要なインメモリ状態(ロード済みのMLモデル、実行中のバッチジョブ、維持中のWebSocket接続など)を保持している場合、この方法は適していません。
方法2:CRIU — Checkpoint/Restore In Userspace
CRIUはLinuxツールで、プロセスの全状態をファイルにダンプし、任意の場所でRestoreできます。DockerはCRIUをexperimentalな機能として2つの主要コマンドで統合しています:
docker checkpoint create— コンテナをフリーズし、状態をディスクにダンプdocker start --checkpoint— 作成したチェックポイントからRestore
実践:Docker Checkpoint + CRIUでコンテナを移行する
ステップ1:両方のサーバーにCRIUをインストール
# Ubuntu/Debian
sudo apt-get install -y criu
# バージョン確認(3.14以上が必要)
criu --version
# カーネルが必要な機能をサポートしているか確認
criu check --ms
ステップ2:DockerデーモンのExperimental機能を有効化
デフォルトではDockerのcheckpoint機能は無効になっています。デーモンの設定ファイルで有効にする必要があります:
sudo nano /etc/docker/daemon.json
{
"experimental": true
}
sudo systemctl restart docker
# experimentalが有効になっているか確認
docker info | grep -i experimental
# Experimental: true
ステップ3:runcランタイムでコンテナを起動
CRIUはruncランタイムでのみ動作します——Docker 24以降のデフォルトであるcontainerdスナップショッターとは互換性がありません。このエラーのデバッグにかなりの時間を費やしました:
# --runtime=runcの使用が必須
docker run -d --name myapp \
--runtime=runc \
--security-opt seccomp:unconfined \
nginx:alpine
# コンテナが実行中であることを確認
docker ps
--security-opt seccomp:unconfinedフラグは、CRIUが状態をダンプする際に必要なシステムコールへのアクセス権限を確保するために必要です。
ステップ4:チェックポイントの作成
# チェックポイント作成(ダンプ中はコンテナが一時停止)
docker checkpoint create myapp checkpoint1
# チェックポイント後もコンテナの実行を継続する場合
docker checkpoint create --leave-running myapp checkpoint1
# 作成済みチェックポイントの一覧表示
docker checkpoint ls myapp
# CHECKPOINT NAME
# checkpoint1
チェックポイントデータはデフォルトで以下に保存されます:
/var/lib/docker/containers/<container-id>/checkpoints/checkpoint1/
ステップ5:新しいサーバーへチェックポイントを転送
# コンテナIDを取得
CONTAINER_ID=$(docker inspect --format='{{.Id}}' myapp)
# チェックポイントディレクトリを圧縮
tar -czf checkpoint1.tar.gz \
/var/lib/docker/containers/${CONTAINER_ID}/checkpoints/checkpoint1/
# 新しいサーバーにコピー
scp checkpoint1.tar.gz user@new-server:/tmp/
# コンテナのファイルシステムをエクスポート(正しい環境でRestoreするために必要)
docker export myapp | gzip > myapp-fs.tar.gz
scp myapp-fs.tar.gz user@new-server:/tmp/
ステップ6:新しいサーバーでRestore
# --- 新しいサーバー上で実行 ---
# 1. コンテナのファイルシステムをインポート
docker import /tmp/myapp-fs.tar.gz myapp-image:restored
# 2. 同じ名前でコンテナを作成(まだ起動しない)
docker create --name myapp \
--runtime=runc \
--security-opt seccomp:unconfined \
myapp-image:restored
# 3. 新しいコンテナIDを取得
NEW_CONTAINER_ID=$(docker inspect --format='{{.Id}}' myapp)
# 4. チェックポイントを正しい場所にコピー
mkdir -p /var/lib/docker/containers/${NEW_CONTAINER_ID}/checkpoints/
tar -xzf /tmp/checkpoint1.tar.gz \
-C /var/lib/docker/containers/${NEW_CONTAINER_ID}/checkpoints/
# 5. チェックポイントからRestore
docker start --checkpoint checkpoint1 myapp
Restore後の確認
# ログを確認してコンテナが中断地点から継続していることを確認
docker logs myapp
# コンテナの詳細状態を確認
docker inspect myapp
私はよくdocker inspectのJSON出力をtoolcraft.app/ja/tools/developer/json-formatterに貼り付けて読みやすくしています——拡張機能をインストールするより手軽で、特にブラウザ拡張機能がない状態でサーバーにSSH接続している場合に重宝します。
ベストプラクティス:CRIUを使う前に知っておくべきことと注意点
CRIUを使うべき場面:
- コンテナがディスクから復元できない重要なインメモリ状態を保持している場合
- 長時間実行のバッチジョブを最初からやり直したくない場合
- CPU/RAM集約型ワークロードのハードウェアアップグレード時に移行が必要な場合
- 本番環境の問題をデバッグする場合:エラー発生時点の状態をスナップショットとして保存し、オフラインで解析できる
CRIUを使うべきでない場面:
- コンテナにアクティブなTCP接続がある場合——Restoreは通常失敗するか、接続が切断される
- GPUワークロードを使用している場合——CRIUはCUDA/GPUの状態をサポートしていない
- コンテナが複雑なユーザーネームスペースを使用しており、Restore時に競合が発生する可能性がある場合
- 厳格なSLAがある本番環境——CRIUはDockerでまだexperimentalであり、本番システムで初めて試すべきではない
定期的なチェックポイントスクリプト
移行が必要になるまで待つのではなく、定期的にチェックポイントを作成するパターンが効果的だと感じています:
#!/bin/bash
# 毎時cronで実行: 0 * * * * /opt/scripts/checkpoint.sh
CONTAINER_NAME="myapp"
CHECKPOINT_NAME="auto-$(date +%Y%m%d-%H%M)"
# 古いチェックポイントを削除し、最新3件を保持
OLD=$(docker checkpoint ls $CONTAINER_NAME | tail -n +2 | head -n -3 | awk '{print $1}')
for cp in $OLD; do
docker checkpoint rm $CONTAINER_NAME $cp 2>/dev/null
done
# 新しいチェックポイントを作成、コンテナを一時停止しない
docker checkpoint create --leave-running $CONTAINER_NAME $CHECKPOINT_NAME
echo "[$(date)] Checkpoint created: $CHECKPOINT_NAME"
実践から得た教訓
- 移行元と移行先のカーネルバージョンは一致している必要があります——CRIUのチェックポイントはカーネルABIに依存しているため、バージョンが大きく異なるとRestoreに失敗します
- まず開発環境でテストする——問題が発生している最中に本番環境で初めて試みないでください
- ダンプ時間はメモリ使用量に依存します——8GBのRAMを使用するコンテナは、チェックポイントに数分かかる場合があります
- Docker SwarmとKubernetesはCRIU移行をネイティブにサポートしていないため、サードパーティのツールか、JVMワークロード向けのCRAC(Coordinated Restore at Checkpoint)を使用する必要があります
CRIUは強力なツールですが、その制限をしっかり理解した上で使う必要があります。ステートレスなサービスには、blue-greenデプロイメントの方がはるかに安全な選択肢です。しかし、単純な再起動では対応できない複雑な状態を保持するコンテナには、CRIUだけが解決策となります。

