
CIで作ったDockerイメージが別のサーバーで動かなかった話:落とし穴はビルドツールのデフォルト設定にあった
何が起きたか
CI のジョブが、こんなエラーで落ちていました。
does not seem to be running inside a container
$ docker run -t -d ... your-registry/your-image:latest cat
ERROR: The container started but didn't run the expected command.「コンテナが起動したけど、期待したコマンドが実行されなかった」というエラーです。
ENTRYPOINT の設定ミスかな、とか、ユーザー権限の問題かな、と最初は思いました。でも原因は全然別のところにありました。
原因:CPUアーキテクチャの不一致
Docker イメージは CPU のアーキテクチャに依存します。大きく分けると「arm64(Apple Silicon や AWS Graviton など)」と「x86_64(Intel/AMD の一般的なサーバー)」があり、片方向けにビルドされたイメージは、もう片方では動きません。
Windows のソフトが Mac で動かないのと、感覚的には近いです。
今回の構成はこうなっていました。
- ビルドジョブ: arm64 のノードで
docker build → push→ arm64 専用イメージができる - 検証ジョブ: 同じ arm64 ノードで動く → 当然 OK
- 本番ジョブ: x86_64 のノードで動く → arm64 イメージを pull してきても実行できない → 即死
「検証は通るのに本番が落ちる」という構造になっていたわけです。ログのエラーも起動失敗を直接示してくれないので、最初は何が起きているのかわかりませんでした。
解決の方針:1つのイメージで両方に対応する
対処方法はいくつか考えられました。
- 本番ジョブのサーバーを arm64 に変える(最小変更)
- arm64 用と x86_64 用の 2 つのイメージを別タグで管理する
- 1 つのイメージで両方のアーキテクチャに対応する(マルチアーキ対応)
1 は手っ取り早いですが、「arm64 のサーバーにしか使えないイメージ」という制約が残ります。2 はタグ管理が煩雑になる。
3 を選びました。docker buildx というツールを使うと、1 つのイメージタグに「arm64 向け」と「x86_64 向け」の両方を詰め込めます。サーバーが自分のアーキテクチャに合う方を自動で選んで pull してくれるので、利用側の Jenkinsfile は一切変更不要です。
buildx + QEMU の仕組み
「arm64 のサーバー上で、どうやって x86_64 向けのイメージをビルドするのか」という問題があります。ここで使うのが QEMU です。
QEMU は CPU エミュレーターで、「あるアーキテクチャの命令を、別のアーキテクチャ上で翻訳しながら実行する」ソフトです。arm64 のサーバー上で x86_64 向けのビルドが走るとき、裏で QEMU が命令を 1 つずつ翻訳してくれます。
# これでarm64ノード上でもx86_64のバイナリが実行できるようになる
docker run --privileged --rm tonistiigi/binfmt:qemu-v8.1.5 --install allデメリットはビルドが遅くなることです。ネイティブ実行の 3〜10 倍かかります。今回は 7 分前後だったビルドが 22 分になりました。「遅い」けど「動く」を選んだわけです。
ビルドコマンド自体はこうなります。
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-t your-registry/your-image:latest \
.--platform で両方を指定して、--push でそのままレジストリに送ります。これで ECR に「arm64 用」と「x86_64 用」が 1 つのタグの下にまとまった状態で push されます。
「直った!」と思ったら、また動かなかった
ここが今回のいちばんの落とし穴でした。
ECR への push は成功。マニフェストを確認すると、arm64 と x86_64 のエントリが両方ある。でも本番ジョブは相変わらず同じエラーで落ちる。
「なぜ?」と調べてみると、ECR に上がっていたマニフェストが想定と違いました。
{
"manifests": [
{ "platform": { "architecture": "amd64", "os": "linux" } },
{ "platform": { "architecture": "arm64", "os": "linux" } },
{ "platform": { "architecture": "unknown", "os": "unknown" } },
{ "platform": { "architecture": "unknown", "os": "unknown" } }
]
}architecture: unknown の正体は、buildx がデフォルトで自動付与するメタデータ(SLSA provenance attestation)です。「このイメージがどこでどうビルドされたか」を証明するための情報で、セキュリティ上は有用なものです。
ただ、これが Jenkins の Docker Plugin と相性が悪い。unknown/unknown のエントリを見て Jenkins 側が混乱し、イメージを正しく扱えなくなっていました。これは buildx と Jenkins の既知の互換性問題です。
解決はシンプルでした。
docker buildx build \
--platform linux/amd64,linux/arm64 \
--provenance=false \
--sbom=false \
--push \
-t your-registry/your-image:latest \
.この 2 フラグを追加するだけ。マニフェストが「amd64」と「arm64」の 2 エントリだけのシンプルな構成になり、Jenkins も問題なく処理できるようになりました。
その他の判断ログ
ノードのサイズを上げた
ビルドが遅くなった原因は QEMU だけではありませんでした。元々使っていた軽量ノード(t 系 micro 相当)はメモリが 1GB しかありません。
npm install や TypeScript のコンパイル、QEMU 自体もメモリを食います。メモリ不足でスワップが起きると、体感より大幅に遅くなります。
ノードのクラスを比べると、vCPU 数はどれも同じで、差はメモリ量だけでした(1GB / 2GB / 4GB)。medium クラス(4GB)に変更したところ 22 分で完了。これは QEMU の影響込みでの数字なので、妥当な範囲だと思っています。
ECR ビルドは 1 日 1 回しか動かないので、ノードサイズを上げてもコストへの影響は無視できる範囲です。
cronを週次に変更した
元は毎日深夜に自動実行していました。目的はベースイメージのセキュリティパッチを自動取り込みすることです。
ただ、ソース変更がない日にも毎日ビルドが走るのは純粋に無駄です。急ぎのときは手動で実行できるので、週次(日曜深夜)に変更しました。最大で 1 週間分の遅れは出ますが、実用上は許容範囲と判断しました。
まとめ
今回やったことを整理すると、こうなります。
問題: arm64でビルド → x86_64で動かない
解決: docker buildx --platform linux/amd64,linux/arm64 --push
落とし穴: それでもJenkinsで動かない
原因: buildxのデフォルト動作でattestationメタデータが付与される
解決: --provenance=false --sbom=false を追加docker buildx 自体の使い方はドキュメントに豊富にあります。でも「これを Jenkins で使うと attestation 問題が出る」という情報は、探してもなかなか出てきません。正直、ここは予想外の障壁でした。
buildx を使ったマルチアーキビルドを CI 環境に組み込もうとしている方は、マニフェストの中身を docker manifest inspect で確認する手順を習慣にしておくといいと思います。「pull できたのになぜ起動しないか」を切り分けるのに直接効きます。
参考
Docker の使い方を体系的に押さえたい方には、以下の書籍が参考になります。
ECR や ECS など AWS でコンテナを動かす設計を学びたい方にはこちら。
Jenkins 側の構成や Docker Plugin との付き合い方をもう少し深く知りたい場合には、こちらもおすすめです。
この記事は役に立ちましたか?

この記事が、何かの整理につながったら
コーヒー1杯分の応援をもらえると嬉しいです。
あなたへのおすすめ
