CUBE SUGAR CONTAINER

技術系のこと書きます。

リモートサーバ上の Docker コンテナで JupyterLab を使う

今回のエントリは、以下のエントリの改訂版となる。 起動するアプリケーションを Jupyter Notebook から JupyterLab にすると共に、いくつか変更を加えた。

blog.amedama.jp

JupyterLab は従来の Jupyter Notebook を置き換えることを目的とした後継プロジェクト。 基本的な部分は Notebook を受け継ぎつつも、より扱いやすいインターフェースを提供している。

今回使う環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-62-generic

Docker をインストールする

まずはリポジトリを更新すると共に、過去にインストールした Docker 関連コンポーネントがあれば削除しておく。

$ sudo apt update
$ sudo apt -y remove docker docker-engine docker.io containerd runc

Docker のインストール用リポジトリを APT に登録する。

$ sudo apt -y install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common \
    python3-pip
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt update

Docker の主要コンポーネントをインストールする。

$ sudo apt -y install docker-ce docker-ce-cli containerd.io
$ sudo docker version
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:29:11 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.8
  Git commit:       6a30dfc
  Built:            Thu Aug 29 05:27:45 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

ついでに Docker Compose もインストールしておく。

$ sudo pip3 install docker-compose
$ docker-compose version
docker-compose version 1.24.1, build 4667896
docker-py version: 3.7.3
CPython version: 3.6.8
OpenSSL version: OpenSSL 1.1.1  11 Sep 2018

JupyterLab を起動するイメージを用意する

今回はデーモンを Supervisord 経由で起動することにした。 JupyterLab だけを起動するなら別に直接起動してもいいんだけど、拡張性などを考えて。 %% で囲まれている部分は後で環境変数経由で置き換える。

$ cat << 'EOF' > supervisord.conf
[supervisord]
nodaemon=true

[program:jupyterlab]
command=%%CONDA_HOME%%/envs/%%CONDA_VENV_NAME%%/bin/jupyter-lab
user=%%USERNAME%%
stdout_logfile=/var/log/supervisor/jupyter-lab.log
redirect_stderr=true
autostart=True
autorestart=True
EOF

続いて Supervisord を起動するエントリーポイントとなるシェルスクリプト。 この中では主に設定ファイルの準備をしている。 JupyterLab の Web UI にトークンなしでアクセスできるようにするなど。 JupyterLab の Web UI には SSH Port Forwarding 経由でアクセスするため、アクセス制御については SSH の部分で担保する。

$ cat << 'EOF' > docker-entrypoint.sh
#!/usr/bin/env bash

set -Ceux

: "Set home directory" && {
  export HOME=/home/${USERNAME}
}

: "Replace template variables" && {
  sed -i.back \
    -e "s:%%USERNAME%%:${USERNAME}:g" \
    -e "s:%%CONDA_HOME%%:${CONDA_HOME}:g" \
    -e "s:%%CONDA_VENV_NAME%%:${CONDA_VENV_NAME}:g" \
  /etc/supervisord.conf
}

: "Jupyter configuration" && {
  if [ ! -e /home/${USERNAME}/.jupyter ]; then
    sudo -iu ${USERNAME} ${CONDA_HOME}/envs/${CONDA_VENV_NAME}/bin/jupyter notebook --generate-config
    sudo -iu ${USERNAME} sed -i.back \
      -e "s:^#c.NotebookApp.token = .*$:c.NotebookApp.token = u'':" \
      -e "s:^#c.NotebookApp.ip = .*$:c.NotebookApp.ip = '*':" \
      -e "s:^#c.NotebookApp.open_browser = .*$:c.NotebookApp.open_browser = False:" \
      -e "s:^#c.NotebookApp.notebook_dir = .*$:c.NotebookApp.notebook_dir = \"${JUPYTER_HOME}\":" \
      /home/${USERNAME}/.jupyter/jupyter_notebook_config.py
  fi
}

: "Start supervisord" && {
  supervisord -c /etc/supervisord.conf
}
EOF

インストールしたい Python のパッケージの一覧を書いておくファイル。

$ cat << 'EOF' > requirements.txt
jupyterlab
EOF

