Python3 の Docker コンテナで PermissionError: [Errno 13] Permission denied on Docker

<h2>Python3 のビルトイン Web サーバの内部ポートを 80 番にすると Docker コンテナが起動しない</h2>

> Python3 のビルトインサーバのコンテナを 80 番ポート起動すると、「`PermissionError: [Errno 13] Permission denied`」エラーで起動しない。

[「python3 docker PermissionError "Errno 13"」でググって](https://www.google.com/search?q=python3+docker+PermissionError+%22Errno+13%22)も、ローカルのファイル権限の記事ばかりだったので、自分のググラビリティとして。

## TL; DR <sub><sup><sub><sup>（今北産業）</sup></sub></sup></sub>

1. `python` ユーザーなどの非ルート・ユーザーでサーバーを起動していませんか？
2. 対症療法なら、とりま `USER root` にする。<br><sub><sup>`--privileged` で起動されない絶対の自信があるなら、安全ではないが `root` のままでもおk</sup></sub>。
3. 結局、ポートを 8080 にして実行ユーザーを python のままにしておくのが<ruby>楽<rt>らく</rt></ruby>で安全。

## TS; DR <sub><sup><sub><sup>コンテナが故に `root` でも大丈夫な理由とダメな理由のコマケーこと</sup></sub></sup></sub>

Python3 の公式コンテナは、ルート権限ユーザーでサーバーを立ち上げないと 80 番ポートが使えません。逆に言えば、Python3 のコンテナは、**非ルート権限でサーバーを立ち上げると 80 番ポートが使えなくなります**。

しかし、非ルート・ユーザーで 80 番ポートが使えないのは Docker の制限ではなく **Linux/UNIX 系 OS のセキュリティ上の仕様制限**です。

特権ポート番号（`Privileged Ports`[^w3]）である 80 番ポートは `root` 権限ユーザーでないと開けられません（特権ポートについては後述）。

Python3 の公式コンテナは、デフォルトでユーザーが `python`（`USER python`） に設定されています。そのため、一番簡単な方法は、`USER` を明示的に `root` に指定することです。Dockerfile の下部に `USER root` ディレクティブを指定すると 80 番ポートが利用可能になります。

セキュリティの観点から非 root ユーザでコンテナを起動する必要がある場合は、`ipchains` や `iptables` などで 80 番ポートを 8080 番ポートなどに転送する必要があります。その場合の設定は、コンテナのベースとなる OS に依存します。

- [Setting Port 80 Access for a Non-Root User](https://www.eclipse.org/jetty/documentation/current/setting-port80-access.html) @ eclipse.org
- [How to run a server on port 80 as a normal user on Linux?](https://serverfault.com/questions/112795/how-to-run-a-server-on-port-80-as-a-normal-user-on-linux) @ ServerFault

**特権ポート番号とは**、`Privileged Ports`[^w3]（特権ポート）と呼ばれる 0〜1023 の範囲のポート番号のことです。

ウェルノウン・ポート[^wiki]とも呼ばれます。この範囲のポートを使う場合、**実行ユーザーが `root` でないと Linux/Unix 系 OS の仕様でポートを開けられない**[^sof1]ため「`PermissionError: [Errno 13] Permission denied`」の権限エラーが出ます。逆に言えば、8000 番や 8080 番ポートなら問題ないと言うことです。

実は、Python3 のビルトイン Web サーバーで Docker のコンテナを作ろうと思い、以下の Qiita 記事を読んで写経をしたところ、ストレートに動作しました。

- 「[Python3 で動作する CGIServer を Docker 上に構築](https://qiita.com/RyosukeKamei/items/61d4c5af7d8ffcf49ad3)」@ Qiita 

このコンテナに限らず、Python3 の Web サーバーの[デフォルトポートは 8000 番](https://github.com/python/cpython/blob/7f193e89698818de5fc60a8667de786036bbef95/Lib/http/server.py#L1252-L1255)です。（Python v3.7.4rc2 現在）

そのため、今回のこの Python3 コンテナ側の 8000 ポートに `http://<コンテナ名>:8000/` で他のコンテナから http アクセスできます。

次にホスト側、つまり Docker ネットワークの外からコンテナにアクセスしたい場合、コンテナを起動時に `-p` もしくは `--publish` オプションでポートを公開します。（`--ports` ではないので注意 → 私）

具体的には `docker run -p 8080:8000 -d <YOUR DOCKER IMAGE>` でコンテナを起動、もしくは docker-compose.yml の場合は `ports: "8080:8000"`[^ports] を記載して起動すると、マシンの 8080 ポートをコンテナの 8000 ポートへポートフォワードしてくれます。つまり、コンテナのポート 8000 番とホストマシンのポート 8080 番をつなげてくれます。

これにより他のパソコンからも `http://<ホスト OS の IP>:8080/` でコンテナにアクセスできます。（もちろん `cURL`でも）

[^ports]: 「[ports](https://qiita.com/zembutsu/items/9e9d80e05e36e882caaa#ports) | Docker Compose - docker-compose.yml リファレンス（日本語）」 @ Qiita

### Docker ネットワーク内では 80 番ポートでコンテナ間通信したかった

> <sub>🐒 &nbsp; 先に最終的に取った方法を言うと、**80 番ポートでの統一を諦めて 8080 番ポートで統一させることにしました**。コンテナごとに（CentOS, Debian, alpine など） OS が異なるため、設定や管理がむしろ煩雑になってしまったからです。</sub>

今回目指したかったのは Docker ネットワーク内で完結、つまりホスト含め外部にさらす必要のない構成です。

`docker-compose up` すると複数コンテナが同じ Docker ネットワーク内で起動します。そして、`exec` コマンドでメインのコンテナにアクセスすると、コンテナ間で http 通信を行ったのち処理結果だけをコンソール（ターミナル）の標準出力に表示させたいのです。

各々のコンテナは PHP7、Golang、Python3 などで書かれた「目的別のシンプルな機能だけを提供している RESTful なコンテナ」です。メインのコンテナは、各コンテナに http リクエストした結果をとりまとめて結果だけを吐き出すイメージです。

その際、**仕様をシンプルにするため各々のコンテナのポートを 80 番に固定することで、`http://<コンテナ名>/` だけでお互いのコンテナがアクセスできるようにしたかった**のです。（同じ Docker ネットワークにいるコンテナ同士に限る）

しかし、PHP7 のビルトイン Web サーバーのコンテナへは `http://<PHPコンテナ名>:80/` で接続できるのに、Python3 のビルトイン Web サーバーの場合、コンテナの内側ポートを 80 番に設定すると「`PermissionError: [Errno 13] Permission denied`」エラーでコンテナが起動しません。デフォルトの 8000 番ポートや 1024 番以上のポートだと起動します。

```python:✅起動する設定
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=8000)
```
```python:✅起動する設定
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=1024)
```
```python:❌起動しない設定
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=80)
```
```python:❌起動しない設定
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=1023)
```

起動しなかったコンテナのログをみると「`PermissionError: [Errno 13] Permission denied`」が発生しています。

```shell-session:起動したコンテナのlogs
$ docker container logs sample_container
Traceback (most recent call last):
  File "/server_start.py", line 3, in <module>
    http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=81)
  File "/usr/local/lib/python3.6/http/server.py", line 1185, in test
    with ServerClass(server_address, HandlerClass) as httpd:
  File "/usr/local/lib/python3.6/socketserver.py", line 453, in __init__
    self.server_bind()
  File "/usr/local/lib/python3.6/http/server.py", line 136, in server_bind
    socketserver.TCPServer.server_bind(self)
  File "/usr/local/lib/python3.6/socketserver.py", line 467, in server_bind
    self.socket.bind(self.server_address)
PermissionError: [Errno 13] Permission denied
```

「`Permission`？え？権限？」と思い調べてみると、PHP7 のビルトインサーバーは `root` ユーザーで実行しているのに対し、Python3 のビルトインサーバーは Dockerfile 内で `python` ユーザーを指定していました。ユーザーを `root` に変更したところ普通に動きました。

「えー、python って `root` じゃないと 80 番ポート使えないのー」と思ったのですが、どうやら**プログラム言語の仕様制限ではなく OS の仕様制限で、単純に実行ユーザーが異なっていたのが原因**でした。

となると、取る方法は２つです。

1. 非 `root` ユーザーでも 80 番ポートを使えるように `iptables` や `ipchains` でコンテナ内でポートフォワードする。
2. `root` ユーザーでコンテナを起動させる。

しかし「`root` ユーザーで起動させるとセキュリティ的に大丈夫なのか」という心配も出てきました。

>> You don't need to add a new username and change everything to adapt it to that unless there's a clear reason to do so. But as the `root` user inside the container only has access to that container, there's is nothing that user can risk, unless you mount a privileged directory or something similar.
>> 
>> （「[Permission error on port 80](https://github.com/tiangolo/uwsgi-nginx-flask-docker/issues/62#issuecomment-394679706)」| Issue #62 | uwsgi-nginx-flask-docker @ GitHub より）[^flask]
>
> 【筆者訳】（一部加筆）
> 明確な理由がない限り、新しいユーザーを作成し、それらが動作するように無理をして設定を変える必要はありません。`root` ユーザーがアクセスできるのは、そのコンテナ内だけだからです。しかし `docker.sock` などのホスト側にアクセスできるようなセンシティブなファイルやディレクトリをマウントしている場合は、その限りではありません。

つまり「`--privileged` で起動させない絶対の自信があるなら、`root` でもいい」と言うことです。確かに、「コンテナ内で完結できる」という Docker のメリットを考えると一理あります。むしろ、そのため（ホストと隔離させるサンドボックス的）にコンテナを使っているわけですから。

筆者の場合、今のところ、すべてのコンテナは自家製でローカル利用が大半なので `root` でも問題ないと思います。

しかし、これは**ホストのマシンが<ruby>非<rt>・</rt></ruby> Linux の場合に限ります**。つまり macOS や Windows WSL2 上の仮想 Linux マシン上で動かしている場合です。

<font color="darkorange">ホスト OS が Linux の場合は、やはりコンテナ内の実行ユーザーは `root` でない方が安全</font>です。と言うのも、Docker 自身は `root` 権限を必要とするためです。

つまり、コンテナの実行ユーザーも `root` の場合、ホストから接続されたもの（コンテナにマウントされたソケット含むファイルやディレクトリ）は、 `root` として触れるということになり、意図しないルート（経緯）から意図しない操作を実行される危険性もあるからです。

そのため、自分を信用していないし、色々考えるのも面倒なので[ポカよけ](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%82%AB%E3%83%A8%E3%82%B1)の意味も込めて 8080 ポートで統一させ、非ルートユーザーで実行させることにしました。

[^flask]: 「[Permission error on port 80](https://github.com/tiangolo/uwsgi-nginx-flask-docker/issues/62#issuecomment-394679706)」| Issue #62 | uwsgi-nginx-flask-docker @ GitHub

[^w3]: 「[Privileged Ports](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html)」@ W3.org

[^wiki]: 「[ウェルノウンポート番号（0–1023）](https://ja.wikipedia.org/wiki/TCP%E3%82%84UDP%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7%E3%81%AE%E4%B8%80%E8%A6%A7#%E3%82%A6%E3%82%A7%E3%83%AB%E3%83%8E%E3%82%A6%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7_(0%E2%80%931023))」| TCPやUDPにおけるポート番号の一覧 @ Wikipedia

[^sof1]: 「[PermissionError: [Errno 13] Permission denied Flask.run()](https://stackoverflow.com/questions/38298652/permissionerror-errno-13-permission-denied-flask-run)」@ StackOverflow