10年以上稼働している業務システムや EC サイトを抱え、「まずは環境差異をなくすために Docker 化から着手しよう」と決断する現場は少なくありません。

前作の記事 でも、全面リプレイスの前に着手すべき対策の1つ目としてコンテナ化を挙げ、Mojolicious 公式 Cookbook に掲載されているシンプルな Dockerfile を紹介しました。

FROM perl
WORKDIR /opt/myapp
COPY . .
RUN cpanm --installdeps -n .
EXPOSE 3000
CMD ./myapp.pl prefork

公式ドキュメントが示す通り、公式イメージには必要なツールが揃っており、新規に立ち上げるプロジェクトであればこれだけで問題なくコンテナが起動します1

しかし、この6行の Dockerfile は「今日から作る新規プロジェクト」を前提としたものであり、10年物のレガシーコードではそのままではビルドが通りません

本番サーバーで何年も問題なく動いていたソースコードをコピーし、意気揚々と docker build を実行した初日、ターミナルは何画面分もの真っ赤なエラーログで埋め尽くされます。「公式の手順通りにやっているはずなのに、なぜ動かないのか」と頭を抱え、初手で作業がストップしてしまうのは、レガシー移行の現場で誰もが最初に躓(つまず)く石です。

この記事では、なぜ公式ドキュメント通りの Dockerfile が10年物のレガシーコードに通らないのか、その構造的な3つの理由を紐解きます。そして、依存関係を「当時のスナップショット」として固定し、確実にコンテナ化を成功させる脱出ルートを解説します。


公式 Dockerfile が10年物に通らない3つの構造的理由

ビルドが失敗する原因は、Docker の書き方が悪いからでも、既存コードが壊れているからでもありません。根本的な原因は、「依存関係の解決メカニズムが、常に最新(HEAD)を向くように作られていること」 にあります。

具体的には、次の3つの壁が同時に立ちはだかります。

[初日に立ちはだかる3つの壁]
1. 最新版解決の罠  ── 推移的依存が最新化され、破壊的変更やAPI非互換を踏む
2. 消えた配布物    ── 古いバージョンがCPANインデックスから削除され404になる
3. ビルド依存の欠落 ── XSモジュールに必要なOSヘッダーや旧ツールチェーンが足りない

① 最新版解決の罠 ── 推移的依存とランタイムが常に最新を向いている

Dockerfile に書かれた cpanm --installdeps . は、プロジェクトの依存宣言(cpanfileMakefile.PL)を読み込み、CPAN の最新インデックス(02packages.details.txt.gz)を参照してモジュールをダウンロードします2

ここで問題になるのが、「推移的依存(Transitive Dependencies)」 です。

たとえアプリケーションの直接の依存モジュールについてバージョンを固定していたとしても、そのモジュールが依存している「孫モジュール」「ひ孫モジュール」の多くは、バージョン指定が >=(以上)となっているか、あるいはバージョン制限なしで定義されています。その結果、ビルドを実行した時点での世界最新のライブラリ群が再帰的に引き込まれてしまいます。

10年前に書かれたコードの足元に、昨夜リリースされたばかりの最新ライブラリが流し込まれるため、非推奨メソッドの削除や API の破壊的変更によって実行時エラーが多発します。

さらに厄介なのが、ベースイメージ FROM perl 自体が最新の Perl ランタイム(Perl 5.40 等)を採用している点です。たとえば Perl 5.26 では、セキュリティ脆弱性への対策(CVE-2016-1238)として、モジュール検索パス @INC の末尾からカレントディレクトリ(.)がデフォルトで削除されました3。10年前の Perl モジュール(特に Module::Install を使った Makefile.PL)は、ビルド時にカレントディレクトリのスクリプトを呼び出す前提で書かれているものが多く、最新の Perl ランタイム上では Can't locate inc/Module/Install.pm in @INC というエラーを出して即死します。

② 消えた配布物 ── CPAN の更新と 404 の壁