続いては、肝心の Dockerfile となる。 Python の実行環境については Miniconda でやってしまうことにした。 インストール先や仮想環境は CONDA_HOMECONDA_VENV_NAME で変更できる。

$ cat << 'EOF' > Dockerfile
FROM ubuntu:18.04

ENV CONDA_HOME=/opt/conda \
    CONDA_VENV_NAME=venv \
    JUPYTER_HOME=/mnt/jupyter

# Use mirror repository
RUN sed -i.bak -e "s%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g" /etc/apt/sources.list

# Install prerequisite packages
RUN apt update \
 && apt -yq dist-upgrade \
 && apt install -yq --no-install-recommends \
      sudo \
      supervisor \
      ca-certificates \
      wget

# Install miniconda
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda3.sh \
 && /bin/bash /tmp/miniconda3.sh -b -p ${CONDA_HOME} \
 && rm /tmp/miniconda3.sh \
 && ${CONDA_HOME}/bin/conda clean -tipsy \
 && ln -s ${CONDA_HOME}/etc/profile.d/conda.sh /etc/profile.d/

# Create virtual environment
COPY requirements.txt /tmp
RUN . /etc/profile.d/conda.sh \
 && conda create -y -n ${CONDA_VENV_NAME} python=3.7 \
 && conda activate ${CONDA_VENV_NAME} \
 && pip install -r /tmp/requirements.txt

# Expose container ports
EXPOSE 8888

# Boot process
COPY supervisord.conf /etc
COPY docker-entrypoint.sh /var/tmp
CMD bash /var/tmp/docker-entrypoint.sh
EOF

起動するときのオプションがなるべく増えないように Docker Compose の設定ファイルも作っておく。 以下では docker-compose.yml のあるディレクトリを JupyterLab の作業ディレクトリとして /mnt/jupyter にマウントしている。 必要に応じて場所は書き換えると良いと思う。

$ cat << 'EOF' > docker-compose.yml
version: "3"
services:
  jupyter-lab:
    build:
      context: .
    environment:
      - USERNAME
      - JUPYTER_HOME=/mnt/jupyter
    image: example/jupyterlab-container
    volumes:
      - /etc/passwd:/etc/passwd:ro
      - /etc/group:/etc/group:ro
      - /home:/home
      - ./:/mnt/jupyter
    ports:
      - "127.0.0.1:8888:8888"
EOF

また、パーミッション問題を避けるために上記では /etc/passwd/etc/group をコンテナにマウントしてしまっている。 このテクニックについては以下を参照のこと。

blog.amedama.jp

準備ができたらイメージをビルドする。

$ sudo docker-compose build

コンテナを起動する

コンテナを起動する前に、念のため SSH に使うポート以外をファイアウォールで閉じておく。 コンテナが bind するアドレスはループバックアドレスなのでインターネットには晒されないけど念のため。

$ sudo ufw allow 22
$ sudo ufw default DENY
$ yes | sudo ufw enable
$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22                         ALLOW       Anywhere                  
22 (v6)                    ALLOW       Anywhere (v6)             

JupyterLab のコンテナを起動する。

$ sudo docker-compose run \
    --name jupyter-lab-container \
    --rm \
    -e USERNAME=$(whoami) \
    --service-ports \
    jupyter-lab

起動ログにエラーがなく、次のように 127.0.0.1:8888 で Listen していれば上手くいっている。

$ ss -tlnp | grep 8888
LISTEN   0         128               127.0.0.1:8888             0.0.0.0:*       

JupyterLab の Web UI にアクセスする

これでリモートサーバ上では JupyterLab が動いたけどループバックアドレスなので疎通がない。 そこで SSH Port Forwarding を使ってリモートサーバ上のポートをローカルに引き出してくる。

今回は Vagrant を使って環境を作っているので次のようにする。

$ vagrant ssh-config > ssh.config
$ ssh -F ssh.config -L 8888:localhost:8888 default

ブラウザで localhost:8888 にアクセスする。

$ open http://localhost:8888

以下のような Web UI が見えればおっけー。 Jupyter Notebook よりも、ちょっと IDE っぽい見た目。

f:id:momijiame:20190920031044p:plain

いじょう。