Python3 のビルトイン Web サーバの内部ポートを 80 番にすると Docker コンテナが起動しない
Python3 のビルトインサーバのコンテナを 80 番ポート起動すると、「
PermissionError: [Errno 13] Permission denied
」エラーで起動しない。
「python3 docker PermissionError "Errno 13"」でググっても、ローカルのファイル権限の記事ばかりだったので、自分のググラビリティとして。
TL; DR (今北産業)
-
python
ユーザーなどの非ルート・ユーザーでサーバーを起動していませんか? - 対症療法なら、とりま
USER root
にする。--privileged
で起動されない絶対の自信があるなら、安全ではないがroot
のままでもおk。 - 結局、ポートを 8080 にして実行ユーザーを python のままにしておくのが楽で安全。
TS; DR コンテナが故に root
でも大丈夫な理由とダメな理由のコマケーこと
Python3 の公式コンテナは、ルート権限ユーザーでサーバーを立ち上げないと 80 番ポートが使えません。逆に言えば、Python3 のコンテナは、非ルート権限でサーバーを立ち上げると 80 番ポートが使えなくなります。
しかし、非ルート・ユーザーで 80 番ポートが使えないのは Docker の制限ではなく Linux/UNIX 系 OS のセキュリティ上の仕様制限です。
特権ポート番号(Privileged Ports
1)である 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 @ eclipse.org
- How to run a server on port 80 as a normal user on Linux? @ ServerFault
特権ポート番号とは、Privileged Ports
1(特権ポート)と呼ばれる 0〜1023 の範囲のポート番号のことです。
ウェルノウン・ポート2とも呼ばれます。この範囲のポートを使う場合、実行ユーザーが root
でないと Linux/Unix 系 OS の仕様でポートを開けられない3ため「PermissionError: [Errno 13] Permission denied
」の権限エラーが出ます。逆に言えば、8000 番や 8080 番ポートなら問題ないと言うことです。
実は、Python3 のビルトイン Web サーバーで Docker のコンテナを作ろうと思い、以下の Qiita 記事を読んで写経をしたところ、ストレートに動作しました。
このコンテナに限らず、Python3 の Web サーバーのデフォルトポートは 8000 番です。(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"
4 を記載して起動すると、マシンの 8080 ポートをコンテナの 8000 ポートへポートフォワードしてくれます。つまり、コンテナのポート 8000 番とホストマシンのポート 8080 番をつなげてくれます。
これにより他のパソコンからも http://<ホスト OS の IP>:8080/
でコンテナにアクセスできます。(もちろん cURL
でも)
Docker ネットワーク内では 80 番ポートでコンテナ間通信したかった
🐒 先に最終的に取った方法を言うと、80 番ポートでの統一を諦めて 8080 番ポートで統一させることにしました。コンテナごとに(CentOS, Debian, alpine など) OS が異なるため、設定や管理がむしろ煩雑になってしまったからです。
今回目指したかったのは 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 番以上のポートだと起動します。
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=8000)
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=1024)
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=80)
import http.server
http.server.test(HandlerClass=http.server.CGIHTTPRequestHandler, port=1023)
起動しなかったコンテナのログをみると「PermissionError: [Errno 13] Permission denied
」が発生しています。
$ 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 の仕様制限で、単純に実行ユーザーが異なっていたのが原因でした。
となると、取る方法は2つです。
- 非
root
ユーザーでも 80 番ポートを使えるようにiptables
やipchains
でコンテナ内でポートフォワードする。 -
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」| Issue #62 | uwsgi-nginx-flask-docker @ GitHub より)5
【筆者訳】(一部加筆)
明確な理由がない限り、新しいユーザーを作成し、それらが動作するように無理をして設定を変える必要はありません。root
ユーザーがアクセスできるのは、そのコンテナ内だけだからです。しかしdocker.sock
などのホスト側にアクセスできるようなセンシティブなファイルやディレクトリをマウントしている場合は、その限りではありません。
つまり「--privileged
で起動させない絶対の自信があるなら、root
でもいい」と言うことです。確かに、「コンテナ内で完結できる」という Docker のメリットを考えると一理あります。むしろ、そのため(ホストと隔離させるサンドボックス的)にコンテナを使っているわけですから。
筆者の場合、今のところ、すべてのコンテナは自家製でローカル利用が大半なので root
でも問題ないと思います。
しかし、これはホストのマシンが非 Linux の場合に限ります。つまり macOS や Windows WSL2 上の仮想 Linux マシン上で動かしている場合です。
ホスト OS が Linux の場合は、やはりコンテナ内の実行ユーザーは root
でない方が安全です。と言うのも、Docker 自身は root
権限を必要とするためです。
つまり、コンテナの実行ユーザーも root
の場合、ホストから接続されたもの(コンテナにマウントされたソケット含むファイルやディレクトリ)は、 root
として触れるということになり、意図しないルート(経緯)から意図しない操作を実行される危険性もあるからです。
そのため、自分を信用していないし、色々考えるのも面倒なのでポカよけの意味も込めて 8080 ポートで統一させ、非ルートユーザーで実行させることにしました。
-
「Privileged Ports」@ W3.org ↩ ↩2
-
「ウェルノウンポート番号(0–1023)」| TCPやUDPにおけるポート番号の一覧 @ Wikipedia ↩
-
「PermissionError: [Errno 13] Permission denied Flask.run()」@ StackOverflow ↩
-
「ports | Docker Compose - docker-compose.yml リファレンス(日本語)」 @ Qiita ↩
-
「Permission error on port 80」| Issue #62 | uwsgi-nginx-flask-docker @ GitHub ↩