はじめに
Dockerは何となく触ってみてはいたものの、Docker上で本格的にWebアプリケーションを構築したことがなかったので、練習も兼ねて趣味プロダクトを以下の構成で作ってみました。
恥ずかしながらDocker ComposeもNginxも今回初めて触ったので、いろいろ間違った使い方をしているところもあるかもしれませんが、お気付きの点があれば @ttskch まで一言いただければ幸いです。
ちなみに今回作ったのは audio2video というサービスで、音声ファイルを動画ファイルに変換するだけのものです。サービスの大まかな紹介については Qiitaに記事を書きました ので、よろしければこちらもご参照ください。
なお、今回作ったものはデータの永続化を必要としないアプリだったので、データボリュームの話とかは出てきません。
対象読者
今回はDockerの初歩的な解説等は割愛させていただきますので、最低限Dockerの概念を理解されている方に向けた内容となります。
実際に手を動かしながら読み進める場合は、事前にDockerエンジンおよびDocker Composeが動作する環境をお手元にご用意ください。先日パブリックベータになった Docker for Mac/Windowsをインストールするのが手っ取り早いと思います。
GitHubリポジトリ
本エントリーで作成したサンプルは こちらのGitHubリポジトリ で公開していますので、参考にしてみてください。(解説の手順ごとにコミットも分けてあります)
目次
- Step1. 普通にローカルでSilexアプリを動かす
- Step2. Docker上でSilexアプリを動かす
- Step3. 同じコンテナ内にNginxを立てる
- Step4. NginxとSilexを別コンテナにしてDocker Composeで連携する
- Step5. 本番環境として適用する
Step1. 普通にローカルでSilexアプリを動かす
まずは silexphp/Silex-Skeleton でアプリケーションを作成して動かしてみましょう。
$ composer create-project fabpot/silex-skeleton docker-compose-nginx-silex-sample ~2.0@dev
$ cd docker-compose-nginx-silex-sample
$ composer run
> echo 'Started web server on http://localhost:8888'
Started web server on http://localhost:8888
> php -S localhost:8888 -t web web/index_dev.php
ここまでは特に問題ないですね。
Step2. Docker上でSilexアプリを動かす
ひとまずPHP環境のDockerコンテナを作りましょう。
./Dockerfile
FROM php:7.0
# install composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php -r "if (hash_file('SHA384', 'composer-setup.php') === 'e115a8dc7871f15d853148a7fbac7da27d6c0030b848d9b3dc09e2a0388afed865e6a3d6b3c0fad45c48e2b5fc1196ae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer
オフィシャルのPHPイメージ を継承して Composeをインストール しただけのDockerfileです。これをビルドします。
$ docker build . -t silex-sample
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
silex-sample latest c15ad5cc878b About a minute ago 493.9 MB
php 7.0 2e2f36b7013d 6 days ago 490.3 MB
では、このコンテナでSilexアプリを動かしてみましょう。
$ docker run -it -v $(pwd):/opt/work -w /opt/work -p 8888:8888 silex-sample composer run
Running composer as root/super user is highly discouraged as packages, plugins and scripts cannot always be trusted
> echo 'Started web server on http://localhost:8888'
Started web server on http://localhost:8888
> php -S localhost:8888 -t web web/index_dev.php
はい、動きませんね。ここで一旦ハマりました。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f366e437df88 silex-sample "composer run" 2 seconds ago Up 2 seconds 0.0.0.0:8888->8888/tcp boring_brown
上記のとおり、docker ps
を見るとちゃんとホストの8888番ポートがコンテナの 0.0.0.0:8888
に転送されています。なのになぜかWebサーバーに接続できていません。
コンテナに入って原因を調べてみる
$ docker run -it -v $(pwd):/opt/work -w /opt/work -p 8888:8888 silex-sample bash
%
% apt-get update && apt-get install -y net-tools # 調査のためにnetstatが使いたいのでインストール
%
% composer run & # バックグラウンドでWebサーバーを起動
%
% netstat -naop --tcp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer
tcp6 0 0 ::1:8888 :::* LISTEN 13/php off (0.00/0/0)
どうやら8888番ポートはIPv6でLISTENされています。ホストとコンテナの間のポート転送はIPv4で行われているので、これが原因と思われます。
コンテナ内の /etc/hosts
を編集して localhost
のアドレスを 0.0.0.0
に固定してしまうという方法でも解決できそうですが、残念ながらコンテナ内の /etc/hosts
はビルド時にはRead OnlyでDockerfileから編集することができないので、Silexアプリの composer.json
の方を修正します。
"scripts": {
"run": [
"echo 'Started web server on http://localhost:8888'",
- "php -S localhost:8888 -t web web/index_dev.php"
+ "php -S 0.0.0.0:8888 -t web web/index_dev.php"
]
}
ソースを修正したら、一旦コンテナ内のプロセスを終了させて、起動しなおしましょう。
% jobs -l
[1]+ 33 Running composer run &
% kill -9 33
% jobs
[1]+ Killed composer run
% jobs
% # 何も出なくなればプロセスが終了できている
% composer run &
%
% netstat -naop --tcp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 39/php off (0.00/0/0)
今度はIPv4の 0.0.0.0
でLISTENされていますね。この状態でブラウザで動作確認してみましょう。
無事に動きました!
ホストのブラウザから index_dev.php
に接続できるようにする
動くには動きましたが、画面をよく見ると、Silex-Skeletonの web/index_dev.php
の処理によって外部サーバーからの接続が拒否されている旨が表示されています。
このままだとアプリの開発に支障があるので、接続を拒否する条件を変更して対処しておきましょう。
web/index_dev.php
+ /*
if (isset($_SERVER['HTTP_CLIENT_IP'])
|| isset($_SERVER['HTTP_X_FORWARDED_FOR'])
|| !in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1'))
) {
+ */
+ if (!preg_match('/:8888$/', $_SERVER['HTTP_HOST'])) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
いろいろ考えてみましたが、要は本番運用時に index_dev.php
へのアクセスを弾きたいだけなので、「8888番ポートでアクセスしているかどうか」で判定することにしました。
再度ブラウザで確認してみましょう。
問題ないですね!
Step3. 同じコンテナ内にNginxを立てる
次は、今作ったDockerイメージにNginxサーバーを追加して、 Nginx + PHP-FPM の構成でSilexアプリが動作するようにしてみましょう。
Nginx + PHP-FPMの構成については、以下の記事が大変参考になります。
nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する
とりあえずDockerfileを修正してみます。
- 継承元のイメージを
php:fpm
に変更 apt-get
でNginxをインストール- バーチャルホストの設定ファイルを設置(
default.conf
) - エントリーポイントでNginxとPHP-FPMの両方を起動
- FROM php:7.0
+ FROM php:fpm
〜略〜
+ # install nginx
+ RUN apt-get update
+ RUN apt-get install -y nginx
+
+ # config nginx
+ COPY ./default.conf /etc/nginx/conf.d/
+
+ CMD nginx && php-fpm
バーチャルホストの設定ファイルの元ネタとなるファイルもコードベースに追加します。
./default.conf
+ server {
+ listen 80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ }
+
+ location ~ \.php$ {
+ fastcgi_pass localhost:9000;
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME /opt/work/web/$fastcgi_script_name;
+ }
+ }
上記の default.conf
の内容は、
- Nginxが80番ポートをLISTENする
/
配下へのアクセスは/usr/share/nginx/html
配下にルーティングされる(Nginxのデフォルトのドキュメントルート).php
で終わるURIに対しては、localhostの9000番ポートで待っているPHP-FPMに処理が渡される- その際、ドキュメントルートは
/opt/work/web
となる
- その際、ドキュメントルートは
というような設定になっています。
一旦これで動作を確認してみましょう。
$ docker build . -t silex-sample
$ docker run -it -v $(pwd):/opt/work -w /opt/work -p 8888:80 silex-sample
ポート転送が 8888:80 に変わっていることに注意(コンテナ内は8888番ではなく80番をLISTENしている)
なんとなく狙いどおりに動いているっぽいですね。/
へのアクセスでは /usr/share/nginx/html/index.html
が表示され、index.php
へのアクセスでは /opt/work/web/index.php
が表示されています。
では、index_dev.php
にもアクセスしてみましょう。
おや?デバッグツールバーが404で表示できないというエラーが出ましたね。
コンソールを見てみると /css/main.css
にもアクセスできていないし、/index_dev.php/_profiler/
配下のURIにもアクセスできていません。
先ほどの default.conf
の内容を思い出してみましょう。
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
}
location ~ \.php$ {
fastcgi_pass localhost:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /opt/work/web/$fastcgi_script_name;
}
}
この設定に従うと、/css/main.css
も /index_dev.php/_profiler/*
も .php
で終わるURIではないので、/
側にルーティングされますね。しかし /usr/share/nginx/html
配下には該当するリソースがないので、当然のごとく404になるわけです。
default.conf
のルーティングの設定をもう少しちゃんと考える必要がありそうです。
結論としては以下のような感じになります。
server {
listen 80;
server_name localhost;
location / {
if ($request_uri ~ 'index_dev\.php') { # ... (1)
rewrite .* index_dev.php last;
}
if (!-f $request_filename) { # ... (2)
rewrite .* index.php last;
}
root /opt/work/web; # ... (3)
}
location ~ \.php$ {
fastcgi_pass localhost:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /opt/work/web/$fastcgi_script_name;
}
}
-
(1) URIに
index_dev.php
を含む場合はすべてindex_dev.php
にルーティング -
(2) (1) に該当せず、かつリクエストされたURIにファイルが実在しない場合はすべて
index.php
にルーティング -
(3) (1) にも (2) にも該当しない場合(=アセットへのリクエスト)は、
/opt/work/web
配下にルーティング
この設定で、完全に期待どおりに動作するようになりました。
(最初は例として /
の root
をNginxデフォルトの /usr/share/nginx/html
にしていましたが、実際にはこのディレクトリを見せる意味はないですね)
Step4. NginxとSilexを別コンテナにしてDocker Composeで連携する
最後に、Docker Composeを使ってNginxサーバーとPHP-FPMサーバーを別々のコンテナに分けましょう。(Dockerの運用原則は「1コンテナ1プロセス」です)
まずはDocker Composeの設定ファイルである docker-compose.yml
を作成しましょう。
version: '2'
services:
nginx:
build: ./docker/nginx # ... (1)
ports:
- '8888:80' # ... (2)
depends_on:
- php # ... (3)
volumes:
- .:/opt/work # ... (4)
php:
build: ./docker/php # ... (5)
volumes:
- .:/opt/work # ... (6)
ごくごくシンプルですね。
nginx
とphp
という2つのサービスを定義- (1)
nginx
サービスは./docker/nginx/Dockerfile
をビルドしてコンテナを作成 - (2)
nginx
サービスは8888:80
でポートを転送 - (3)
nginx
サービスはphp
サービスが起動したあとで起動される - (4)
nginx
サービスは.
をコンテナの/opt/work
にマウント - (5)
php
サービスは./docker/php/Dockerfile
をビルドしてコンテナを作成 - (6)
php
サービスは.
をコンテナの/opt/work
にマウント
docker-compose.yml
で使える各種ディレクティブについては以下の日本語ドキュメントが詳しいです。
http://docs.docker.jp/compose/compose-file.html
(1) と (5) でDockerfileの場所を指定したので、これに従ってファイルを配置し直しましょう。
$ tree docker
docker
├── nginx
│ ├── Dockerfile
│ └── default.conf
└── php
└── Dockerfile
docker/nginx/Dockerfile
FROM nginx
COPY ./default.conf /etc/nginx/conf.d/
せっかくDockerfileが独立したので、先ほどまでのように apt-get
で手動インストールせずに オフィシャルのnginxイメージ を活用します。
docker/nginx/default.conf
server {
listen 80;
server_name localhost;
location / {
if ($request_uri ~ 'index_dev\.php') {
rewrite .* index_dev.php last;
}
if (!-f $request_filename) {
rewrite .* index.php last;
}
root /opt/work/web/;
}
location ~ \.php$ {
- fastcgi_pass localhost:9000;
+ fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /opt/work/web/$fastcgi_script_name;
}
}
default.conf
の内容は先ほどまでとほぼ同じですが、一箇所だけ変更が必要です。
先ほどまではNginxとPHP-FPMは同じサーバー内で起動していましたが、今はコンテナを分けたことによってNginxサーバーから見たPHP-FPMサーバーのアドレスはlocalhostではなくなっています。
Docker Composeを使ってコンテナを起動すると、各コンテナの /etc/hosts
に自動的に 他のコンテナのIPアドレスがサービス名で登録される ため、上記のようにあまり深く考えずにサービス名をホスト名として使用することができます。便利ですね。
docker/php/Dockerfile
FROM php:fpm
# install composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php -r "if (hash_file('SHA384', 'composer-setup.php') === 'e115a8dc7871f15d853148a7fbac7da27d6c0030b848d9b3dc09e2a0388afed865e6a3d6b3c0fad45c48e2b5fc1196ae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
RUN php composer-setup.php
RUN php -r "unlink('composer-setup.php');"
RUN mv composer.phar /usr/local/bin/composer
こちらは先ほどまでのDockerfileからNginxに関する定義を削除しただけの内容です。
なお、Nginx・PHP-FPMとも、それぞれのコンテナのポートの解放やエントリーポイントの指定などは継承元のイメージに定義されているので今回の例では自前で追記する必要はありません。
エントリーポイントが正常に定義されていない(何かしらのプロセスがフォアグラウンドで起動しない)コンテナがあると期待どおりに動作しませんので、Dockerイメージを自作する際にはご注意ください。
起動してみる
docker-compose.yml
が置いてあるディレクトリで以下のコマンドを叩けば起動できます。
$ docker-compose up
この状態でブラウザで表示してみて、先ほどまでと同様に正常に動作することを確認してください。
Step5. 本番環境として適用する
本番環境として使う場合は、ホストの8888番ポートではなく80番ポートを転送するように docker-compose.yml
を修正しましょう。
version: '2'
services:
nginx:
build: ./docker/nginx
ports:
- - '8888:80'
+ - '80:80'
depends_on:
- php
volumes:
- .:/opt/work
php:
build: ./docker/php
volumes:
- .:/opt/work
ちゃんと index_dev.php
へのアクセスも拒否できていますね。
本番サーバー上で docker-compose up
する場合は、コンテナをバックグラウンドで起動したいので、以下のように -d
オプションを使用します。
$ docker-compose up -d
バックグラウンドで起動している各コンテナをまとめて終了させるには docker-compose.yml
が置いてあるディレクトリで
$ docker-compose down
とすればOKです。
おわりに
というわけで、Docker Compose + Nginx + Silex でとりあえずアプリケーションを動かすことができましたね。
ちなみに、Step5で「本番環境」と表現しましたが、実際に業務システムを本番運用するならもっと色々考えることが出てくると思います。今のままだとサーバーを再起動した時にコンテナが自動起動すらしませんし。本エントリーの内容はあくまで入門レベルと考えてください 😅
繰り返しになりますが、内容の間違いや「本番で使うならこういう点に気をつけた方がいいよ」等、ご意見がありましたらぜひ @ttskch までご連絡ください。
また、本エントリーで作成したサンプルは こちらのGitHubリポジトリ で公開していますので、よろしければご参照ください。