Docker内Pythonで、JupyterやFlaskを使う開発環境の構築(VSCode/code-server両対応)

Pythonを少し使ってみようと思ったのですが、環境構築するために色々インストールするのは嫌でした。なのでDockerを使って環境を構築してみました。

インタラクティブな環境およびWebの両方を利用できて、VSCode(またはcode-server)で開発できる環境を目指します。特にVSCodeのPython拡張機能からDocker内Pythonで動作するJupyterへの接続にはハマる所がありましたので、環境構築手順を本稿にメモとして残します。

## 要件
- CLIでPythonプログラムが実行できる
- [Jupyter](https://jupyter.org/)を利用して、インタラクティブにPythonが実行できる
- [Flask](https://github.com/pallets/flask)を利用して、WebでPythonのアウトプットができる
- [Visual Studio Code](https://code.visualstudio.com/)または[code\-server](https://github.com/cdr/code-server)で、Pythonのランタイムを利用したコード補完等ができる

上記が全部できる開発環境を構築します。実際に本記事を利用するときは、必要な箇所をかいつまんで利用してください。

### 環境
下記の環境で動作確認済

- Windows 10 WSL 2 Ubuntu 16.04.5 + docker 18.09.6 + docker desktop 2.3.0.3
- ChromeOS Crostini (Linux) Debian 9.11 + docker 19.03.4

## 環境構築手順
まず適当な作業用ディレクトリを切ります。

```bash
$ mkdir python-dev && cd $_
```
または、`$ poetry new`を使っても良いと思います。

```bash
$ docker run --interactive --tty --rm --volume $(pwd):/app --workdir /app python:3.9.5-slim sh
# pip install poetry
# poetry new --src python-dev
$ cd python-dev
```

お好みでGitの初期化をしておきます。

```bash
$ git init .
$ wget https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore --output-document=.gitignore
```
パッケージ管理には[Poetry](https://github.com/python-poetry/poetry)を使います。Poetryでの環境構築用に、Dockerfileとdocker-compose.ymlを記述します。

```Dockerfile:Dockerfile
FROM python:3.9.5-slim
WORKDIR /app

# VS Code Remote Containerを使う場合はGitをコンテナ内にインストールすることを推奨
RUN apt-get update && apt-get install -y \
    git \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

RUN pip install poetry
```
コンテナイメージには使いたいPythonのバージョンが含まれたものを指定します。将来的にはイメージ内に色々な依存パッケージのインストールをしたくなる事を見据えつつ、最初はイメージの容量を軽くしておきたいためslimイメージを使います。

```yml:docker-compose.yml
version: '3.9'
services:
  web:
    build: .
    volumes:
      - .:/app
    environment:
      - FLASK_APP=index.py
    ports:
      - 127.0.0.1:5000:5000
    command: "poetry run flask run --host=0.0.0.0"
  note:
    build: .
    volumes:
      - .:/app
    ports:
      - 127.0.0.1:8888:8888
    command: "poetry run jupyter lab --no-browser --ip=0.0.0.0 --port=8888 --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True"
```
今回はコンテナ内のJupyterにホストからアクセスする都合上、Jupyterのセキュリティ機能を無効化するためにいくつかの設定を入れています。

- `--NotebookApp.token=''`
- `--NotebookApp.password=''`
- `--NotebookApp.disable_check_xsrf=True`

もしコンピュータのローカル環境ではなく公開されたサーバ上で実行するならば、このような設定は避けたほうが良いと思います。

WebサーバやNotebookは使わないという場合も、VS Codeで開発するならコンテナが立ち上がりっぱなしになっていた方が便利です。以下のような`docker-compose.yml`で、起動しっぱなしにできます。

```docker-compose.yml
version: '3'
services:
  app:
    build: .
    volumes:
      - .:/app
    command: /bin/sh -c "echo 'docker dev ready'; while sleep 1000; do :; done"
```

dockerの用意ができたら、Poetryをセットアップします。

```bash
$ docker-compose run --rm web poetry init --no-interaction
$ docker-compose run --rm web poetry add jupyterlab flask
```
Dockerのビルド時に`$ poetry install`するように、コマンドを追加します。

```Dockerfile:Dockerfile
FROM python:3.9.5-slim
WORKDIR /app

RUN pip install poetry

COPY pyproject.toml ./
RUN poetry config virtualenvs.in-project true
RUN poetry install
```
Flaskで立ち上げるためのindex.pyを配置します。

```python:index.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, World!"
```
ビルドして完成です。

```bash
$ docker-compose build
```

### 利用方法
`$ docker-compose up` した後

- CLIで使う
    - `$ docker-compose run --rm web python --version`
- Webで使う
    - http://127.0.0.1:5000 にアクセスします。
- Jupyterで使う
    - http://127.0.0.1:8888 にアクセスします。

## VSCodeでDocker内のPythonを利用する
まずVSCodeに下記をインストールしておきます。

- [Remote Development拡張機能(ms-vscode-remote.vscode-remote-extensionpack)](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack)
- [Docker
拡張機能(ms-azuretools.vscode-docker)](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)

VSCode上のDocker拡張機能で対象のコンテナを選んで「Attach Visual Studio Code」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/1ca076dd-0f8e-ce1d-9827-839706f6a932.png)

※Windowsの場合、[Remote \- WSL拡張機能](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl)でWSL2にRemoteアクセスしているVSCodeではコンテナにアクセスすることができません。Windows上に(Remoteを使っていない)VSCodeのウィンドウを開いて「Attach Visual Studio Code」してください。

コンテナにリモートアクセスするVSCodeウィンドウを開いたら、[Python拡張機能(ms-python.python)](https://marketplace.visualstudio.com/items?itemName=ms-python.python)を「インストール on Container」ボタンでインストールし、VSCodeをReloadします。

または`.vscode/extensions.json`にこのように記述すれば、環境を問わずVS Codeでこのリポジトリを開くと拡張機能のインストールを勧めてくるようになります。

```.vscode/extensions.json
{
  "recommendations": [
    "ms-python.vscode-pylance",
    "ms-python.python"
  ]
}
```

VSCodeの`>Python: Select Interpreter`等で表示されるPythonが、Docker内のもの(今回の例ならば`/usr/local/bin/python`にあるPython 3.9.5)になっていれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/1c600b8a-ab46-0ad8-2585-b274f07251a9.png)

### VSCode上でJupyterを使う
上記方法でコンテナにアタッチしたVSCodeで、`>Python: Specify local or remote Jupyter server for connections`で`Existing`を選択して`http://localhost:8888/`と入力します。
または、`.vscode/settings.json`というファイルを作成します。

```json:.vscode/settings.json
{
  "python.dataScience.jupyterServerURI": "http://localhost:8888/"
}
```

VSCodeをReloadし、`>Python: Create New Blank Jupyter Notebook`することで、VSCode上でJupyterを使うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/8c4f3a1b-768c-5a61-9b83-1be251af6929.png)

パスワードを求められた場合は、空文字のままEnterを入力すればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/76cce908-2467-39b6-3f54-f6bdb4c3bb51.png)

### Pylance拡張機能で静的型チェックをする
VS CodeのPylance拡張機能を使うと、静的型チェックができます。

https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance

インストールしたらお好みで設定します。個人的にはstrict modeで有効化し、`pyrightconfig.json`で各種設定を緩和して使うことが多いです。

```.vscode/settings.json
{
  "python.analysis.typeCheckingMode": "strict"
}
```

```pyrightconfig.json
{
  "reportMissingTypeStubs": "information",
  "reportUnknownParameterType": "information",
  "reportUnknownMemberType": "information",
  "reportUnknownArgumentType":  "information"
}
```

下記のような型エラーが表示されるようになれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/e7b3053c-6ffe-afe6-3136-ac51d0f8651e.png)


