はじめに
コンピュータ上に仮想化環境を構築して、異なるOSやWebサービスのテストや運用をすることはすでに一般的で、複数ある選択肢のうちDockerに興味があったので使い始めました。
参考:仮想環境についてまとめてみる(https://qiita.com/9en/items/f4eab2f61485a9f3885a)
Dockerでは、仮想化環境の構成要素を設定ファイルとして記述して環境構築を自動化します。そのため、Dockerが動いている他のマシンと設定ファイルを共有することで同じ仮想化環境を構築できるほか、同じ設定をCloudにデプロイしてローカルで試行した環境をほぼそのままCloudサーバ上に構築するといった使い方ができることもDockerの魅力だと思います。
しかしながら、Dockerを使用すればホストマシンによらず同じ環境を構築することができるかというと、必ずしもそうではなく、特にDocker for Windowsの問題なのかもしれませんが、Volumeがうまく機能しない場合があるなど、他のサイトで紹介されているすべての内容が同じように動くとは限らないことがわかってきました。
今回、Docker for Windowsを利用して下図のような作業用Linux環境(Debian)とローカルマシン上で動作する2種類(NginxベースとNode.jsベース)のWebサーバ環境を構築し、ある程度まともに動くようになったので、これまでに得たノウハウをこのページに記します。
Docker for Windowsのインストール
Docker for Windowsの前提条件:
Windows 10 Pro または Enterprise and Education
以下のページを参考にDocker for Windowsをインストールしてアカウントを作成します。
- https://docs.docker.com/docker-for-windows/install/
- Docker for WindowsをWindows10 Proにインストール
- Docker for Windows をインストールしてdocker-composeを動かすまで
Dockerの起動
インストールできたらデスクトップにクジラのショートカットができていると思います。
このショートカットからDockerを起動するか、または、デフォルトで以下のような場所にインストールされていると思いますので、exeファイルを実行します。
C:¥Program Files¥Docker¥Docker¥Docker for Windows.exe
インストール直後はSettingsの「Start Docker Desktop when you log in」にチェックが入っていてPCを起動するとDockerが立ち上がる状態でしたが、使う時だけDockerを立ち上げたかったので、チェックを外しました。後述の「エラー対応」に記載したようにWindowsを終了する前にDockerを終了しておかないと次回起動時にうまく動かないことがあったことも自動起動をOFFにした理由の1つです。
Dockerが正常に起動していれば、コマンドプロンプトやPowerShellから以下のコマンドを実行するとヘルプが表示されるはずです。
docker --help
設定ファイルの準備
任意の場所に作業フォルダを作成して、そこに以下の設定ファイルを作成していきます。
- docker-compose設定ファイル: docker-compose.yml
Dockerの環境構築を楽にしてくれるDockerのラッパーコマンドdocker-composeの設定ファイル - Docker設定ファイル: Dockerfile
Dockerによる環境構築内容を指定する設定ファイル
docker-composeを使うと多数のDockerのオプションを設定として記述でき、ビルドや起動が楽になるのでおすすめです。
フォルダ構成
docker/ ├─ docker-compose.yml ├─ base/ │ └─ Dockerfile ├─ work/ ├─ data/ │ ├─ html/ │ │ ├─ disp.js │ │ ├─ get.php │ │ ├─ index.html │ │ └─ style.css │ └─ node/ │ ├─ app.js │ └─ views/ │ └─ index.ejs ├─ nginx/ │ └─ default.conf ├─ node/ │ └─ Dockerfile └─ postgres/ ├─ createdb.sql ├─ Dockerfile └─ postgresql.conf
以降でファイルの内容を紹介しますが、上記全てのファイルの中身を記載しておりません。
GitHubに全ファイルを置いていますので、紹介していないファイルの内容を知りたい場合はご利用ください。
docker-compose.ymlの例
docker-compose.ymlはインデント(字下げ)で階層構造を表すYAML形式の設定ファイルです。
Dockerではデフォルト状態ではサービスに割り振られるIPアドレスが毎度変わってしまいますが、別のサービスから接続する際にURLが変わってしまうと不便なため、一部のサービスのIPアドレスが固定されるNetwork設定にしています。
version: "3.4" services: base: build: base container_name: base command: /usr/sbin/sshd -D ports: - "49944:22" - "50080:8080" volumes: - type: bind source: ./work target: /work - type: bind source: ./data target: /data networks: mynet: ipv4_address: 172.18.0.10 nginx: image: nginx:1.13.5-alpine container_name: nginx ports: - "80:80" depends_on: - php volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf - ./data/html:/var/www/html php: image: php:7.1.9-fpm container_name: php volumes: - ./data/html:/var/www/html node: build: ./node container_name: node volumes: - ./data/node/app.js:/src/app.js - ./data/node/views:/src/views working_dir: /src ports: - "3000:3000" networks: mynet: ipv4_address: 172.18.0.4 depends_on: - postgres entrypoint: - node - app postgres: build: ./postgres container_name: pg environment: POSTGRES_DB: mydb POSTGRES_USER: dbuser POSTGRES_PASSWORD: dbpass ports: - 5432:5432 volumes: - pgdb:/var/lib/postgresql/data networks: mynet: ipv4_address: 172.18.0.2 volumes: pgdb: external: false networks: mynet: ipam: config: - subnet: 172.18.0.0/16
49152~65535はユーザが自由に使えるポート番号です。netstatコマンド等で使用状況を調べ、空いている番号を選択するのが無難です。
Dockerfileの例
DockerfileはDockerがイメージを作る際の設定ファイルになります。
base/Dockerfile:
FROM python:3.6 ARG USER=user ARG GROUP=developer ARG PASS=password ENV DEBIAN_FRONTEND noninteractive ENV DEBCONF_NOWARNINGS yes # Common RUN apt-get update && apt-get install -y \ apt-transport-https \ libasound2-dev \ bash-completion \ build-essential \ bzip2 \ cmake \ curl \ gcc \ g++ \ git \ less \ libatlas-base-dev \ libgl1-mesa-dev \ locales \ make \ man \ manpages-dev \ mosquitto-clients \ net-tools \ openssh-server \ openssh-client \ p7zip \ postgresql-client \ software-properties-common \ sudo \ unzip \ vim \ wget \ xorg-dev \ zlib1g-dev \ zsh # Node-js RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - RUN apt-get update && apt-get install -y nodejs # Heroku CLI RUN npm install -g heroku RUN groupadd -g 1000 $GROUP RUN useradd -g $GROUP -G sudo -m -s /bin/bash $USER RUN mkdir /var/run/sshd RUN echo "${USER}:${PASS}" | chpasswd RUN echo "root:${PASS}" | chpasswd # SSH settings. Otherwise user is kicked off after login RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd RUN sed -e 's@#Port 22@Port 22@' -e 's@#AddressFamily any@AddressFamily inet@' -i /etc/ssh/sshd_config # OpenCV and Tensorflow RUN ln -s /usr/include/libv4l1-videodev.h /usr/include/linux/videodev.h RUN mkdir /tmp_cv RUN cd /tmp_cv && wget https://github.com/Itseez/opencv/archive/3.1.0.zip && unzip 3.1.0.zip RUN cd /tmp_cv/opencv-3.1.0 && cmake CMakeLists.txt -DWITH_TBB=ON \ -DINSTALL_CREATE_DISTRIB=ON \ -DWITH_FFMPEG=OFF \ -DWITH_IPP=OFF \ -DCMAKE_INSTALL_PREFIX=/usr/local RUN cd /tmp_cv/opencv-3.1.0 && make -j2 && make install # Note: The latest tornado(6.0.1) caused a problem not to connect to jupyter kernel; tornado==5.1.1 is a W/A. RUN pip3 install numpy tensorflow opencv-python Pillow scipy matplotlib pandas keras jupyter tornado==5.1.1 # Go 1.11 RUN mkdir /tmp_go RUN apt-get update && apt-get -y upgrade && \ cd /tmp_go && \ wget https://dl.google.com/go/go1.11.4.linux-amd64.tar.gz && \ tar xvf go1.11.4.linux-amd64.tar.gz && \ mv go /usr/local #### User #### USER $USER WORKDIR /home/$USER # Settings RUN echo 'alias ls="ls -a --color=auto --show-control-chars --time-style=long-iso -FH"' >> /home/$USER/.profile RUN echo 'alias ll="ls -a -lA"' >> /home/$USER/.profile RUN echo 'alias h=history' >> /home/$USER/.profile RUN echo 'alias vi=vim' >> /home/$USER/.profile RUN echo 'alias jupyter_notebook="jupyter notebook --ip=0.0.0.0 --port=8080"' >> /home/$USER/.profile RUN echo 'export GOROOT="/usr/local/go"' >> /home/$USER/.profile RUN echo 'export GOPATH="/work/go"' >> /home/$USER/.profile RUN echo 'export PATH="/work/bin:$GOPATH/bin:$GOROOT/bin:$PATH"' >> /home/$USER/.profile RUN echo 'PS1="\$ "' >> /home/$USER/.bashrc RUN echo 'set background=dark' > /home/$USER/.vimrc RUN echo 'syntax on' >> /home/$USER/.vimrc RUN touch /home/$USER/.Xauthority && chmod 600 /home/$USER/.Xauthority USER root #ADD .netrc /home/$USER #ADD .gitconfig /home/$USER #RUN chown $USER:$GROUP /home/$USER/.netrc /home/$USER/.gitconfig #RUN chmod 644 /home/$USER/.netrc /home/$USER/.gitconfig EXPOSE 22 EXPOSE 8080
ファイル先頭付近で設定しているARG変数USER, GROUP, PASSはお好みの内容に変えてください。
環境変数DISPLAYを設定していませんが、Dockerでは何も設定せずともデフォルトでホストマシンのDISPLAYが設定されるようです。
上記DockerfileにはGo言語やHeroku CLIなどの設定も含んでいますが、本記事の中では触れていません。
php/Dockerfile:
FROM php:7.1.9-fpm ADD php.ini /usr/local/etc/php/conf.d/php.ini WORKDIR /tmp # For composer RUN apt-get update \ && apt-get install -y apt-utils libzip-dev zlib1g-dev \ && docker-php-ext-install zip # Copy composer COPY --from=composer:1.5 /usr/bin/composer /usr/bin/composer # Set composer environment ENV COMPOSER_ALLOW_SUPERUSER 1 ENV COMPOSER_HOME /composer ENV PATH $PATH:/composer/vendor/bin # Install laravel installer RUN composer global require "laravel/installer" # PHP's DB setting RUN apt-get update \ && apt-get install -y libpq-dev \ && docker-php-ext-install pdo_mysql pdo_pgsql mysqli RUN apt-get update \ && apt-get install -y libmemcached-dev zlib1g-dev \ && pecl install memcached-3.0.3 \ && docker-php-ext-enable memcached opcache # Install Node.js RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \ && apt-get update \ && apt-get install -y nodejs
node/Dockerfile:
FROM node:10.15-alpine RUN mkdir /src WORKDIR /src RUN echo {} > package.json RUN npm install --save express && \ npm install --save ejs && \ npm install --save pg EXPOSE 3000
postgres/Dockerfile:
FROM postgres:alpine COPY ./postgresql.conf /var/lib/postgresql COPY ./createdb.sql /var/lib/postgresql EXPOSE 5432 CMD ["postgres", "-D", "/var/lib/postgresql/data", "-c", "config_file=/var/lib/postgresql/postgresql.conf"]
環境変数PATHなど、コンテナ内でも環境変数として利用したい値はENVを使い、ビルド時にのみ利用したい値はARGを使うと良いです。
画面表示を伴うプログラムも使えるよう、base環境はssh接続+X11フォワーディングでGUI表示を実現しています。これ以外にVNC系のツールでブラウザなどに画面を飛ばす方法も試しましたが、私は前者が使いやすかったので採用しました。
docker-composeコマンド
通常はdocker-composeコマンドで操作するのが便利です。以下によく使うコマンドを紹介します。
コマンド | 説明 | 補足 |
---|---|---|
docker-compose build | サービスの構築 | 単にdocker-compose buildとすると、docker-compose.ymlに定義したすべてのサービスをビルドします。 ただし、Dockerfileを使わない既存イメージそのままのサービスはビルドされず、up時にpull(ダウンロード)されます。 docker-compose build サービス名 とすると指定したサービスをビルドします。 |
docker-compose up | コンテナの作成と開始 | 単にdocker-compose upとすると、docker-compose.ymlに定義したすべてのサービスのコンテナを作成・開始します。 docker-compose up サービス名 とすると指定したサービスのコンテナを作成・開始します。 ビルドしていないサービスはビルドしてからコンテナ作成・開始されます。 docker-compose up -dまたはdocker-compose up -d サービス名 とするとバックグラウンドで実行されます。 docker-composeで複数のサービスを対象にする場合、-d を付けてバックグラウンド実行するのが基本になると思います。 |
docker-compose down | コンテナの停止と削除 | docker-compose.ymlに定義したサービスのコンテナを停止・削除します。 |
dockerコマンド
dockerコマンドでなければ実施できない操作がありますので、以下を覚えておくと良いでしょう。
コマンド | 説明 | 補足 |
---|---|---|
docker ps | コンテナの一覧表示 | 単にdocker psとすると、起動中のコンテナ一覧が表示されます。 docker ps -aとすると、停止中のコンテナも表示されます。 |
docker run | コンテナの作成・開始・接続 | docker run --rm -v /c/Users/user/docker/work:/work -p 49944:22 -it IMAGE-ID bash のようにvolumeとポートを指定して実行することが多い。 -vで相対パス(./data/workなど)を使用するとDocker for Windowsではエラーになるので絶対パスで書きます。 |
docker start CONTAINER-ID | コンテナの開始 | 指定したIDのコンテナを開始します。 |
docker éxec CONTAINER-ID ※ | コマンド実行 | 指定したIDの実行中コンテナ内で新たにコマンドを実行します。 |
docker attach CONTAINER-ID | コンテナへの接続 | 指定したIDのコンテナへ接続します。 |
docker stop CONTAINER-ID | コンテナの停止 | 指定したIDのコンテナを停止します。 |
docker rm CONTAINER-ID | コンテナの削除 | 指定したIDのコンテナを削除します。 |
docker rmi IMAGE-ID | イメージの削除 | 指定したIDのイメージを削除します。 |
docker volume ls | Volumeを表示 | Volumeの一覧を表示します。 |
docker volume create VOLUME-NAME | Volumeを作成 | 新規Volumeを作成します。 |
docker volume inspect VOLUME-NAME | Volumeの情報表示 | 指定した名前のVolumeの情報を表示します。 |
docker volume rm VOLUME-NAME | Volumeを削除 | 指定した名前のVolumeを削除します。 |
docker volume prune | Volumeの実体を削除 | docker volume rmではVolumeの実体は消えず、また同名でdocker volume createするとデータが復活します。実体を消去するためにこのコマンドを使います。 |
docker network ls | Networkを表示 | Networkの一覧を表示します。 |
docker network create NETWORK-NAME | Networkを作成 | 新規Networkを作成します。 |
docker network inspect NETWORK-NAME | Networkの情報表示 | 指定した名前のNetworkの情報を表示します。 |
docker network rm NETWORK-NAME | Networkを削除 | 指定した名前のNetworkを削除します。 |
docker logs [-f] CONTAINER-NAME | コンテナのログを参照する | 指定した名前のコンテナのログを参照します。-fを付けると最新のログを待ち受ける状態になります(tail -fと同様)。 |
※ WordPressのTablepressプラグインではテーブル内にexeという文字列を書けないようで、eの代わりにéを使って表現していますが、実際は通常のeを使ってdocker execとしてください。
Volumeについて
Dockerでは起動したコンテナ上で作成したファイルは、コンテナを停止すると消えてしまいますが、データを永続化するためにVolumeという仕組みが用意されています。Volumeには以下の2種類があります。
①ホストマシン(ここではWindows)のディレクトリやファイルをマウントしてファイルを共有。ホストマシンから参照したり、ファイルを変更することが可能。
②データVolumeとしてdockerが管理するデータ領域を確保し、コンテナ上のディレクトリにマウントする。ホストマシンから直接中身を参照できない。
ただし、やっかいなのがVolumeとしてマウントしたディレクトリはroot権限になってしまいます(これはどうやらDocker for Windowsだけのようです)。
ホームディレクトリは環境立ち上げ後に各種ツールの設定を変更したり、historyを残したりしたいと思い、当初、ユーザのホームディレクトリをVolume指定しようと試みたのですが、vimを閉じる際に.viminfoのオーナーがrootのため、.viminfoへの自動書き込みに失敗するといった問題が生じました。
これを回避する良い方法が見つけられず、別途作成したVolumeディレクトリにホームのファイルとパーミッションをセーブしたりロードするシェルスクリプトを書いてバックアップするようにしていました。
しかし、この自力バックアップ方式も次第に使わなくなっていきました。結局ホームディレクトリの状態を更新したくなるのは、インストールするツールを増やしたときであり、そのときはコンテナをbuildし直すので、Dockerfileで.profileなどに書き足したり、ADDでファイルを置いてchown, chmodする方法に落ち着きました。.bash_historyのコマンド履歴がコンテナ停止時に無くなることは、私にとってはさほど重要ではありませんでした。
実行確認
以降、構築した環境が正しく動作するか確認した内容を記載します。確認のレベルがそれぞれ異なることについては、ご容赦ください。
Dockerコンテナの起動
PowerShellでdocker-compose.ymlファイルを置いたフォルダに移動し、docker-compose up -d コマンドを使ってDockerコンテナを起動します。
PS> docker-compose up -d Creating network "docker_mynet" with the default driver Creating network "docker_default" with the default driver Creating php ... done Creating base ... done Creating pg ... done Creating node ... done Creating nginx ... done
次にMobaXtermやTera Termなどを使ってbase環境にssh接続を行います。
MobaXtermの場合、起動すると下図のウィンドウが立ち上がります。下図はbase接続を設定してから画面キャプチャーしたものになります。一度設定するとRecent Sessionsのところにあるbaseをクリック、または左のタブにあるbaseをダブルクリックすることでコンテナに入れます。
グラフィックなどX11経由で画面表示できるよう、Settings→ConfigurationからX11タブを開き、X11 remote accessをfullに設定しておきます。
Sessions→New session→SSHから、以下のように接続先を設定してSSH接続を行います。
Remote hostには localhost を入れ、Specify usernameにチェックを入れてDockerfileで作成したユーザ名を記入、PortにはDockerfileでbaseコンテナの22番ポートに割り当てたホスト側のポート番号(:の左側、私の例では49944)を入れてOKを押します。
その後、初回の接続時のみパスワードが必要になります。
うまく接続できると以下のようにbashのコマンドウィンドウが立ち上がります。
複数ウィンドウを立ち上げたい場合は、Terminal→Duplicate current tab または bashウィンドウの上の方のbaseと表示のある部分で右クリック→Duplicate tabから立ち上げられます。
立ち上げた後は、baseと表示のある部分をドラッグしてMobaXtermの枠外にドロップすることで複数ウィンドウを並べて表示できます。
Tera Termの場合、新しい接続の画面から下図のようにホスト名に localhost を入れ、TCPポート#にDockerfileでbaseコンテナの22番ポートに割り当てたホスト側のポート番号(:の左側、私の例では49944)を入れてOKを押します。
ユーザ名とパスフレーズにDockerfileで作成したユーザ名とパスワードを入れてOKを押します。
正しく接続できると以下のように表示されます。
続いてそれぞれのサービスの動作確認をしていきます。まずは、作業場所となるbase環境から。
base/ Python
Pythonの動作確認としてnumpy, pandas, scypy, matplotlibを試していきます。
import numpy as np import scipy.integrate as itg import matplotlib.pyplot as plt import pandas as pd VMIN = -2.0 VMAX = 2.0 def func1(x): return x ** 2 def func2(x0, x1): return itg.quad(func1, x0, x1)[0] x = pd.Series([i for i in np.arange(VMIN, VMAX, 0.01)]) y = x.map(func1) y_sum = pd.Series([func2(VMIN, i) for i in x]) table = pd.DataFrame({'y':y, 'y_sum':y_sum}) table.index = x print(table) table.plot(title='Plot Example') plt.show()
上のようなPythonコードを用意して実行します。この例では-2~2の範囲で y=x2 とその積分値をグラフ化しています。
cd /work/test python test_python.py
私の例では/work/testにPythonプログラムを保存したので、そこへcdで移動しています。
以下のようなグラフが表示されれば成功です。
base/ OpenCV(Python)
OpenCVで画像の平滑化とエッジ抽出を試してみます。
import numpy as np import cv2 import matplotlib.pyplot as plt #%matplotlib inline RESIZEX = 300 RESIZEY = 400 img = cv2.imread('../img/sample/dedenne.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (RESIZEX, RESIZEY)) blr = cv2.blur(img, (5, 5)) edg = cv2.Canny(blr, 50, 80) plt.subplot(1, 3, 1), plt.imshow(img) plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(1, 3, 2), plt.imshow(blr) plt.title('Blur'), plt.xticks([]), plt.yticks([]) plt.subplot(1, 3, 3), plt.imshow(edg, cmap = 'gray') plt.title('Edge'), plt.xticks([]), plt.yticks([]) plt.show()
上のようなPythonコードを用意して実行します。この例では入力画像をリサイズしたものをOriginalとし、平滑化、エッジ抽出しています。
python test_opencv.py
以下のような画像が表示されれば成功です。
base/ Tensorflow, Keras, Jupyter-Notebook
機械学習ライブラリKerasとTensorflowバックエンドを使ってFashion MNIST(服の分類)を試してみます。
import keras from keras.datasets import fashion_mnist from keras.models import Sequential from keras.layers import Dense, Dropout from keras.optimizers import RMSprop from IPython import get_ipython import matplotlib.pyplot as plt import numpy as np import copy #%matplotlib inline ipy = get_ipython() if ipy is not None: ipy.run_line_magic('matplotlib', 'inline') np.random.seed(0) batch_size = 128 num_classes = 10 epochs = 20 # the data, split between train and test sets (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data() x_test_org = copy.deepcopy(x_test) # show a part of training images label_name = ('T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ancle boot') n_rows = 5 n_cols = 5 fig, axs = plt.subplots(n_rows, n_cols, figsize=(28,28)) plt.subplots_adjust(wspace=0.0, hspace=0.6) i = 0 for ax, pixels, label in zip(axs.flat, x_train, y_train): ax.imshow(pixels, cmap="gray") ax.set_title('{}: {}'.format(i, label_name[label]), fontsize=20) ax.set_xticks([]) ax.set_yticks([]) i+=1 plt.show() x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) x_train = x_train.astype('float32') x_test = x_test.astype('float32') x_train /= 255 x_test /= 255 print(x_train.shape[0], 'train samples') print(x_test.shape[0], 'test samples') # convert class vectors to binary class matrices y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) model = Sequential() model.add(Dense(512, activation='relu', input_shape=(784,))) model.add(Dropout(0.2)) model.add(Dense(512, activation='relu')) model.add(Dropout(0.2)) model.add(Dense(num_classes, activation='softmax')) model.summary() model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy']) history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(x_test, y_test)) score = model.evaluate(x_test, y_test, verbose=0) print('Test loss:', score[0]) print('Test accuracy:', score[1]) # pick wrong choices prex = model.predict(x_test) wrong = [] title = [] i = 0 while True: yi = y_test[i:i+1] prei = prex[i:i+1] if prei.argmax() != yi.argmax(): wrong.append(i) title.append('{}: {} -> {}'.format(i, label_name[yi.argmax()], label_name[prei.argmax()])) if len(wrong) == n_rows * n_cols or i == len(y_test): break i+=1 print('wrong choice: {}'.format(title)) fig, axs = plt.subplots(n_rows, n_cols, figsize=(28,28)) plt.subplots_adjust(wspace=0.0, hspace=0.6) for ax, num, label in zip(axs.flat, wrong, title): ax.imshow(x_test_org[num], cmap="gray") ax.set_title(label, fontsize=20) ax.set_xticks([]) ax.set_yticks([]) plt.show()
このプログラムは前の例と同様にコマンドラインからpythonで実行することもできますが、今回は可視化に優れた解析ツールJupyter-Notebookを使って実行してみます。
jupyter_notebook
上のコマンドはDockerfileで jupyter notebook –ip=0.0.0.0 –port=8080 にaliasしていますので、実際に以下のように打ち込んでも同じです。
jupyter notebook --ip=0.0.0.0 --port=8080
上記どちらかの方法でJupyter-Notebookを実行すると、画面に出るログの中に以下のような部分が見つかると思います。
その中の /?token= 以降をコピーしておきます。
Windows上でブラウザを立ち上げ、URLとして localhost:50080 に続けて先ほどコピーした文字列を貼り付けEnterを押します。
localhost:50080/?token=~
ポート番号の 50080 はDockerfileでbaseコンテナの 8080 ポートに割り当てたホスト側のポート番号です。
下図のようなJupyter-Notebookの画面がブラウザに表示されるので、Newと表示されているプルダウンメニューからPython 3を選択します。
コマンド入力部に以下を打ち込み、Shift+Enterを押します。
run -i test_keras.py
以下のように出力されれば成功です。Jupyter-Notebookでは出力した画像もログのように扱われて履歴として残るので便利ですね。
上の例では6万件の訓練用画像を学習したモデルを評価用画像に適用して、誤認識したものの一部を表示してみました。
Nginx + PHP
WebサーバーのNginx(エンジンエックス)とサーバーサイドWeb開発スクリプト言語であるPHPを使う環境を試します。
前述のdocker-compose.ymlの例のとおり、今回はnginx:1.13.5-alpineとphp:7.1.9-fpmのイメージをそのまま使っているので、NginxとPHP用のDockerfileは作成していません。
Nginxの設定ファイルであるdefault.confは以下の内容とし、Volumeでコンテナに反映します。
server { listen 80; server_name _; root /var/www/html; index index.html; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(\.+)$; fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } }
data/htmlには次のようなファイルを置いておきます。
index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test Ajax and PHP</title> <link rel="stylesheet" href="style.css" type="text/css" /> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script src="disp.js"></script> </head> <body> <p id="first" style="display:inline">Hello </p> <p id="second" style="display:inline">World!</p> <br> <select id="greet"> <option value="Hello">Hello </option> <option value="おはよう">おはよう </option> <option value="やあ">やあ </option> </select> <button id="world" class="button1">World!</button> <button id="everyone" class="button1">みなさん</button> <button id="japan" class="button1">日本</button> </body> </html>
style.css
button.button1{ height: 30px; line-height: 30px; }
disp.js:
$(function(){ $('#greet').on("change",function(){ let param = { "opt": $(this).val() }; $.post({ url: "get.php", data: param, dataType: "json", }).done(function(data){ $("#first").text(data.output_text); }).fail(function(XMLHttpRequest, textStatus, errorThrown){ alert(errorThrown); }); }); $('#world').on("click",function(){ let param = { "opt": "World!" }; $.post({ url: "get.php", data: param, dataType: "json", }).done(function(data){ $("#second").text(data.output_text); }).fail(function(XMLHttpRequest, textStatus, errorThrown){ alert(errorThrown); }); }); $('#everyone').on("click",function(){ let param = { "opt": "みなさん" }; $.post({ url: "get.php", data: param, dataType: "json", }).done(function(data){ $("#second").text(data.output_text); }).fail(function(XMLHttpRequest, textStatus, errorThrown){ alert(errorThrown); }); }); $('#japan').on("click",function(){ let param = { "opt": "日本" }; $.post({ url: "get.php", data: param, dataType: "json", }).done(function(data){ $("#second").text(data.output_text); }).fail(function(XMLHttpRequest, textStatus, errorThrown){ alert(errorThrown); }); }); });
get.php:
<?php // Accept only from ajax $request = isset($_SERVER['HTTP_X_REQUESTED_WITH']) ? strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) : ''; if($request !== 'xmlhttprequest') exit; $text = filter_input(INPUT_POST, 'opt'); echo json_encode(['output_text' => "$text"]);
ここまで準備できたら、nginxとphpを起動し、ホストマシン(Windows)のWebブラウザでlocalhostにつなぐと下のように動的に文字を変更するページが表示されるはずです。
JQueryでクリック動作を拾い、PHPにJSONデータを投げ、PHPからJSONで返ってきたテキストデータで<p>オブジェクトの内容を書き換えています。
処理の内容としては全くPHPを使う必然性がありませんが、これで一応NginxとPHPを連携させた動的なWebページの動作確認ができました。
Node.js + PostgreSQL
次はイベント駆動のJavascript環境であるNode.jsとPostgreSQLデータベースの連携でデータを永続化した動的Webページを試します。
当初、データベースとしてmongoDBやmysqlを試していたのですが、データの永続化がうまくいかず、PostgreSQLを試しているうち、前述の「Volumeについて」の②の方法でデータの永続化ができたのでこれを採用しました。(もしかするとmongoDBやmysqlでも②の方法で動くのかもしれませんが、PostgreSQLでうまくいったあとは試していません。)
PostgreSQLサービスのIPアドレスはdocker-compose.ymlの設定で172.18.0.2固定にしてあります。
WebアプリフレームワークExpressとテンプレートエンジンejsを使ってNode.js用JavaScriptを記述します。
data/node/app.js:
//======== Express settings ======== let express = require('express'), app = express(); app.set('views', __dirname + '/views'); app.set('view engine', 'ejs'); //======== Postgresql settings ======== let address_list; let { Client } = require('pg'); let client = new Client({ user: 'dbuser', database: 'mydb', password: 'dbpass', host: '172.18.0.2', port: 5432 }); client.connect(); //======== Routing ======== app.get('/', (req, res) => { client.query("SELECT * from address_book") .then(res1 => { address_list = res1.rows; console.log('--------'); console.log(address_list); res.render('index', {posts: address_list}); }) .catch(e => console.error(e.stack)); }); app.get('/add/:name', (req, res) => { client.query("INSERT INTO address_book (name, address) VALUES ('" + req.params.name + "', '" + req.query.address + "')") .then(res1 => { client.query("SELECT * from address_book") .then(res2 => { address_list = res2.rows; console.log('--------'); console.log(address_list); res.json(address_list); }) .catch(e => console.error(e.stack)); }) .catch(e => console.error(e.stack)); }); app.get('/del/:name', (req, res) => { client.query("DELETE FROM address_book WHERE name='" + req.params.name + "' AND address='" + req.query.address + "'") .then(res1 => { client.query("SELECT * from address_book") .then(res2 => { address_list = res2.rows; console.log('--------'); console.log(address_list); res.json(address_list); }) .catch(e => console.error(e.stack)); }) .catch(e => console.error(e.stack)); }); app.listen(3000, () => { console.log('server listening...'); });
初回’/’にGET要求が来た場合はテンプレートindex.ejsの中身を置き換えて表示し、Add, DelボタンクリックからのGET要求に対しては追加や削除後のリストデータをJSON形式で返し、JQueryで動的にテーブルの内容を更新しています。
data/node/views/index.ejs:
<html> <head> <title>Address Book</title> <meta charset="utf-8"> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script> $( () => { let update = (data) => { $('#tbl').empty(); $('#sel').empty(); $('#tbl').append('<tr style="background: #ccccff;"><td>Name</td><td>Address</td></tr>'); for(let i in data){ $('#tbl').append('<tr><td>' + data[i].name + '</td><td>' + data[i].address + '</td></tr>'); $('#sel').append('<option address="' + data[i].address + '">' + data[i].name + '</optio>'); } } $('#add').on("click", () => { $.get( 'http://localhost:3000/add/' + $('#name').val(), {address:$('#address').val()}, 'json' ).done( (data) => { update(data); $('#name').val(''); $('#address').val(''); }).fail( (XMLHttpRequest, textStatus, errorThrown) => { alert(errorThrown); }); }); $('#del').on("click", () => { $.get( 'http://localhost:3000/del/' + $('#sel option:selected').val(), {address:$('#sel option:selected').attr('address')}, 'json' ).done( (data) => { update(data); }).fail( (XMLHttpRequest, textStatus, errorThrown) => { alert(errorThrown); }); }); }); </script> </head> <body> <p>Address List</p> <form> Name: <input type="text" id="name"> Address: <input type="text" id="address"> <button id="add">Add</button> </form> <select id="sel"> <% for (let i in posts){ %> <option address="<%= posts[i].address %>"><%= posts[i].name %></option> <% } %> </select> <button id="del">Delete</button> <br><br> <table border="1" cellpadding="5" cellspacing="2" id="tbl"> <tr style="background: #ccccff;"><td>Name</td><td>Address</td></tr> <% for (let i in posts){ %> <tr><td><%= posts[i].name %></td><td><%= posts[i].address %></td></tr> <% } %> </table> </body> </html>
続いて、PostgreSQLのデータベースを作成し、初期データを格納するSQL文を次のように作成します。
postgres/createdb.sql:
CREATE TABLE address_book (name varchar(255), address varchar(255)); GRANT ALL ON address_book TO dbuser; INSERT INTO address_book( name, address) VALUES ('User', 'Tokyo Japan'), ('John', 'Calif US');
これを使って以下のようにコンテナに入り、psqlコマンドでPostgreSQLをコマンドラインで立ち上げ、¥iコマンドでcreatedb.sqlを読み込みます。
PS> docker exec -e POSTGRES_DB=mydb -e POSTGRES_PASSWORD=dbpass -it pg bash
bash> psql -U dbuser mydb
psql> ¥i /var/lib/postgresql/createdb.sql CREATE TABLE GRANT INSERT 0 2
以下のテーブルができていることを確認します。
psql> ¥dt List of relations Schema | Name | Type | Owner --------+--------------+-------+-------- public | address_book | table | dbuser (1 row)
次にテーブルの中身を確認します。
psql> select * from address_book ; name | address ------+------------- User | Tokyo Japan John | Calif US (2 rows)
ログは以下のように確認できます。
PS> docker logs pg PostgreSQL init process complete; ready for start up. 2019-04-27 02:09:11.591 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 2019-04-27 02:09:11.591 UTC [1] LOG: listening on IPv6 address "::", port 5432 2019-04-27 02:09:11.600 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" 2019-04-27 02:09:11.628 UTC [41] LOG: database system was shut down at 2019-04-27 02:09:11 UTC 2019-04-27 02:09:11.636 UTC [1] LOG: database system is ready to accept connections 2019-04-27 02:09:35.621 UTC [54] LOG: statement: CREATE TABLE address_book (name varchar(255), address varchar(255)); 2019-04-27 02:09:35.640 UTC [54] LOG: statement: GRANT ALL ON address_book TO dbuser; 2019-04-27 02:09:35.643 UTC [54] LOG: statement: INSERT INTO address_book( name, address) VALUES ('User', 'Tokyo Japan'), ('John', 'Calif US'); :(省略) 2019-04-27 02:09:52.005 UTC [54] LOG: statement: select * from address_book ;
以下はコンテナの起動からlocalhost:3000への接続、表示されたアドレス帳ページで項目の追加、削除を行う実施例です。
データベースの情報はVolumeに記録されるので、コンテナやdockerを停止してもデータは永続化され、次回起動時には最後の状態が表示されます。
Dockerコンテナの停止・削除
PowerShellからdocker-composeコマンドを使ってDockerコンテナを停止・削除します。
PS> docker-compose down Stopping nginx ... done Stopping node ... done Stopping base ... done Stopping php ... done Stopping pg ... done Removing nginx ... done Removing node ... done Removing base ... done Removing php ... done Removing pg ... done Removing network docker_mynet Removing network docker_default
エラー対応
何らかのエラーが出て、いつものようにコンテナが立ち上がらないなどの場合、Windowsツールバーの^印からDockerのクジラマーク上で右クリック→RestartでDockerを再起動するとうまくいくケースがありました。
PCの電源を落とす前にDocker for Windowsを終了しておくと起動時のトラブルが減るように思われます。
Error response from daemon: driver failed programming external connectivity on endpoint ~
上記のエラーが出てDockerを再起動しても直らないときは、ネットワークデバイスをクリンナップする以下のコマンドを実行し、PCを再起動します。
netcfg -d
おまけ:Ctrl+N無効化
私はssh接続を選択しましたが、NoVNCなどブラウザでデスクトップ画面を表示する方法を試した際の困りごと解決方法について書きます。
Linuxではコマンドhistoryをさかのぼったり、進めたりするのにCtrl+P, Ctrl+N(矢印キー↑↓も可)を使います。しかし、WindowsマシンでCtrl+Nを押すと新しいウィンドウ(ブラウザ)が立ち上がってしまって不快な思いをしました。
対策を調べるうち、AutoHotkeyが良い解決法であることがわかりました。
以下の内容をAutoHotkey.ahkというファイル名で「ドキュメント」フォルダ(C:¥Users¥¥Documents)に置くことでCtrl+Nを↓に置き換えることができ、快適になりました。
^n::Send,{Down}
以下のフォルダにAutoHotkey.exe(C:¥Program Files¥AutoHotkey¥AutoHotkey.exe)へのショートカットを置くことで自動的に起動するようになります。
C:¥Users¥user¥AppData¥Roaming¥Microsoft¥Windows¥Start Menu¥Programs¥Startup
Docker for Macでもテスト
本ページの内容をDocker for Macでも試し、以下のようにXquartzをインストールしておくことで動作することを確認しました。
1行目のコマンドはbrewのインストールですので、インストール済みの場合は2行目のみ実行することで良いです。
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" brew cask install xquartz
Xquartzのインストールが終わったら一旦Macを再起動します。
コンテナを立ち上げてssh接続します。
docker-compose up -d ssh -Y -p 49944 user@localhost
感想
Dockerは何かと問題に当たることも多いですが、一度好みの環境が立ち上がると快適で、スタンドアロンのLinux PCの置き換えとして十分機能すると思います。さらに複数マシンがローカルなネットワークにつながった状態を作って試せるのはとても便利です。
Python等の依存関係に問題が起きた場合など、通常は再インストールからやり直すのはハードルが高いですが、DockerではユーザデータはVolumeとして別管理にできるのと環境構築が自動化されていることで、何度でも環境を更新できるのも良いと感じています。
みなさまも快適なDocker Lifeを!
なお、現時点で構築したDocker環境について書きましたが、今後もDockerを使いながら環境を更新していくので、それに合わせて本ページの内容を更新することがあると思います。
書籍紹介
コンテナ型アプリケション開発の概念から運用まで説明されており、これまで何となくDockerを使っていた方が本格的に学ぶのに役立つ一冊です。
参考文献
- 仮想環境についてまとめてみる(https://qiita.com/9en/items/f4eab2f61485a9f3885a)
- Docker Wikipedia(https://ja.wikipedia.org/wiki/Docker)
- Install Docker Desktop for Windows(https://docs.docker.com/docker-for-windows/install/)
- Docker for WindowsをWindows10 Proにインストール(https://qiita.com/anikundesu/items/7ecf20b7e8a60f8439a8)
- Use argument in Dockerfile(https://blog.lorentzca.me/use-argument-in-dockerfile/)
- DockerでpythonのTensorFlowとOpenCVの実行環境を構築する.(https://qiita.com/naoyukiyama/items/29054cff00923f9543ce)
- How to Install Go 1.11 on Debian 9/8/7(https://tecadmin.net/install-go-on-debian/)
- kearsのmnistのサンプルを読んでみる(https://qiita.com/ash8h/items/29e24fc617b832fba136)
- Docker for Windowsでpostgresのデータマウントができない人へ(https://qiita.com/kyo-bad/items/47b96883144a5bf1cb1e)
- Node.jsからPostgreSQLに接続する(node-postgres)(https://symfoware.blog.fc2.com/blog-entry-2114.html)
- AutoHotkey(https://www.autohotkey.com/download/)