この記事の目的
Dockerを布教していたところ
Dockerで仮想環境を簡単に起動できるのはわかった。
ただ、Docker Hubに公開されているものならいいが、自作の環境を試行錯誤しながら作りたいときはどうすればいいのか。
というフィードバックをもらいました。
そこで、Dockerで環境を自作する様子を実際に私がたどった手順として公開し、イメージを掴んでもらうこととしました。
今回作成する環境は、Play Frameworkが起動する環境です。
筆者はこのとき初めてPlay Frameworkをインストールした初心者ですが、問題なく構築できました。
「空」のベースコンテナを起動し、インタラクティブに手順を確立していく
「Dockerで自作のイメージを作る」=「Dockerfileを作成する」です。
ほかにも方法はありますが、環境をコードとして残すことのメリットは大きいため、この方法がおすすめです。
しかし、いきなり正解のDockerfileを書くことはできません。
Dockerfileの中身はシェルコマンドの集まりですが、大抵の場合、書き間違いにより失敗します。
Dockerfileを書いてイメージをビルド、失敗した部分についてDockerfileを修正して再ビルド、などとしていては時間がかかりすぎます。
効率的なのは、「空」のベースコンテナを起動してsh
でコンテナ内に入り込み、対話的に正解のコマンドを探していくことです。
動作確認できたコマンドをDockerfileに書いていくのです。
この手法は以下の記事で説明されています:
効率的に安全な Dockerfile を作るには - Qiita
では具体的に始めてみます。
ベースとなる「空」のコンテナを起動する
Dockerfile作成
作業フォルダーに最低限のDockerfileを作成します。
うまくいった手順をメモする先として使い、そのまま完成形としてやるためです:
# ベースイメージを指定する
# 軽量なAlpineを使う
FROM alpine:3.5
# コンテナ起動時に実行するコマンド
# とりあえず即時終了しないコマンドを指定しておく
CMD ["top"]
コンテナは、CMD
またはENTRYPOINT
で指定したコマンドが実行中のみ、起動状態となります。
最終形としては、サーバーの起動コマンドを指定しますが、試行錯誤中はとりあえず即時終了しないtop
コマンドを指定しておきます。
即時終了しなければいいので、tail -F
でもOKです。
docker-compose.yml作成
コンテナの起動時オプションも、コードとして残したいので、作業フォルダーにdocker-compose.ymlも作成します:
# バージョン2記法で記述する
version: '2'
# 起動するコンテナを定義する
services:
# Play Frameworkコンテナ
play:
# DockerfileによってPlay Frameworkイメージの自作を目指す
# このファイルと同じフォルダーのDockerfileを使う
build: ./
# コンテナ名を指定する
# Docker Composeが自動生成するコンテナ名は長く、タイピングが面倒なため
container_name: play
Docker Composeは複数コンテナを管理するためのツールですが、起動時オプションをコード化するためにも使えます。
多くのオプションを連ねてコマンドを入力するのは大変ですし、以下のようなシェルスクリプトを用意するより、専用のツールを使うほうが良いでしょう:
#/!bin/bash
docker run --rm -ti -p 8888:8888 -v /Users/kazuma/qiita/2017/feb18:/root/feb18 alpine:3.5 sh
コンテナ起動
コンテナを起動するには、docker-compose.ymlと同じ階層でdocker-compose up
コマンドを実行します。
念のため--force-recreate
オプションをつけ、今後docker-compose.ymlを変更したとき、変更を取り入れ損ねないようにします:
$ docker-compose up --force-recreate
Recreating play
Mem: 624228K used, 1422636K free, 151476K shrd, 64500K buff, 406840K cached
play | CPU: 0% usr 0% sys 0% nic 100% idle 0% io 0% irq 0% sirq
play | Load average: 0.30 0.22 0.09 1/252 6
play | PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
コンテナに接続
-d
オプションをつけない場合、コンテナと接続し、コンテナ起動コマンドのtop
コマンドがフォアグラウンドで実行された状態となります。
この状態ではほかのコマンドを入力できません。
しかも、Ctrl + Cでtop
を止めると、コンテナを起動中に保つプロセスがなくなるため、コンテナごと終了してしまいます:
Mem: 624228K used, 1422636K free, 151476K shrd, 64500K buff, 406840K cached
play | CPU: 0% usr 0% sys 0% nic 100% idle 0% io 0% irq 0% sirq
play | Load average: 0.30 0.22 0.09 1/252 6
play | PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
^CGracefully stopping... (press Ctrl+C again to force)
Killing play ... done
$
そのため、コンテナ内で作業をするには、docker-compose up
を入力したのとは別の画面を使うか、docker-compose up -d
としてバックグラウンドでコンテナを起動する必要があります。
別の画面かバックグラウンドでコンテナを起動したら、以下のように、docker exec
コマンドでコンテナに接続します。
正確には異なりますが、SSHログインをしたような状態となります。
ちなみに、今回ベースイメージに使ったAlpine Linuxにはbashがインストールされていないため、sh
(Almquist shell) を使う必要があります:
$ docker exec -ti play sh
/ # pwd
/
/ # whoami
root
/ # ls
bin dev etc home lib media mnt proc root run sbin srv sys tmp usr var
この状態になったら、インストール作業ができるようになります。
Play Frameworkをインストールする
Play Frameworkは、Alpine Linuxのパッケージマネージャーapkで探しても見つからないようなので、公式ページからダウンロードする方法でインストールします。
手順はPlay Frameworkの公式ページに従います。
JDKインストール
前提条件にはJDK1.8が必要とありますが、初期状態ではJavaがインストールされていません:
/ # which java
/ #
JDKはapkでインストールできるようなので、apk経由でインストールします:
/ # apk update
fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/community/x86_64/APKINDEX.tar.gz
v3.5.1-31-g83b3d6857a [http://dl-cdn.alpinelinux.org/alpine/v3.5/main]
v3.5.1-29-ga981b1f149 [http://dl-cdn.alpinelinux.org/alpine/v3.5/community]
OK: 7958 distinct packages available
/ # apk search jdk
openjdk8-jre-base-8.121.13-r0
openjdk7-jre-base-7.121.2.6.8-r0
openjdk8-jre-8.121.13-r0
openjdk7-jre-7.121.2.6.8-r0
openjdk7-jre-lib-7.121.2.6.8-r0
openjdk8-8.121.13-r0
openjdk8-doc-8.121.13-r0
openjdk7-7.121.2.6.8-r0
openjdk8-jre-lib-8.121.13-r0
openjdk7-doc-7.121.2.6.8-r0
openjdk8-dbg-8.121.13-r0
openjdk8-demos-8.121.13-r0
/ # apk info openjdk8
openjdk8-8.121.13-r0 description:
OpenJDK 8 provided by IcedTea
openjdk8-8.121.13-r0 webpage:
http://icedtea.classpath.org/
openjdk8-8.121.13-r0 installed size:
19746816
/ #
apk search
やapk info
でリポジトリー内を探したところ、openjdk8
パッケージが使えそうです。
apk add
でインストールします:
/ # apk add openjdk8
(1/39) Installing libffi (3.2.1-r2)
(2/39) Installing libtasn1 (4.9-r0)
…
(39/39) Installing openjdk8 (8.121.13-r0)
Executing busybox-1.25.1-r0.trigger
Executing ca-certificates-20161130-r0.trigger
Executing java-common-0.1-r0.trigger
OK: 99 MiB in 50 packages
/ #
インストール成功です:
/ # java -version
openjdk version "1.8.0_121"
OpenJDK Runtime Environment (IcedTea 3.3.0) (Alpine 8.121.13-r0)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)
/ #
JDKをインストールするまでの手順をDockerfileに記録しておく
いったんここまでの手順をDockerfileに記録しておきます。
RUN
インストラクションに、先ほど試したapk add openjdk8
を書くだけです:
# ベースイメージを指定する
# 軽量なAlpineを使う
FROM alpine:3.5
# 各種ライブラリーのインストール
# まずはJavaをインストールする
RUN apk --no-cache add openjdk8
# コンテナ起動時に実行するコマンド
# とりあえず即時終了しないコマンドを指定しておく
CMD ["top"]
apk add openjdk8
に対して少しトリックを加えています。
--no-cache
オプションを使い、apk update
とrm -rf /var/cache/apk
を実行したのと同じ効果を得ています。
つまり、インストール後に余計なキャッシュが残らず、Dockerイメージのサイズを小さく抑えられるのです。
docker-compose build
コマンドでイメージを再ビルドし、docker-compose up --force-recreate -d
でコンテナを再生成します:
$ docker-compose build
Building play
Step 1/3 : FROM alpine:3.5
---> 88e169ea8f46
Step 2/3 : RUN apk --no-cache add openjdk8
---> Using cache
---> a9c2d3ba4db0
Step 3/3 : CMD top
---> Using cache
---> d2f55a5051eb
Successfully built d2f55a5051eb
$ docker-compose up --force-recreate -d
Recreating play
$
すでにビルドしたことがあるとUsing cache
と表示されます。
初回は、その位置にインストールの様子が表示されます。
コンテナに接続すると、java
コマンドが使えるようになっていることが確認できます:
$ docker exec -ti play sh
/ # java -version
openjdk version "1.8.0_121"
OpenJDK Runtime Environment (IcedTea 3.3.0) (Alpine 8.121.13-r0)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)
/ #
Activatorをインストールする
次に、Activator1なるものをダウンロードせよとあるので、そのとおりにします。
ダウンロードURLは、Lightbend公式のダウンロードページにある"DOWNLOAD"ボタンのリンクをコピーして使います:
/ # curl -L -O https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12.zip
sh: curl: not found
/ # wget https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12.zip
Connecting to downloads.typesafe.com (54.230.111.211:443)
wget: can't execute 'ssl_helper': No such file or directory
wget: error getting response: Connection reset by peer
/ #
おや、curl
コマンドはインストールされていないし、wget
は(多分)依存パッケージがなく使えないようです。
wget
を使えるようにする手もあるかもしれませんが、今回はcurl
をインストールすることにします:
/ # apk update
fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/community/x86_64/APKINDEX.tar.gz
v3.5.1-31-g83b3d6857a [http://dl-cdn.alpinelinux.org/alpine/v3.5/main]
v3.5.1-29-ga981b1f149 [http://dl-cdn.alpinelinux.org/alpine/v3.5/community]
OK: 7958 distinct packages available
/ # apk info curl
curl-7.52.1-r1 description:
An URL retrival utility and library
curl-7.52.1-r1 webpage:
http://curl.haxx.se
curl-7.52.1-r1 installed size:
200704
/ # apk add curl
(1/3) Installing libssh2 (1.7.0-r2)
(2/3) Installing libcurl (7.52.1-r1)
(3/3) Installing curl (7.52.1-r1)
Executing busybox-1.25.1-r0.trigger
OK: 100 MiB in 53 packages
/ #
curl
がインストールできたようなので、/tmp
にActivatorをダウンロードします:
/ # cd /tmp/
/tmp # curl -L -O https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12.zip
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 682M 100 682M 0 0 6096k 0 0:01:54 0:01:54 --:--:-- 5411k
/tmp #
ZIPファイルを解凍し、activator
コマンドにパスを通してインストール完了です:
/tmp # unzip typesafe-activator-1.3.12.zip
…
inflating: activator-dist-1.3.12/bin/activator-dist
creating: activator-dist-1.3.12/lib/
inflating: activator-dist-1.3.12/lib/org.scala-lang.scala-library-2.11.8.jar
inflating: activator-dist-1.3.12/lib/com.typesafe.activator.activator-dist-1.3.12.jar
/tmp # ls
activator-dist-1.3.12 hsperfdata_root typesafe-activator-1.3.12.zip
/tmp # ls activator-dist-1.3.12/
LICENSE.html README.html bin lib libexec repository templates
/tmp # ls activator-dist-1.3.12/bin/
activator activator-dist activator-dist.bat activator.bat
/tmp # export PATH="/tmp/activator-dist-1.3.12/bin:$PATH"
/tmp # echo $PATH
/tmp/activator-dist-1.3.12/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/tmp # which activator
/tmp/activator-dist-1.3.12/bin/activator
/tmp #
解凍先やPATHの設定方法は公式ページの手順を参考にしました。
Activatorを起動できるようにする
activator
コマンドにパスまで通しましたが、まだ使えません:
/tmp # activator -h
env: can't execute 'bash': No such file or directory
/tmp #
Activatorはbashに依存しているようです。
Alpineにはash (Almquist Shell) しかないため、エラーになるのです。
apk add
でbashをインストールします:
/tmp # apk info bash
bash-4.3.46-r5 description:
The GNU Bourne Again shell
bash-4.3.46-r5 webpage:
http://www.gnu.org/software/bash/bash.html
bash-4.3.46-r5 installed size:
700416
/tmp # apk add bash
(1/5) Installing ncurses-terminfo-base (6.0-r7)
(2/5) Installing ncurses-terminfo (6.0-r7)
(3/5) Installing ncurses-libs (6.0-r7)
(4/5) Installing readline (6.3.008-r4)
(5/5) Installing bash (4.3.46-r5)
Executing bash-4.3.46-r5.post-install
Executing busybox-1.25.1-r0.trigger
OK: 108 MiB in 58 packages
/tmp # bash
bash-4.3# exit
exit
/tmp #
これでActivatorが起動できるようになりました。
バックグラウンドでactivator ui
を起動します:
/tmp # activator ui &
Checking for a newer version of Activator (current version 1.3.12)...
…
Play server process ID is 313
[info] play - Application started (Prod)
[info] play - Listening for HTTP on /127.0.0.1:8888
[info] a.e.s.Slf4jLogger - Slf4jLogger started
Unable to open a web browser!
Please point your browser at:
http://127.0.0.1:8888/
/tmp # curl -L http://127.0.0.1:8888/
<!-- Copyright (C) 2016 Lightbend, Inc <http://www.lightbend.com> -->
<!DOCTYPE html>
…(長いスクリプト)
</body>
</html>
/tmp #
curl
で繋ぐと、生のHTMLからは中身が把握できませんが、無事起動したことは確認できました。
Activatorをインストールするまでの手順をDockerfileに記録しておく
ここまででActivatorをインストールできました。
その手順をDockerfileに書き残しておきます:
# ベースイメージを指定する
# 軽量なAlpineを使う
FROM alpine:3.5
# 各種ライブラリーのインストール
# curl: Activatorのパッケージダウンロード用
# openjdk8, bash: Activator起動用
RUN apk --no-cache add openjdk8 curl bash && \
cd /tmp && \
curl --location --remote-name https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12.zip && \
unzip -q typesafe-activator-1.3.12.zip -d /root && \
rm -f typesafe-activator-1.3.12.zip
# Activatorにパスを通す
ENV PATH /root/activator-dist-1.3.12/bin:$PATH
# ホストに公開したいコンテナ内のポートを指定する
# Dockerfile内の指定だけでは公開されないので、コンテナ起動時に引数を指定する必要がある
EXPOSE 8888 9000
# CMDの実行フォルダーを変更する
WORKDIR /root/activator-dist-1.3.12
# コンテナ起動時に実行するコマンド
# activatorコマンドで8888ポートにHTTPサーバーを起動する
CMD ["activator", "ui"]
変更点は、まずRUN
インストラクションです。
curlとbashをインストールしているほか、Activatorもダウンロード&展開しています。
可読性を上げるためcurl
のオプションをフルネームで記載したり、unzip
の出力が冗長なので抑制したり、展開先を/tmp
から変えたり、手直しもしています。
ちなみに、RUN
の中では&&
でコマンドをつなぎ、さらに最後でZIPを削除していますが、これはDockerイメージのサイズを抑えるために重要です。
Dockerイメージのサイズは、インストラクションごとに増える一方のため、別のRUN
内でファイルを削除しても意味がないのです。
また、パスはENV
インストラクションで変更しています。
DockerfileでPATHを通す時はRUNではなくENVを使おう - Qiita
EXPOSE
インストラクションにて、Activatorがデフォルトで使う8888ポートと9000ポートを指定しました。
ですが、これはコンテナが外部に公開するポートの意思表示をしているだけであるため、実際にホストから接続するには、起動時にオプションを渡す必要があります。
具体的には、docker-compose.ymlの最後に以下のように追記すればOKです:
…
# コンテナ内のポートをホストに公開する
# 書式 "HOST:CONTAINER"
ports:
# Activatorのポートを同じ番号でホストに公開する
- "8888:8888"
- "9000:9000"
Play Frameworkの公式ページには、ZIP展開したらcd activator*
せよとあったので、WORKDIR
インストラクションでそのように指定しています。
CMD
インタラクションは、top
コマンドからactivator ui
に変更しました。
フォアグラウンドで起動しないとコンテナが即時終了するので、&
を使う必要はありません。
docker-compose build
コマンドでイメージを再ビルドし、docker-compose up --force-recreate
でコンテナを再生成します:
$ docker-compose build
Building play
Step 1/6 : FROM alpine:3.5
---> 88e169ea8f46
Step 2/6 : RUN apk --no-cache add openjdk8 curl bash && cd /tmp && curl --location --remote-name https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12.zip && unzip -q typesafe-activator-1.3.12.zip -d /root && rm -f typesafe-activator-1.3.12.zip
---> Using cache
---> c1432da40503
Step 3/6 : ENV PATH /root/activator-dist-1.3.12/bin:$PATH
---> Using cache
---> 2a2622235ec8
Step 4/6 : EXPOSE 8888 9000
---> Using cache
---> ea17f5c3b67e
Step 5/6 : WORKDIR /root/activator-dist-1.3.12
---> Using cache
---> 6fcbc688b566
Step 6/6 : CMD activator ui
---> Running in 862918668a84
---> 43fd43cb4d90
Removing intermediate container 862918668a84
Successfully built 43fd43cb4d90
$
$ docker-compose up --force-recreate
Recreating play
…
play | Play server process ID is 55
play | [info] play - Application started (Prod)
play | [info] play - Listening for HTTP on /127.0.0.1:8888
play | [info] a.e.s.Slf4jLogger - Slf4jLogger started
play | Unable to open a web browser!
play | Please point your browser at:
play | http://127.0.0.1:8888/
ホストからActivator UIに接続できるようにする
前節で起動したコンテナは、docker-compose ps
でチェックすると、期待どおり起動してポートも公開しているはずですが、しかしホストのブラウザから http://localhost:8888/ へ接続しても、画面は表示されません:
$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------
play activator ui Up 0.0.0.0:8888->8888/tcp, 0.0.0.0:9000->9000/tcp
$
コンテナ内からcurl
で接続すると確かにレスポンスがあるのですが。
$ docker exec -ti play sh
~/activator-dist-1.3.12 # curl -L http://localhost:8888/
…
</body>
</html>
~/activator-dist-1.3.12 #
原因は、Activator側の設定でした。
127.0.0.1しかリッスンしておらず、ホストからの経路は弾かれていたようです。
-Dhttp.address=0.0.0.0
オプションを渡すと、ホストからもアクセスできるようになります:
scala - How to force Typesafe Activator to listen 0.0.0.0:8888 - Stack Overflow
…
# コンテナ起動時に実行するコマンド
# activatorコマンドで8888ポートにHTTPサーバーを起動する
# ホストからアクセスするために http.address=0.0.0.0 が必要
CMD ["activator", "-Dhttp.address=0.0.0.0", "ui"]
docker-compose build
コマンドでイメージを再ビルドし、docker-compose up --force-recreate
でコンテナを再生成すると、ホストのブラウザから以下の画面が見られるようになります。
$ docker-compose up --force-recreate
Recreating play
…
play | Play server process ID is 54
play | [info] play - Application started (Prod)
play | [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:8888
play | [info] a.e.s.Slf4jLogger - Slf4jLogger started
play | Unable to open a web browser!
play | Please point your browser at:
play | http://0.0.0.0:8888/
まとめ
「空」のAlpineイメージから始めて、Activator UIを起動するイメージを作成できました。
Play FrameworkとActivatorの関係がよくわかっていませんが、ひとまず環境の準備はできたので、docker exec
で接続して色々と試すことができます。
いじった結果が気に食わなければ、コンテナを破棄して作り直せばよいだけです。
「Activatorをインストールして起動できるようにする」という環境構築の大変さは、ホストマシン上でもDockerコンテナ上でも変わりません。
しかし、Dockerを使って環境を隔離すれば、やり直しが簡単なため、確実に効率化できるはずです。
Dockerfileとdocker-compose.ymlで構築手順をコード化できるのも利点です。
ファイルの最終形
# バージョン2記法で記述する
version: '2'
# 起動するコンテナを定義する
services:
# Play Frameworkコンテナ
play:
# DockerfileによってPlay Frameworkイメージの自作を目指す
# このファイルと同じフォルダーのDockerfileを使う
build: ./
# コンテナ名を指定する
# Docker Composeが自動生成するコンテナ名は長く、タイピングが面倒なため
container_name: play
# コンテナ内のポートをホストに公開する
# 書式 "HOST:CONTAINER"
ports:
# Activatorのポートを同じ番号でホストに公開する
- "8888:8888"
- "9000:9000"
# ベースイメージを指定する
# 軽量なAlpineを使う
FROM alpine:3.5
# 各種ライブラリーのインストール
# curl: Activatorのパッケージダウンロード用
# openjdk8, bash: Activator起動用
RUN apk --no-cache add openjdk8 curl bash && \
cd /tmp && \
curl --location --remote-name https://downloads.typesafe.com/typesafe-activator/1.3.12/typesafe-activator-1.3.12.zip && \
unzip -q typesafe-activator-1.3.12.zip -d /root && \
rm -f typesafe-activator-1.3.12.zip
# Activatorにパスを通す
ENV PATH /root/activator-dist-1.3.12/bin:$PATH
# ホストに公開したいコンテナ内のポートを指定する
# Dockerfile内の指定だけでは公開されないので、コンテナ起動時に引数を指定する必要がある
EXPOSE 8888 9000
# CMDの実行フォルダーを変更する
WORKDIR /root/activator-dist-1.3.12
# コンテナ起動時に実行するコマンド
# activatorコマンドで8888ポートにHTTPサーバーを起動する
# ホストからアクセスするために http.address=0.0.0.0 が必要
CMD ["activator", "-Dhttp.address=0.0.0.0", "ui"]
-
旧 Typesafe Activator、現 Lightbend Activator のようです。 ↩