## code-serverでDocker内のPythonを利用する
VSCodeではなく[code\-server](https://github.com/cdr/code-server)を使うとブラウザで動作するため、特にChromebookの場合LinuxのGUI環境ではなくChrome上で軽快に動作します。Python等の拡張機能もコンテナ内に含められるため、開発環境ごとコード化できるメリットもあります。

デメリットは、イメージの容量が大きくなります。(約1GB追加で必要)

code-serverを利用するには、コンテナにcode-serverと拡張機能を含めてビルドするようにします。

```Dockerfile:Dockerfile
FROM python:3.9.5-slim AS python
WORKDIR /app

RUN pip install poetry

COPY pyproject.toml ./
RUN poetry config virtualenvs.create false --local
RUN poetry install

FROM python AS code-server

RUN apt-get update && apt-get install -y curl
RUN curl -fsSL https://code-server.dev/install.sh | sh
RUN code-server \
  --install-extension ms-python.python \
  --install-extension ms-ceintl.vscode-language-pack-ja
```
docker-composeにも、code-serverを起動するserviceを追加します。

```yml:docker-compose.yml
version: '3.4'
services:
  web:
    build:
      context: .
      target: python
    volumes:
      - .:/app
    environment:
      - FLASK_APP=index.py
    ports:
      - 127.0.0.1:5000:5000
    command: "poetry run flask run --host=0.0.0.0"
  note:
    build:
      context: .
      target: python
    volumes:
      - .:/app
    ports:
      - 127.0.0.1:8888:8888
    command: "poetry run jupyter notebook --no-browser --ip=0.0.0.0 --port=8888 --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True"
  code:
    build:
      context: .
      target: code-server
    ports:
      - 127.0.0.1:8080:8080
    volumes:
      - ./:/app
    entrypoint: "code-server --auth none --bind-addr=0.0.0.0:8080 /app"
```

これで`$ docker-compose build`すれば完成です。(少し時間がかかります)
http://127.0.0.1:8080 にアクセスすると、ブラウザ上でVSCodeが利用できます。

code-serverでJupyterを使うには、`>Python: Specify local or remote Jupyter server for connections`で`http://note:8888/`のようにjupyterが動いているコンテナを指定します。

```json:.vscode/settings.json
{
  "python.dataScience.jupyterServerURI": "http://note:8888/"
}
```

## 参考文献
- [Unable to connect to an authless remote jupyter server · Issue \#7137 · microsoft/vscode\-python](https://github.com/microsoft/vscode-python/issues/7137)
- [Installing extensions from command line · Issue \#171 · cdr/code\-server](https://github.com/cdr/code-server/issues/171#issuecomment-628200623)

また、本稿にて作成したコードの全文は下記のリポジトリになります。

https://github.com/s2terminal/python-dev