または、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
はそれぞれ関数として実装されているため実行できなかった。