または、sdkman, rbenv, nvmなどのパッケージマネージャをDockerfileやコンテナで動かす方法について。
TL;DR
DockerfileのRUNがbashかつログインシェルで動いてほしいとき
SHELL ["/bin/bash", "-l", "-c"]
DockerfileのCMDや、docker runに渡したコマンドがbashのログインシェルで動いてほしいとき
RUN echo '#!/bin/bash\nexec /bin/bash -l -c "$*"' > /entrypoint.sh && \
chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
経緯
諸事情でJava, Gradle, Ruby, Node.jsをそれぞれ指定されたバージョンでインストール済みのコンテナイメージを作る必要があった。
apt-getなどOS標準のパッケージマネージャで過去のバージョンを指定してインストールしたり、個別にインストールするのが面倒に感じたため、sdkman, rbenv, nvmといったバージョンマネージャをDockerfile内でインストールして使うことにした(いろいろあって結局使うのをやめたが……)。
しかしこれらのツールは、Dockerfile内ではうまく動作しなかった。
なぜならsdkmanなどのバージョンマネージャは
- bashを前提としているものがある(関数やsourceコマンドなど)
-
.bashrcで初期化スクリプト(PATHの設定など)が実行される
つまり、動作にはbashかつログインシェルで実行される必要がある。一方、DockerfileのRUN命令はデフォルトで
-
/bin/sh -cの引数として実行される - bashでもログインシェルでもないため
.bash_profileや.bashrcが実行されない
解決策
SHELL命令を使うとDockerfile内のRUNがどのシェル+引数によって実行されるかを指定できる。
SHELL ["/bin/bash", "-l", "-c"]
また、ENTRYPOINT命令でCMDやdocker runに渡したコマンドを実行するプロセスを指定できる。これは以下のシェルスクリプトを実行させると大抵の場合うまくいくという結論に達した。
# !/bin/bash
exec /bin/bash -l -c "$*"
ENTRYPOINT ["/entrypoint.sh"]
ENTRYPOINTの指定について
上のentrypoint.shはずいぶん回りくどく感じると思うので、いくつか他にも試して問題があったケースを挙げておく。
SHELLと同じ指定をした場合
ENTRYPOINT ["/bin/bash", "-l", "-c"]
-cオプションが引数を1つしか受け取らないため、CMDやdocker runでコマンドをまとめて1つの文字列として渡す必要があり、かなり面倒。
shebangでログインシェルを指定してexecだけで実行させた場合
# !/bin/bash -l
exec $*
execコマンドの第1引数には実行ファイルを指定する必要があるため、関数やbash組み込みのコマンドが実行できない。
sdk, rbenv, nvmはそれぞれ関数として実装されているため実行できなかった。