Pythonコンテナで非rootユーザーを使おうとしてハマった話
背景
こちらの記事で紹介したStreamlitの開発コンテナテンプレートを作るときに、
コンテナ内のユーザーを非rootにするために、DockerFile作成でかなりハマったので、その内容をまとめました。
結論
上記の記事で紹介したリポジトリのDockerFileが結論ですが、
コンテナ内のユーザーを非rootにする場合、
- 作成したユーザーのディレクトリでPATHを通す
-
pip install
の際に--user
オプションをつける
の2点を行う必要があるようです。
経緯
なぜ非rootユーザーを使うのか
ググると色々な記事が出てくるのですが、
大きく分けて2つの理由があるようです。
- セキュリティ
- rootユーザーでコンテナを実行すると、コンテナ内でroot権限が使えてしまう
- ファイルの所有権
- rootユーザーでコンテナを実行すると、コンテナ内で作成したファイルの所有権がrootになってしまう
- そのため、ホスト側でファイルを編集する際に、root権限が必要になってしまう
- rootユーザーでコンテナを実行すると、コンテナ内で作成したファイルの所有権がrootになってしまう
1はまあ個人開発であれば、そこまで気にしなくてもいいかなと思いますが、
2はGitが動かないなど結構実害があるので、非rootユーザーを使うことにしました。
devcontainerとdocker compose
今回テンプレートを作成するにあたり、
開発時はdevcontainerを使い、完成したアプリはdocker composeで動かすことができるようにしようと考えました。
理由としては、
開発時はdevcontainerを使うことでVS Codeの拡張機能も同時に管理したいですし、
開発に必要なライブラリがインストールできるようにsudo権限もつけたいです。
ファイルの変更をホスト側に反映もさせたいので、mountも必要です。
逆に完成したアプリは、
devcontainerの拡張機能は不要ですし、
sudo権限も必要ないですし、
mountも不要です。
ということで、開発時はdevcontainerを使い、完成したアプリはdocker composeで動かすことにしました。
これを実現するために、DockerFileのマルチステージビルドを使いました。
DockerFileのマルチステージビルド
DockerFileのマルチステージビルドは、
複数のDockerFileを1つのDockerFileにまとめることができる機能です。
今回は、
1つ目のステージで開発時・完成時共通の処理を行い、
2つ目のステージで開発時のみ、完成時のみの処理をそれぞれ行うようにしました。
# 1つ目のステージ(開発時・完成時共通)
FROM python:latest as base
# ユーザー作成
ARG USERNAME=pyuser
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME
USER $USERNAME
# ディレクトリ作成
WORKDIR /workspace
# pipとsetuptoolsのアップデート
RUN pip install --upgrade pip && \
pip install --upgrade setuptools
# 2つ目のステージ(開発時のみ)
FROM base as dev
USER root
# sudoのインストール
RUN apt-get update \
&& apt-get install -y sudo \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
USER $USERNAME
CMD [ "bash" ]
# 2つ目のステージ(完成時のみ)
FROM base as prd
USER $USERNAME
# ディレクトリの内容をコピー
COPY . /workspace
# ライブラリのインストール
RUN pip install -r requirements.txt
# アプリの実行
CMD ["streamlit", "run", "src/Home.py", "--server.port", "8501"]
ハマった点
上記のDockerFileを使用して、devcontainerとdocker composeの両方で動作確認をしたところ、
devcontainerではコンテナ起動後にpip install -r requirements.txt
を実行し、streamlit run src/Home.py
で問題なく動作しましたが、
docker composeでは以下のエラーが発生しました。
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "streamlit": executable file not found in $PATH: unknown
エラーの内容を見るとstreamlit
が見つからないということなので、pip install
が失敗していることはわかるのですが、
devcontainerでは問題なく動作しているので、なぜdocker composeでは失敗するのか全然わかりませんでした。
色々と調べてみたもののあまり情報がなく、仕方なくChatGPTに質問してみたところ、
pip install
はシステム全体にインストールされるため、非rootユーザーに対してインストールするときは--user
オプションをつける必要があるとのことでした。
なるほどと思い下記のようにDockerFileを修正して、再実行しました。
< RUN pip install --upgrade pip && \
< pip install --upgrade setuptools
> RUN pip install --user --upgrade pip && \
> pip install --user --upgrade setuptools
---
< RUN pip install -r requirements.txt
> RUN pip install --user -r requirements.txt
ところが全く状況は改善せず、同様のエラーが発生しました。
どういうことなんだとChatGPTに再度質問してみたところ、
--user
オプションをつけた場合、/home/$USERNAME/.local
にPATHを通す必要があるから、
DockerFileに以下の内容を追加する必要があるとのことでした。
+ ENV PATH=/home/$USERNAME/.local/bin:$PATH
確かにuserのディレクトリにインストールされているので、PATHを通さないと見つからないのは納得です。
ということで、言われたとおりにDockerFileを修正して、再実行しました。
・・・が、やはり同様のエラーが発生しました。。。
どういうことだと再度ChatGPTを問い詰めたところ、
PATHを通すなら以下の内容も書かないとダメだよって言われました。。。
+ ENV PATH=$PYTHONUSERBASE/bin:$PATH
PATHを通すんだから言われてみれば当たり前だよなと思いましたが、
なぜ最初から教えてくれなかったのか。。。
ここまでやってやっと動作しました。
まとめ
結果的にちゃんと動くものができたので良かったのですが、
結局なぜdevcontainerでは最初からうまくいったのかは謎のままです。。。
おそらく、コンテナ作成時にRUN
でpip install
を実行するのと、コンテナ作成後にコンテナ内でpip install
を実行するのとでは、何らかの違いがあるのだと思います。
また、devcontainerではPATHを通さなくても動作していたので、そちらも謎です。
devcontainer周りでは、VS Codeが結構黒魔術で色々やってくれていたりするので(compose.ymlでPortのフォワーディング設定をしてなくても、アプリが起動したら勝手にやってくれたりする)、今回もその類かもしれません。
「コンテナは非rootユーザー使えよ」って記事は結構出てくるのですが、
具体的にどうやって実現するのかはあまり出てこないので、
今回の記事やリポジトリが少しでも誰かの役に立てれば幸いです。
Discussion