はじめに

Dockerは何となく触ってみてはいたものの、Docker上で本格的にWebアプリケーションを構築したことがなかったので、練習も兼ねて趣味プロダクトを以下の構成で作ってみました。

恥ずかしながらDocker ComposeもNginxも今回初めて触ったので、いろいろ間違った使い方をしているところもあるかもしれませんが、お気付きの点があれば @ttskch まで一言いただければ幸いです。

ちなみに今回作ったのは audio2video というサービスで、音声ファイルを動画ファイルに変換するだけのものです。サービスの大まかな紹介については Qiitaに記事を書きました ので、よろしければこちらもご参照ください。

なお、今回作ったものはデータの永続化を必要としないアプリだったので、データボリュームの話とかは出てきません。

対象読者

今回はDockerの初歩的な解説等は割愛させていただきますので、最低限Dockerの概念を理解されている方に向けた内容となります。

実際に手を動かしながら読み進める場合は、事前にDockerエンジンおよびDocker Composeが動作する環境をお手元にご用意ください。先日パブリックベータになった Docker for Mac/Windowsをインストールするのが手っ取り早いと思います。

GitHubリポジトリ

本エントリーで作成したサンプルは こちらのGitHubリポジトリ で公開していますので、参考にしてみてください。(解説の手順ごとにコミットも分けてあります)

目次

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

image

ここまでは特に問題ないですね。

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

image

はい、動きませんね。ここで一旦ハマりました。

$ 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されていますね。この状態でブラウザで動作確認してみましょう。

image

無事に動きました!

ホストのブラウザから 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番ポートでアクセスしているかどうか」で判定することにしました。

再度ブラウザで確認してみましょう。

image

問題ないですね!

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している)

image

image

なんとなく狙いどおりに動いているっぽいですね。/ へのアクセスでは /usr/share/nginx/html/index.html が表示され、index.php へのアクセスでは /opt/work/web/index.php が表示されています。

では、index_dev.php にもアクセスしてみましょう。

image

おや?デバッグツールバーが404で表示できないというエラーが出ましたね。

image

コンソールを見てみると /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 配下にルーティング

image

image

この設定で、完全に期待どおりに動作するようになりました。

(最初は例として /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)

ごくごくシンプルですね。

  • nginxphp という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

image

image

ちゃんと 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リポジトリ で公開していますので、よろしければご参照ください。