「依存が最新化されて壊れるなら、10年前のバージョンを明示的に指定してインストールすればいい」と考えるかもしれません。しかし、単純に cpanm Mojolicious@4.20 のように過去のバージョンを指定しても、今度はダウンロード自体が 404 Not Found で失敗します。

Perl のパッケージ配布基盤である CPAN は、世界中のミラーサーバーの容量を健全に保つため、「最新の安定版のみをアクティブなインデックスに保持する」 というポリシーで運用されています。開発者が PAUSE(Perl Authors Upload Server)に新しいバージョンをアップロードすると、古いバージョンの tarball はメインミラー群のインデックスから自動的に削除(クリーンアップ)されます45

PAUSE、CPANメインミラー、BackPANのアーカイブ関係図

過去に一度でもアップロードされた全配布物は、BackPAN と呼ばれる歴史的アーカイブに永久保存されています5。しかし、cpanm はデフォルトでアクティブな CPAN ミラーを参照するため、BackPAN ミラー(例: cpanm --mirror http://backpan.perl.org/ --mirror-only <モジュール名>)を明示的に指定しない限り、10年前の配布物には辿り着けません。

③ ネイティブビルド依存の欠落 ── XS モジュールと OS ヘッダーの断絶

Perl モジュールには、Perl スクリプトだけで完結する Pure Perl モジュールのほかに、高速化やシステム連携のために C/C++ で書かれた XS(C 言語拡張)モジュール が多数存在します(DBD::mysql, IO::Socket::SSL, XML::Parser, JSON::XS など)。

Docker の軽量ベースイメージ(perl:slim や Alpine Linux 等)には、C コンパイラ(gcc, make)や各種ミドルウェアの開発用ヘッダー(libssl-dev, libmysqlclient-dev 等)が含まれていません6

さらに、OS やコンパイラ自体の世代交代が時限爆弾となります。

  • GCC 14 の厳格化: 近年の Linux ディストリビューションに搭載されている GCC 14 以降では、C99 以前の暗黙の関数宣言(-Wimplicit-function-declaration)が警告ではなくデフォルトでコンパイルエラー扱いになります7。10年前の C/XS コードは、最新のコンパイラではコンパイルすら通りません。
  • OpenSSL 3.0 の構造体不透明化: OpenSSL 1.0/1.1 から OpenSSL 3.0 へのメジャーアップデートにより、古い暗号化 API や構造体への直接アクセスが禁止されました8。古いバージョンの Net::SSLeayCrypt::SSLeay は、最新の Debian 12 や Ubuntu 22.04+ 上ではビルドできません。

抜け出す手順: 「当時の世界」をスナップショットで固定する

この泥沼から抜け出すための唯一確実なアプローチは、「アプリケーション層」と「OS 層」の双方で、当時の状態をスナップショットとして固定することです。

1. アプリケーション層の固定: 初期スナップショットの生成と Carton

現代の Perl アプリケーションでは、Carton を使って依存モジュールを管理するのが定石です9

Carton は Ruby の Bundler や PHP の Composer に相当するツールで、推移的依存を含むすべてのモジュールの正確なバージョンと配布物パス(pathname)を cpanfile.snapshot という単一のロックファイルに記録します。

# cpanfile.snapshot の記録例
# carton snapshot format: version 1.0
DISTRIBUTIONS
  Mojolicious-4.20
    pathname: S/SR/SRI/Mojolicious-4.20.tar.gz
    provides:
      Mojolicious 4.20
    requirements:
      ExtUtils::MakeMaker 0
      perl 5.010001
  YAML-0.84
    pathname: M/MS/MSTROUT/YAML-0.84.tar.gz
    provides:
      YAML 0.84
    requirements:
      ExtUtils::MakeMaker 6.59

ここで「10年前のレガシーにはそもそも cpanfile.snapshot が存在しない」というニワトリと卵の問題に直面します。現代の環境でゼロから carton install を実行すると、前述の3つの壁に直撃してスナップショットの生成自体に失敗します。

そのため、初期スナップショットは 「いま現実に動いている本番サーバーやステージング環境」から抽出する のが最も安全です。稼働中のサーバー上で carton を導入して carton snapshot コマンドを実行し、現在のインストール済み環境から cpanfile.snapshot を書き出すか、@INC にインストールされている既存モジュールのバージョン情報をダンプしてスナップショットを構築します。

一度 cpanfile.snapshot を作成してリポジトリにコミットすれば、Dockerfile 内では次のコマンドで決定論的(再現可能)なビルドが可能になります。

# cpanfile と cpanfile.snapshot を先にコピー
COPY cpanfile cpanfile.snapshot ./

# snapshot に記録されたバージョンのみを厳密にインストール
RUN carton install --deployment

--deployment オプションを付与すると、Carton は CPAN の最新インデックスを見に行かず、スナップショットに記録された通りのバージョンと tarball パスだけをインストールします。

さらに carton bundle コマンドを実行して全 tarball を vendor/cache/ ディレクトリにダウンロードしておけば、外部ネットワークに一切依存しない完全オフラインビルドも実現できます。

2. OS 層のタイムマシン: snapshot.debian.org の活用

10年前のランタイムや C ライブラリのヘッダーを過不足なく揃えるには、OS レベルでも「当時の状態」を復元する必要があります。

ここで役立つのが、Debian が公式に提供しているアーカイブサービス snapshot.debian.org です10。このサービスは、過去の Debian パッケージリポジトリの全状態を日時指定で保持しています。

最新のベースイメージを使うのではなく、当時の Debian コードネーム(例: Debian 8 Jessie)のベースイメージを選定し、パッケージ取得先を snapshot.debian.org に固定します。

この際、過去のリポジトリに対して apt-get update を実行すると、Release ファイルの有効期限切れエラー(Release file expired)が発生するため、APT の有効期限チェックを無効化する設定を追加します1112

# 当時のベースイメージ(Debian 8 Jessie)を指定
FROM debian:jessie-20160503

# snapshot.debian.org を指定し、期限切れチェックを無効化
RUN echo 'deb [check-valid-until=no trusted=yes] http://snapshot.debian.org/archive/debian/20160501T000000Z/ jessie main' > /etc/apt/sources.list && \
    echo 'deb [check-valid-until=no trusted=yes] http://snapshot.debian.org/archive/debian-security/20160501T000000Z/ jessie/updates main' >> /etc/apt/sources.list && \
    echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-check-valid-until

# 当時のコンパイラツールチェーンと開発用ヘッダーを導入
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libssl-dev \
    libmysqlclient-dev \
    perl \
    cpanminus \
    && rm -rf /var/lib/apt/lists/*

この設定により、2016年時点の GCC や OpenSSL 1.0 系のヘッダーが正確に復元され、10年前の XS モジュールでも一切のエラーなくコンパイルを通すことができます。


言語非依存の教訓: 「ロックファイルなき依存解決」はどの言語でも同じ壁に当たる

ここまでの話は、Perl に限った特殊な事例ではありません。Ruby、PHP、Python など、パッケージマネージャーを持つすべての言語において、「ロックファイルなき依存解決」は時間の経過とともに必ず腐敗(Bit Rot)します

10年前のプロジェクトでは、ロックファイルという概念がまだ普及していなかったり、サーバー上のグローバル環境に「秘伝のタレ」のようにライブラリが手動で蓄積されていたりするケースが多々あります(たとえば Python であれば、稼働中サーバーで pip freeze > requirements.txt を実行して全推移的依存のバージョンをピン留めする作業に相当します)。直接の依存関係しか書かれていない設定ファイル(Gemfile, composer.json, requirements.txt 単体)を使って無防備にコンテナをビルドしようとすると、どの言語でも全く同じ壁にぶつかります。

言語直接依存宣言推移的依存のロックファイル決定論的デプロイコマンド
Perlcpanfilecpanfile.snapshotcarton install --deployment9
RubyGemfileGemfile.lockbundle config set --local deployment 'true' && bundle install1314
PHPcomposer.jsoncomposer.lockcomposer install --no-dev --optimize-autoloader1516
Pythonrequirements.in / pyproject.tomlrequirements.txt (pinning) / uv.lockpip install --no-deps -r requirements.txt17 / uv sync --frozen18

レガシーシステムのコンテナ化における黄金律は、「コンテナ化(動作環境の再現)」と「最新化(ライブラリや言語のアップデート)」を同時にやってはいけない ということです。

初手で「せっかく Docker にするのだから、Perl や PHP のバージョンも最新に上げて、フレームワークも最新版にしよう」と欲張ると、何が原因で壊れたのか切り分けがつかなくなり、移行プロジェクトはほぼ確実に座礁します。

このスナップショット固定はゴールではなく、安全にテストを回しながら段階的に最新化へ進むための「検証可能な足場(ベースライン)」を手に入れる作業です。まずは「当時の OS スナップショット」と「当時の依存関係ロックファイル」を使って、10年前のコードがそのまま100%動くコンテナ を作り上げることが先決です。安全に動くベースラインを確保し、自動テストを回せる状態を整えて初めて、安全な段階的バージョンアップへの道が開かれます。


まとめとご相談

公式ドキュメントに書かれている数行の Dockerfile は、あくまでクリーンな新規アプリケーションのためのものです。10年物のレガシーシステムをコンテナ化する際は、初日に次のトラブルが起きる前提で見積もりを立てる必要があります。

  1. 推移的依存の最新化: 直接依存だけでなく孫依存まで含めた破壊的変更を踏む
  2. CPAN からの配布物削除: 旧版 tarball の 404 エラーと BackPAN による救済
  3. ネイティブビルドの断絶: GCC や OpenSSL の世代断絶による C 拡張のビルドエラー

これらを突破するには、「本番環境からの依存関係抽出によるロックファイルの作成」と「OS レベルのスナップショット固定」が不可欠です。

「10年以上動いているシステムをコンテナ化したいが、依存関係のエラーが解消できない」「当時の環境を再現できるエンジニアが社内にいない」といった課題を抱えている情シス・開発責任者の方は、ぜひ コンタクトフォーム よりお気軽にご相談ください。レガシーシステムの安全な環境復元から段階的なモダン化まで、現場の状況に合わせた実践的なサポートを提供いたします。

Footnotes

  1. Mojolicious::Guides::Cookbook (Containers) — https://docs.mojolicious.org/Mojolicious/Guides/Cookbook

  2. App::cpanminus — https://metacpan.org/pod/App::cpanminus

  3. Perl 5.26.0 Delta - Removal of current directory from @INC — https://perldoc.perl.org/perl5260delta

  4. PAUSE FAQ — https://pause.perl.org/pause/query?ACTION=pause_faq

  5. CPAN Mirror FAQ — https://www.cpan.org/misc/how-to-mirror.html 2

  6. Docker Official Image - perl — https://hub.docker.com/_/perl

  7. Porting to GCC 14 — https://gcc.gnu.org/gcc-14/porting_to.html

  8. OpenSSL 3.0 Migration Guide — https://www.openssl.org/docs/man3.0/man7/migration_guide.html

  9. GitHub - perl-carton/carton — https://github.com/perl-carton/carton 2

  10. Debian Wiki - DebianSnapshot — https://wiki.debian.org/DebianSnapshot

  11. Debian Wiki - RollbackUpdate — https://wiki.debian.org/RollbackUpdate

  12. Debian APT Configuration man page — https://wiki.debian.org/DebianRepository/UseThirdParty

  13. Bundler - bundle config — https://bundler.io/man/bundle-config.1.html

  14. Bundler - bundle install — https://bundler.io/man/bundle-install.1.html

  15. Composer Basic Usage — https://getcomposer.org/doc/01-basic-usage.md

  16. Composer CLI — https://getcomposer.org/doc/03-cli.md

  17. pip User Guide - Repeatable Installs — https://pip.pypa.io/en/stable/topics/repeatable-installs/

  18. uv Documentation - Project Sync — https://docs.astral.sh/uv/concepts/projects/sync/