※ この記事は 2022年7月 に作成したものを一部改稿したものです。
Docker は、アプリケーションとそれが動作する最低限の環境を1つにまとめたコンテナと呼ばれる仮想環境を作成・配布・実行するためのソフトウェアおよびプラットフォームで、Go 言語で実装されています。
ホストマシンのカーネルを利用して仮想化を行うため、ハイパーバイザを利用してゲストOSを動作させる VirtualBox などの仮想マシンと比べて軽量で、環境の導入や配布、廃棄を手軽に行うことが可能です。
理解を深めるためにイメージを自作してみたので、概要や構成を紹介したいと思います。
以下、WSL 2 (Ubuntu) 上の Docker Engine で動作を確認しています。
$ docker version
Client: Docker Engine - Community
Version: 20.10.17
API version: 1.41
Go version: go1.17.11
Git commit: 100c701
Built: Mon Jun 6 23:02:57 2022
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 20.10.17
API version: 1.41 (minimum version 1.12)
Go version: go1.17.11
Git commit: a89b842
Built: Mon Jun 6 23:01:03 2022
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.6.6
GitCommit: 10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
runc:
Version: 1.1.2
GitCommit: v1.1.2-0-ga916309
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Docker イメージの概要
今回作成したアプリケーションは、Markdown 形式で記述されたテキストを HTML または PDF に変換してブラウザ上で表示する、という単純なものです。
作成したイメージは md-parser
という名前で Docker Hub に公開しています。
イメージはコンテナの土台 (雛形) となるテンプレートのようなものです。
Docker Hub は Docker イメージを公開・配布・取得するためのプラットフォームで、無料で利用する (ただしプライベートリポジトリは1つまで) ことができます。
まずは、作成したイメージを実際に使用してみます。
Docker のサービスを起動し、変換したい Markdown (.md) ファイルがあるディレクトリに移動して以下のコマンドを実行します。
$ docker run -d -p 80:80 -v $PWD:/tmp cralfa/md-parser
Unable to find image 'cralfa/md-parser:latest' locally
latest: Pulling from cralfa/md-parser
461246efe0a7: Pull complete
060bfa6be22e: Pull complete
b34d5ba6fa9e: Pull complete
8128ac56c745: Pull complete
44d36245a8c9: Pull complete
ebcc2cc821e6: Pull complete
24b7d80b6341: Pull complete
bcfa06b783d2: Pull complete
dd9c98fffa19: Pull complete
d275f7bd70eb: Pull complete
a7cd589031b4: Pull complete
13443ee00f50: Pull complete
91c8d63e27b9: Pull complete
Digest: sha256:d113ccc231c95f30b84f4f39444de6ae60e1e585d886476660f0fef83a88d72e
Status: Downloaded newer image for cralfa/md-parser:latest
3e77e7e0de428a04e5c945ddd270c4896a2541afbc4e8de985df1117cee98b51
docker run
コマンドを使用すると Docker イメージを基に新しいコンテナを作成しアプリケーションを実行することができます。
コマンドラインの末尾の cralfa/md-parser
で実行するイメージをリポジトリ名で指定しています。
Docker Hub のリポジトリではタグを用いてイメージのバージョンを区別しますが、イメージの取得時や作成時にタグを省略すると latest
タグが使用されます。
タグを指定するには「<リポジトリ名>:<タグ名>」のようにします。
cralfa/md-parser:latest
のイメージがローカルに存在しない場合、Docker Hub からイメージがダウンロードされます。
1度ダウンロードしたイメージはローカルに保管され次回以降はローカルのイメージが使用されます。
-d
はコンテナをバックグラウンドで起動するオプション、
-p 80:80
はホスト側の80番ポートをコンテナの80番ポートに接続する指定、
-v $PWD:/tmp
はホスト側のカレントディレクトリをコンテナの /tmp
ディレクトリにバインドマウントする指定です。
コンテナの起動が完了すると、コンテナのID (SHA-256 ハッシュ値、上記の例では 3e77e7e0de42...
) が表示されます。
起動が完了したら、ブラウザでローカルホストの80番ポート ( http://localhost/ ) に接続します。
すると、上の画像のように Markdown を変換した HTML が表示されます。
(Markdown のサンプルとして Ruby の 日本語版 README を使用しています。)
今度は、クエリ文字列として ?pdf
を付加したURL ( http://localhost/?pdf ) を表示してみます。
すると、少しの待ち時間の後 Markdown を変換した PDF が埋め込まれたページが表示されます。
(上の画像は Google Chrome での表示です。ブラウザによって表示や挙動が異なる場合があります。)
ページ上部のダウンロードボタンをクリックすると「名前を付けて保存」のウィンドウが表示され、PDFファイルをダウンロードすることができます。
Docker イメージの作成
Docker イメージを新しく作成するには、Dockerfile というテキストファイルを作成し、イメージを作り上げるために実行するコマンドライン命令を記述します。
md-parser
のイメージ作成時に使用した Dockerfile の内容は以下になります。
FROM nginx:1.23.1
RUN apt -qq update \
&& apt -qq -y install --no-install-recommends fcgiwrap spawn-fcgi pandoc wkhtmltopdf unzip \
&& apt clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/share/fonts
RUN curl https://moji.or.jp/wp-content/ipafont/IPAexfont/IPAexfont00401.zip -O \
&& unzip IPAexfont00401.zip \
&& chmod a+r IPAexfont00401/*.ttf \
&& fc-cache -fv IPAexfont00401 \
&& rm -rf IPAexfont00401.zip
EXPOSE 80
COPY default.conf /etc/nginx/conf.d/
WORKDIR /usr/share/nginx
RUN mkdir -p cgi-bin/output \
&& chown -R nginx:nginx .
COPY index.cgi cgi-bin/
COPY template/ cgi-bin/template
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
まず、FROM
命令でベースとなるイメージを指定します。
今回はブラウザからリクエストを受けてレスポンスを返すWebサーバが必要になるため、nginx のイメージを利用しています。
nginx のイメージは Alpine Linux をベースとしたものと Debian をベースにしたものが配布されています。
最初は軽量な Alpine Linux ベースのイメージを利用しようと考えていましたが、他に利用するツールのサイズが大きく最終的なイメージのサイズに大きな差がないことと、Bash が入っていないことから Debian ベースのイメージを利用しています。
次の RUN
命令では apt
コマンドを使用して 追加で必要になるソフトウェアのパッケージをインストールしています。
インストール後に apt clean
を実行したりキャッシュファイルを削除したりすることで、極力イメージのサイズが小さくなるようにしています。
続いて、WORKDIR
命令で作業ディレクトリを移動して日本語フォント (IPAフォント) のインストールを行っています。
今回 PDF への変換に利用している wkhtmltopdf
パッケージは日本語フォントを含んでいないため、インストールしないと日本語が脱落した PDF が生成されます。
次の EXPOSE
命令で80番ポートを listen するよう指定し、COPY
命令で nginx の設定ファイルをコンテナ内にコピー (上書き) しています。
コピー元の default.conf は Dockerfile と同じディレクトリに置いておきます。
続いて nginx のドキュメントルートに移動して必要なファイルを配置し、
最後にサービスの起動スクリプトをコピーしエントリーポイントとして実行するよう指定しています。
Dockerfile を作成したら、docker build
コマンドを実行して Dockerfile からイメージをビルドします。
$ docker build -t cralfa/md-parser .
Sending build context to Docker daemon 83.97kB
Step 1/12 : FROM nginx:1.23.1
1.23.1: Pulling from library/nginx
461246efe0a7: Already exists
060bfa6be22e: Already exists
b34d5ba6fa9e: Already exists
8128ac56c745: Already exists
44d36245a8c9: Already exists
ebcc2cc821e6: Already exists
Digest: sha256:bd06dfe1f8f7758debd49d3876023992d41842fd8921565aed315a678a309982
Status: Downloaded newer image for nginx:1.23.1
---> 670dcc86b69d
Step 2/12 : RUN apt -qq update && apt -qq -y install --no-install-recommends fcgiwrap spawn-fcgi pandoc wkhtmltopdf unzip && apt clean && rm -rf /var/lib/apt/lists/*
---> Running in bdb4c8bcb641
︙
Step 5/12 : EXPOSE 80
---> Running in a5c77d81e807
Removing intermediate container a5c77d81e807
---> 91f7331dc918
Step 6/12 : COPY default.conf /etc/nginx/conf.d/
---> f4ca26576ebc
Step 7/12 : WORKDIR /usr/share/nginx
---> Running in 92bad22dd555
Removing intermediate container 92bad22dd555
---> 409e87f820fd
Step 8/12 : RUN mkdir -p cgi-bin/output && chown -R nginx:nginx .
---> Running in 7e70bc592216
Removing intermediate container 7e70bc592216
---> 462af5572727
Step 9/12 : COPY index.cgi cgi-bin/
---> 083b271cfbff
Step 10/12 : COPY template/ cgi-bin/template
---> 4b8dafa074e5
Step 11/12 : COPY docker-entrypoint.sh /
---> b20677154f1e
Step 12/12 : ENTRYPOINT ["/docker-entrypoint.sh"]
---> Running in 787f61330883
Removing intermediate container 787f61330883
---> ed6d70667e34
Successfully built ed6d70667e34
Successfully tagged cralfa/md-parser:latest
作成したイメージは docker images
コマンドで確認できます。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cralfa/md-parser latest ed6d70667e34 2 minutes ago 582MB
nginx 1.23.1 670dcc86b69d 11 days ago 142MB
public.ecr.aws/sam/emulation-nodejs16.x rapid-1.52.0-x86_64 0625e2ecab4c 2 weeks ago 398MB
︙
イメージを Docker Hub で公開するためには、docker login
コマンドで Docker レジストリにログイン後、docker push
コマンドを実行します。
$ docker push cralfa/md-parser
Using default tag: latest
The push refers to repository [docker.io/cralfa/md-parser]
c56311591fde: Pushed
c2113d2a192a: Pushed
193a90899511: Pushed
f360a2bf7769: Pushed
9063d6aaccf5: Pushed
689661a3e16e: Pushed
0861aaab41bb: Pushed
abc66ad258e9: Layer already exists
243243243ee2: Layer already exists
f931b78377da: Layer already exists
d7783033d823: Layer already exists
4553dc754574: Layer already exists
43b3c4e3001c: Layer already exists
latest: digest: sha256:a08625584f8b4f5ad7600ff59d779e9915d1b66d6e44851aebaa4e8738bb6f2b size: 3030
Webアプリケーションの構成
続いて、md-parser
の中核となるWebアプリケーション部分の構成を紹介します。
md-parser
はブラウザからリクエストを受けたタイミングで動的に変換を行うつくりにしており、nginx では静的ファイルをホストするのではなくCGIスクリプト (Bash シェルスクリプト) を実行するようにしています。
nginx で CGI (Common Gateway Interface) を実行するためには fcgiwrap
と spawn-fcgi
という FastCGI 上で CGI を実行するためのツールが必要になるため、apt でインストールしています。
エントリーポイントの起動スクリプトでは、以下のように FCGI Wrap と nginx のサービスの起動を行っています。
#!/bin/bash -eu
spawn-fcgi -u nginx -g nginx -s /var/run/fcgiwrap.socket -M 766 -- /usr/sbin/fcgiwrap -f
nginx -g 'daemon off;'
nginx の設定ファイルは以下のようになっています。
server {
listen 80;
server_name localhost;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
location / {
root /usr/share/nginx/cgi-bin;
index index.cgi;
}
location ~ \.cgi$ {
root /usr/share/nginx/cgi-bin;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
fastcgi_index index.cgi;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
1つ目の location
ディレクティブでルートURLリクエスト時にCGIスクリプトを実行するよう指定し、
2つ目で FastCGI 用の設定値を指定しています。
リクエスト時に実行されるCGIスクリプトは以下のようになっています。
#!/bin/bash -x
readonly DATA_DIR='/tmp'
readonly BASE_DIR="$(dirname $0)"
readonly OUT_DIR="$BASE_DIR/output"
get_markdown () {
find $DATA_DIR/ -name '*.md' | head -n 1
}
serve_error () {
echo -e 'Content-Type: text/html\n'
cat $BASE_DIR/template/error.html | sed "s/\{\}/$1/"
exit 1
}
serve_html () {
local md=$(get_markdown)
[ -z "$md" ] && serve_error 'Please specify Markdown (.md) file.'
echo -e 'Content-Type: text/html\n'
pandoc -f gfm --template $BASE_DIR/template/template.html "$md"
}
serve_pdf () {
local md=$(get_markdown)
[ -z "$md" ] && serve_error 'Please specify Markdown (.md) file.'
local pdf_name=$(basename ${md%.md}.pdf)
pandoc -f gfm -t html5 "$md" -o $OUT_DIR/output.pdf || exit $?
echo 'Content-Type: application/pdf'
echo -e "Content-Disposition: inline; filename=\"${pdf_name}\"\n"
cat $OUT_DIR/output.pdf
}
if [[ "$QUERY_STRING" == **pdf** ]]; then
serve_pdf
else
serve_html
fi
exit 0
リクエストURLのクエリ文字列に「pdf」が含まれているかどうかで HTML に変換するか PDF に変換するかを場合分けし、Content-Type
ヘッダと変換後のファイルを含む HTTP レスポンスを標準出力に出力しています。
Markdown から HTML, PDF への変換には pandoc
コマンドを使用しています。
PDF のレスポンス時にセットしている Content-Disposition: inline
のヘッダで、ファイルをすぐにダウンロードさせるのではなくページとして表示するようブラウザに伝えています。
Dockerfile は基本的に root ユーザで実行されますが、CGIスクリプトは nginx ユーザで実行されます。
そのため、CGIスクリプトで読み書きするファイルやディレクトリは nginx ユーザ向けに権限を設定する必要があります。
Dockerfile でフォントファイルに読み取り権限を付与したり /usr/share/nginx
ディレクトリ以下の所有者を変更したりしているのはそのためです。