Skip to content

Instantly share code, notes, and snippets.

@udzura
Last active December 22, 2021 04:48
Show Gist options
  • Save udzura/1515e20d9d2f0bbf187f to your computer and use it in GitHub Desktop.
Save udzura/1515e20d9d2f0bbf187f to your computer and use it in GitHub Desktop.
私家版 Dockerfile Pattern

前提

Service パターン

Solo Service

  • 基本的なコンテナ
  • 単独のサービスを CMD で動かす
  • よくやるのがsshdの起動
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 ログインできるはず。
$ 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のおまじない
[email protected]'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の親プロセスも終了する仕組みを導入すべきだろう。
  • ラッパーシェルを雑に書くと吉。
#!/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
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
$ docker build -t lesson/service-multi service/multi/
$ id=$( docker run -d -P lesson/service-multi )
$ docker port $id 22 # or 27017
....
  • tailしているlogも参照可能
$ docker logs $id

Web Service

  • Web UIのあるツールを動かし、そのまま80番をEXPOSE
  • https://github.com/udzura/docker-munin を写経してみると良い(Ubuntuのバージョンが違うので微妙にコードは違う...)
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"]
#!/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
$ 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 は動作する/sbin/initを備えているので、ほぼ普通のUbuntuのようにサービスを動かせる
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が継承される
$ docker build -t lesson/service-upstart service/upstart/
$ id=$( docker run -d -P lesson/service-upstart )
  • 入って何かを実行できる。sshもいいけどdocker execでね
$ 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

FROM buildstep

ADD . /app
RUN /build/builder
CMD /start web
$ 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、標準出力経由などの方法で取り出す。
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
$ 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用のバイナリも作れる(!!)
$ 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のイメージ がこのパターンと言える。
$ docker build -t boot2docker . && docker run --rm boot2docker > boot2docker.iso

Command パターン

Shell

  • docker run -ti $IMAGE /bin/bash で対話的にシェルに入れる。
  • 何かのアプリケーションというより、むしろデバッグ用途でよくやる。
FROM ubuntu:trusty

ENV TERM screen-256color
ENV HELLO world
$ 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ベースイメージnodejsベースイメージ を参照。
  • 個人的に、Drone.ioの CIのベースイメージ として使われる bradrydzewski/ruby シリーズを良く使っている。そもそものベースである bradrydzewski/base も使い勝手が良い(go、Rubyなんかが一通りセットアップ済み)。
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"]
$ 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により実行されるパラメータの前に挿入されるイメージ。
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 パターン

Pusherアプリ

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
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アプリ

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
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
  • 以上のアプリケーションコンテナを連携させる。
# データコンテナの起動
$ 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:) にアクセスする
# すると...

37360cdf9881559b6b0282930ea10c11

  • app001側にアクセスするたびapp002の画面が更新されていく。
  • この data001 コンテナの中身をバックアップするには次のようなコマンドで。
$ 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を書く。
FROM node:0.10-onbuild

CMD ["./bin/hubot", "-a", "shell"]
$ 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を書け!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment