私家版 Dockerfile Pattern
* [Service パターン](#service-パターン)
* [Solo Service](#solo-service)
* [Multi Services](#multi-services)
* [Web Service](#web-service)
* [Upstart(Using ubuntu-upstart)](#upstartusing-ubuntu-upstart)
* [Buildstep](#buildstep)
* [Builder パターン](#builder-パターン)
* [Command パターン](#command-パターン)
* [Shell](#shell)
* [Wrapper/Bundler](#bundler)
* [Data Volume Container パターン](#data-volume-container-パターン)
* [Onbuild パターン](#onbuild-パターン)
## 前提
* boot2docker 1.3 を入れてセットアップする(TBD)。
* REPO: https://github.com/udzura/dockerfile-patterns
## Service パターン
### Solo Service
* 基本的なコンテナ
* 単独のサービスを CMD で動かす
* よくやるのが[sshdの起動](http://docs.docker.com/examples/running_ssh_service/)
```dockerfile
FROM ubuntu:trusty
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -i 's/UsePam yes/UsePam no/' /etc/ssh/sshd_config
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
```
* これで、以下を実行すれば localhost の 2222 番ポートで root/root ログインできるはず。
```bash
$ docker build -t lesson/service-solo service/solo/
$ docker run -d -p 2222:22 lesson/service-solo
$ ssh root@$(boot2docker ip) -p 2222 \
-oStrictHostKeyChecking=no \
-oUserKnownHostsFile=/dev/null # sshconfigのおまじない
root@192.168.59.103's password:
Welcome to Ubuntu 14.04 LTS (GNU/Linux 3.2.0-54-generic x86_64)
* Documentation: https://help.ubuntu.com/
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
root@f83ab81e48ef:~#
root@f83ab81e48ef:~# exit
logout
```
* Hello, Docker!!
### Multi Services
* 複数のサービスを動かして & で裏に送り、最後にログを tail -fする。
* なぜなら、デーモンモードの場合、CMDで動かす実行ファイルは終了してはならないから。
* よりゲンミツに運用する場合は、終了しない tail -f よりはいずれかのプロセスの終了を検知してCMDの親プロセスも終了する仕組みを導入すべきだろう。
* https://twitter.com/golden_eggg/status/535636978482429952 こちらのご指摘の通り
* ラッパーシェルを雑に書くと吉。
```bash
#!/bin/bash
# This is wrapper.sh
/usr/sbin/sshd -D >> /var/log/container.log &
/usr/bin/mongod --dbpath /var/spool/mongodb >> /var/log/container.log &
tail -f /var/log/container.log
```
```dockerfile
FROM ubuntu:trusty
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -i 's/UsePam yes/UsePam no/' /etc/ssh/sshd_config
RUN apt-get install -y mongodb
RUN mkdir /var/spool/mongodb
ADD ./wrapper.sh /usr/local/bin/wrapper.sh
RUN chmod a+x /usr/local/bin/wrapper.sh
EXPOSE 22
EXPOSE 27017
CMD /usr/local/bin/wrapper.sh
```
* EXPOSE を有効にするには -P
```bash
$ docker build -t lesson/service-multi service/multi/
$ id=$( docker run -d -P lesson/service-multi )
$ docker port $id 22 # or 27017
....
```
* tailしているlogも参照可能
```bash
$ docker logs $id
```
### Web Service
* Web UIのあるツールを動かし、そのまま80番をEXPOSE
* https://github.com/udzura/docker-munin を写経してみると良い(Ubuntuのバージョンが違うので微妙にコードは違う...)
```dockerfile
FROM ubuntu:trusty
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update -y
RUN apt-get install -y wget
RUN apt-get install -y cron munin apache2
# muninの作るmunin.confがApache2.4互換でない...(まめ知識)
RUN sed -i 's/Allow from localhost.*/Require all granted/g' /etc/apache2/conf-enabled/munin.conf
RUN sed -i '/Order allow,deny/d' /etc/apache2/conf-enabled/munin.conf
RUN ( mkdir -p /var/run/munin && chown -R munin:munin /var/run/munin )
ADD wrapper.sh /usr/local/bin/wrapper.sh
RUN chmod a+x /usr/local/bin/wrapper.sh
VOLUME /var/lib/munin
VOLUME /var/log/munin
EXPOSE 80
CMD ["/usr/local/bin/wrapper.sh"]
```
```bash
#!/bin/bash
# This is wrapper.sh
# placeholder html to prevent permission error
cat << EOF > /var/cache/munin/www/index.html
<html>Munin has not run yet. Please try again in a few moments.</html>
EOF
# make accessible from munin
chown -R munin:munin /var/lib/munin /var/log/munin
chown munin:munin /var/cache/munin/www/index.html
# start cron
/usr/sbin/cron &
# start local munin-node
/usr/sbin/munin-node > /dev/null 2>&1 &
# start apache
/usr/sbin/apache2ctl -DFOREGROUND
```
```bash
$ docker build -t lesson/service-web service/web/
$ id=$( docker run -d -P lesson/service-web )
$ echo http://$(boot2docker ip):$(docker port $id 22 | cut -f2 -d:)/munin
出てきたURL(例:http://192.168.59.103:49167/munin)にアクセス、ちなみにしばらくしたらグラフも出る
```
* あるいは、普通にウェブアプリをLLのフレームワークで書いて走らせる場合。後述するBuildstepやBundlerパターンも参照せよ。
### Upstart(Using ubuntu-upstart)
* ふつう、コンテナの/sbin/initは動作しないものに置換されて提供されている。
* [ubuntu-upstart](https://registry.hub.docker.com/u/library/ubuntu-upstart/) は動作する/sbin/initを備えているので、ほぼ普通のUbuntuのようにサービスを動かせる
```dockerfile
FROM ubuntu-upstart:trusty
RUN apt-get update -y
RUN apt-get install -y mysql-server
RUN apt-get install -y nginx
RUN apt-get install -y mongodb
ENV TERM screen-256color
EXPOSE 3306 80 27017
RUN echo "Hello, upstart!" > /usr/share/nginx/html/index.html
# CMD は書かない。そうするとubuntu-upstart:trustyのCMDが継承される
```
```bash
$ docker build -t lesson/service-upstart service/upstart/
$ id=$( docker run -d -P lesson/service-upstart )
```
* 入って何かを実行できる。sshもいいけどdocker execでね
```bash
$ docker exec $id /bin/ps -ef
$ docker exec -ti $id /usr/bin/top
$ docker exec -ti $id /bin/bash
root@1f694d4b0be0:/# ls -l
```
* これで良いじゃん感が高いベースイメージ。
* とはいえ CMD が init に固定されるなど微妙に融通が利かない時もある。
### Buildstep
* [buildstep](https://github.com/progrium/buildstep) を用いてHubotやSinatraなど軽量なアプリを動かせる。
* [非常に簡単なSinatraのサンプル]()
```dockerfile
FROM buildstep
ADD . /app
RUN /build/builder
CMD /start web
```
* [building.gem](https://github.com/CenturyLinkLabs/building) がラップしてくれる
```bash
$ gem install building highline
$ cd /path/to/app
$ building -f progrium/buildstep lesson/service-buildstep
identical Dockerfile
building docker build -t lesson/service-buildstep:latest .
Sending build context to Docker daemon 8.704 kB
Sending build context to Docker daemon
Step 0 : FROM progrium/buildstep
---> 5e2463ec79bd
Step 1 : ADD . /app
---> 3a7ac4f51633
Removing intermediate container 919307717ba6
Step 2 : RUN /build/builder
---> Running in 493f510dd029
-----> Ruby app detected
-----> Compiling Ruby/Rack
-----> Using Ruby version: ruby-2.0.0
-----> Installing dependencies using 1.6.3
Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment
Fetching gem metadata from http://rubygems.org/..........
Using bundler 1.6.3
Installing tilt 1.4.1
Installing rack 1.5.2
Installing rack-protection 1.5.3
Installing sinatra 1.4.5
Your bundle is complete!
Gems in the groups development and test were not installed.
It was installed into ./vendor/bundle
Bundle completed (9.12s)
Cleaning up the bundler cache.
###### WARNING:
You have not declared a Ruby version in your Gemfile.
To set your Ruby version add this line to your Gemfile:
ruby '2.0.0'
# See https://devcenter.heroku.com/articles/ruby-versions for more information.
-----> Discovering process types
Procfile declares types -> web
Default process types for Ruby -> rake, console, web
---> 5debf36082f2
Removing intermediate container 493f510dd029
Step 3 : CMD /start web
---> Running in 8b74b5b7ee74
---> 2bd8bede7de1
Removing intermediate container 8b74b5b7ee74
Successfully built 2bd8bede7de1
hint To run your app, try: docker run -d -p 8080 -e "PORT=8080" lesson/service-buildstep:latest
hint To re-build your app, try: docker build -t lesson/service-buildstep .
$ id=$( docker run -d -p 8080 -e "PORT=8080" lesson/service-buildstep:latest )
$ echo http://$(boot2docker ip):$(docker port $id 8080 | cut -f2 -d:) # and access it
```
* というかdokkuが中で使ってるので、dokku経由で何かをホストする場合は勝手にこのパターンになる。
## Builder パターン
* docker build または docker run でコンパイルが走る。結果物はVOLUME、docker cp、標準出力経由などの方法で取り出す。
```dockerfile
FROM ubuntu:trusty
RUN apt-get update -y
RUN apt-get install -y git golang-go
# Repo is https://gist.github.com/25f6379bc6e57e9adbe0
RUN git clone https://gist.github.com/25f6379bc6e57e9adbe0.git /usr/local/src/helloworld
WORKDIR /usr/local/src/helloworld
RUN chmod a+x ./make.sh
RUN mkdir /var/build
CMD ./make.sh # build and sleep
```
```bash
$ docker build -t lesson/builder builder/
$ id=$( docker run -d lesson/builder )
$ docker cp $id:/var/build/helloworld tmp
$ file tmp/helloworld
tmp/helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
```
* 今回はGoなのでMac用のバイナリも作れる(!!)
```bash
$ id=$( docker run -e GOOS=darwin -d lesson/builder )
$ docker cp $id:/var/build/helloworld tmp
$ ./tmp/helloworld
Hello, I am born in Docker!
```
* 上記の、ビルド時にgit cloneしてキャッシュするパターンは一番単純だが、CMDでチェックアウトもラップすればビルドを繰り返すことも出来るだろう。チェックアウトのキャッシュにVOLUMEが使える。
* [boot2dockerのイメージ](https://github.com/boot2docker/boot2docker/blob/master/doc/BUILD.md) がこのパターンと言える。
```bash
$ docker build -t boot2docker . && docker run --rm boot2docker > boot2docker.iso
```
## Command パターン
### Shell
* `docker run -ti $IMAGE /bin/bash` で対話的にシェルに入れる。
* 何かのアプリケーションというより、むしろデバッグ用途でよくやる。
```dockerfile
FROM ubuntu:trusty
ENV TERM screen-256color
ENV HELLO world
```
```bash
$ docker run -ti lesson/command-shell /bin/bash
root@883e836318b9:/# echo $HELLO
world
root@883e836318b9:/#
```
* `-ti` 、仮想端末を割り当ててSTDINを保持すると言うこと。 `docker exec` でも同様。
```
-i, --interactive=false Keep STDIN open even if not attached
-t, --tty=false Allocate a pseudo-TTY
```
### Wrapper
* コンテナ内部に環境を作り、 docker run からその内部でコマンドを実行する
* ENTRYPOINT使うとかっこよく引数を渡せる。
* 単体のコマンドをラップしてもあまり嬉しくないはずなので、後述のようにLLの環境ごとbuildする場合が多い。
#### Bundler
* 内部でチェックアウト、bundle installしてからのbundle execをラップする
* npmその他でも同じパターンができる
* 公式の [rubyベースイメージ](https://registry.hub.docker.com/_/ruby/) 、 [nodejsベースイメージ](https://registry.hub.docker.com/_/node/) を参照。
* 個人的に、Drone.ioの [CIのベースイメージ](https://github.com/drone/drone/blob/v0.2.1/README.md#images) として使われる bradrydzewski/ruby シリーズを良く使っている。そもそものベースである bradrydzewski/base も使い勝手が良い(go、Rubyなんかが一通りセットアップ済み)。
```dockerfile
FROM bradrydzewski/ruby:2.0.0
# Ruby project is https://gist.github.com/86e8021eeaaccf100541
RUN git clone https://gist.github.com/86e8021eeaaccf100541.git /home/ubuntu/sample-task
WORKDIR /home/ubuntu/sample-task
RUN /home/ubuntu/.rbenv/shims/bundle install --path vendor/bundle
ENTRYPOINT ["/home/ubuntu/.rbenv/shims/bundle", "exec", "rake"]
CMD ["-T"]
```
```console
$ docker run -t lesson/command-bundler
rake days_ago # Getting past days
rake multiply # Calculating multiply
rake sum # Calculating sum
$ docker run -t lesson/command-bundler sum
Calculating sum(1..10)...
55
$ docker run -t lesson/command-bundler days_ago
Getting 6 day(s) ago
2014-11-13 00:55:56 +0000
```
* ENTRYPOINT と CMD の違いは、...
* http://docs.docker.com/reference/builder/ を見ても分かりづらいかも。
* `CMD` は、あくまで `docker run` のイメージ名より後ろの引数が無い時のデフォルトの値。
* `ENTRYPOINT` は、CMDにより実行されるパラメータの前に挿入されるイメージ。
```dockerfile
ENTRYPOINT ["/usr/bin/bundle", "exec", "rake"]
CMD ["-T"]
```
* 上記であれば
* `docker run image` → `/usr/bin/bundle exec rake -T`
* `docker run image spec` → `/usr/bin/bundle exec rake spec`
* `docker run image spec --trace=true` → `/usr/bin/bundle exec rake spec --trace=true`
* `docker run --entrypoint /usr/bin/ruby image --version` → `/usr/bin/ruby --version`
## Data Volume Container パターン
* https://docs.docker.com/userguide/dockervolumes/ の通り。
* もうちょっと具体的な、シンプルなSinatraアプリケーションを用いて試してみる。
### Pusherアプリ
```ruby
require 'sinatra'
configure do
file = File.new("/data/access.log", 'a+')
file.sync = true
use Rack::CommonLogger, file
end
get '/' do
"Accessed: root"
end
get '/:slug' do
"Accessed: /#{params[:slug]}"
end
```
```dockerfile
FROM ruby:2.0.0-p598
RUN mkdir /data
ADD ./app.rb /root/app.rb
WORKDIR /root
RUN gem install sinatra
CMD ruby app.rb -o 0.0.0.0 -p $PORT
```
### Consumerアプリ
```ruby
require 'sinatra'
get '/' do
access = File.read("/data/access.log") rescue "none."
<<-EOH
<html><body>
<h1>Pusher's access log</h1>
<pre>
#{access}
</pre>
</body></html>
EOH
end
```
```dockerfile
FROM ruby:2.0.0-p598
RUN mkdir /data
ADD ./app.rb /root/app.rb
WORKDIR /root
RUN gem install sinatra
CMD ruby app.rb -o 0.0.0.0 -p $PORT
```
* 以上のアプリケーションコンテナを連携させる。
```bash
# データコンテナの起動
$ docker run -tid --name data001 -v /data ruby:2.0.0-p598 /bin/bash
# ログのpusher
$ docker build -t lesson/data-volume-pusher data-volume/pusher/
$ docker run -d -e PORT=8080 -p 8080 --name app001 --volumes-from data001 lesson/data-volume-pusher
# http://$(boot2docker ip):$(docker port app001 8080 | cut -f2 -d:) にアクセスする
# アクセスログがdata側も更新されている
$ docker exec data001 /usr/bin/tail /data/access.log
192.168.59.3 - - [19/Nov/2014 02:27:57] "GET / HTTP/1.1" 200 14 0.0021
192.168.59.3 - - [19/Nov/2014 02:27:57] "GET /favicon.ico HTTP/1.1" 200 22 0.0004
192.168.59.3 - - [19/Nov/2014 02:27:57] "GET /favicon.ico HTTP/1.1" 200 22 0.0003
192.168.59.3 - - [19/Nov/2014 02:27:59] "GET /hello HTTP/1.1" 200 16 0.0003
192.168.59.3 - - [19/Nov/2014 02:28:01] "GET /world HTTP/1.1" 200 16 0.0002
# pusherのログを表示するアプリ
$ docker build -t lesson/data-volume-consumer data-volume/consumer/
$ docker run -d -e PORT=8080 -p 8080 --name app002 --volumes-from data001 lesson/data-volume-consumer
# http://$(boot2docker ip):$(docker port app002 8080 | cut -f2 -d:) にアクセスする
# すると...
```

* app001側にアクセスするたびapp002の画面が更新されていく。
* この data001 コンテナの中身をバックアップするには次のようなコマンドで。
```bash
$ docker run --volumes-from data001:ro -v $(pwd)/tmp:/out ubuntu:trusty tar cvf /out/backup.tar /data
```
* 用途は色々思いつくが、
* サービスだけでなく、データもポータブルにできる(インポート/エクスポートが容易になる)
* DRDB のように用いることが出来るので、MySQLなどの安全なレプリケーション、高可用構成が実現できる
* などなど...
## Onbuild パターン
* 最近出てきた機能(といっても0.8からか...)。 http://deeeet.com/writing/2014/03/21/docker-onbuild/ の通り。
* Dockerfile単体でコンテナ生成のために使うと言うより、例えば、既に存在するプロジェクトをさっくり動かす用途に向いていると思う。
* Hubotプロジェクトのルートで以下のようなDockerfileを書く。
```dockerfile
FROM node:0.10-onbuild
CMD ["./bin/hubot", "-a", "shell"]
```
```bash
$ docker build -t lesson/onbuild-hubot onbuild/
$ docker run -ti lesson/onbuild-hubot
Hubot>
Hubot> hubot help
Hubot> Hubot adapter - Reply with the adapter
Hubot animate me <query> - The same thing as `image me`, except adds a few parameters to try to return an animated GIF instead.
Hubot echo <text> - Reply back with <text>
Hubot help - Displays all of the help commands that Hubot knows about.
Hubot help <query> - Displays all help commands that match <query>.
Hubot image me <query> - The Original. Queries Google Images for <query> and returns a random top result.
...
```
* ビルドのタイミングで、 Hubotプロジェクトを `ADD` し、 `npm install` を走らせる、を実行してくれる。
* (CMDはデフォルトでは `npm start` なので上書きしている)
## その他...
* ここに記したパターンは割とただの備忘録である。どちらかと言うとエンタープライズアプリケーションへの利用よりは、Dockerの学習の為に写経をする場合に役に立つのではないだろうか。
* あくまで筆者の短い経験から見てきたパターンを整理しただけで、ここに載らないパターンが今後も生まれるのでは、と思う。
* 一緒に https://docs.docker.com/userguide も眺めてほしい。 container linking とかここでは紹介してない。
* お前ら!こんな記事をありがたがってるんじゃない!まずDockerfileを書け!