Docker環境においてroot以外でpip installしたパッケージをrootで使おうとしてはまった話 

# はじめに
ある日GettingStartedで用意されていた手順とDockerfileでpythonの開発環境のコンテナを立て、開発を始めようと思ったときに、出鼻をくじかれたので何が起こっていたかメモとして残しました。

# 概要
- root userでpip install したパッケージは別ユーザからも使用できるが、root user以外でインストールしたものはそのユーザからしか見えない。
- 原因はpip install時のパスでrootの場合`/usr/local`配下インストールされるが、他ユーザは`~/.local`配下になるため。
    - root でpythonを実行するときに読まれるパスは`/usr/local`配下のもののみで、他ユーザの場合は`/usr/local`に加え`~/.local`配下が読まれる
- 解決方法は、何とかしてパスが通った状態にする。

# 何をやっていたか
とあるOSSのGettingStartedでDockerを使ったpythonの開発環境を使用していました。
原因部分を抜粋してすごく簡略化して書いていますが、大体以下のような構成です。
- ファイル構成
```shell-session
$ tree
.
├── Dockerfile
└── test.py
```
- Dockerfile
```Dockerfile
FROM python:3.11-slim

COPY test.py /

RUN groupadd --system --gid 999 cacaomath \
  && useradd --no-log-init --system -u 999 --gid cacaomath --create-home cacaomath

RUN pip install pytest

USER cacaomath
RUN pip install web.py

# ...そのほかのいろいろな処理...
```
- test.py
```py
import web

# 実働確認用
print("web module exists")

# 実際にはこのあとにも処理がたくさん続いています。...
```

このような環境において、
`sudo docker run -it -uroot test python test.py`を実行するような手順が用意されていたのですが、実行しても`ModuleNotFoundError: No module named 'web'`のエラーになるばかりでどうしても実行することができませんでした。
```shell-session
```shell-session
$ sudo docker run -it -uroot test python test.py
Traceback (most recent call last):
  File "//test.py", line 1, in <module>
    import web
ModuleNotFoundError: No module named 'web'
```
かなしい..

# 原因
`ModuleNotFoundError: No module named 'web'`のエラーの根本的な原因としては、
**PATHが通っていない**といった単純なことでした。

まず、今回の環境はDockerの環境内でrootと別のユーザで行う処理のすみわけがされており、
rootとそれ以外のユーザで使うパッケージを意図的に分けてインストールがされる構成になっていました。
```Dockerfile
RUN pip install pytest # rootおよびそれ以外のユーザの処理で使うパッケージ

USER cacaomath
RUN pip install web.py # root以外のユーザが使うパッケージ
```

これ自体はいいと思うのですが、問題はそれぞれのユーザでインストールされるパッケージのPATHでした。

通常pip innstallを行ったときそれぞれインストール場所は以下です。
|user|PATH|
|-|-|
|root|/usr/local|
|root以外のuser|~/.local|

これを踏まえたうえで、python実行時に読み込まれるPATHを見てみると以下の様になっていました。
- rootの場合
```shell-session
$ sudo docker run -it -uroot  test bash
root@6156950b997e:/# python
Python 3.11.4 (main, Jun 14 2023, 18:25:14) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/usr/local/lib/python311.zip', '/usr/local/lib/python3.11', '/usr/local/lib/python3.11/lib-dynload', '/usr/local/lib/python3.11/site-packages']
```
- root以外の場合
```shell-session
$ sudo docker run -it -ucacaomath  test bash
[sudo] password for cacaomath: 
cacaomath@b7853c2835f9:/$ python
Python 3.11.4 (main, Jun 14 2023, 18:25:14) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/usr/local/lib/python311.zip', '/usr/local/lib/python3.11', '/usr/local/lib/python3.11/lib-dynload', '/home/cacaomath/.local/lib/python3.11/site-packages', '/usr/local/lib/python3.11/site-packages']
```
root以外のユーザからは`/usr/local`配下と`~/.local`配下が参照されるのに対して、
rootからは`/usr/local`からしか参照されていません。

以上のことから、rootで実行している`sudo docker run -it -uroot test python test.py`は、
実行時にmoduleの`web`がインストールされている`~/.local`(ここでは`/home/cacaomath/.local`)配下のPATHが読み込めず、`ModuleNotFoundError: No module named 'web'`が発生する事態が起きていたようでした。

# 解決方法
今回の例では、rootユーザを使わずに、
`sudo docker run -it -ucacaomath test python test.py`をすれば動くには動くのですが、
実際の環境ではrootを使わないと別のところで不都合が起きるため、`PYTHONPATH`で実行にPATHを渡すことで解決方法としました。
- `docker run`の -eオプションで`PYTHONPATH`を渡す
```shell-session
$ sudo docker run -it -uroot -e PYTHONPATH=/home/cacaomath/.local/lib/python3.11/site-packages test bash
root@f2f75615fb10:/# python
Python 3.11.4 (main, Jun 14 2023, 18:25:14) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/home/cacaomath/.local/lib/python3.11/site-packages', '/usr/local/lib/python311.zip', '/usr/local/lib/python3.11', '/usr/local/lib/python3.11/lib-dynload', '/usr/local/lib/python3.11/site-packages']
```

実際にこうすることでmoduleの読み込みがされ、後続の処理もできていることがわかります。
```shell-session
$ sudo docker run -it -uroot -e PYTHONPATH=/home/cacaomath/.local/lib/python3.11/site-packages test python test.py
web module exists
```

一応、rootを使わない方法
```
$ sudo docker run -it -ucacaomath  test python test.py
web module exists
```

# おわりに
今回は用意されていた手順およびファイルにおいて、PATHが通らない状態で出鼻をくじかれましたが、改めて勉強になったと思います。
それでも、やっぱりPATHは提供されている時点で通っている(or 手順になっている)ほうがいいので、自分で実装する際には考慮できるようにして行きたいと思いました。

# 参考
https://endy-tech.hatenablog.jp/entry/never_execute_sudo_pip

https://docs.docker.jp/engine/reference/commandline/run.html#e-env-env-file