2016 年 5 月 4 日

Docker1.11でLaravel5.2アプリをコンテナ化

Docker1.11でLaravel5.2アプリをコンテナ化

Laravelのウェブアプリ開発時も、できるだけプロダクション環境に近い環境がある方が、安心ですよね。
Webサーバもapacheだけでなく、nginx+php-fpmで、データベースもMySQLではなく、PostgreSQLでも動作させてみたいと思うこともあります。
特にミドルウェアが指定されていない開発の場合、ベストな組み合わせを探りたいという気持ちも湧きます。

Dockerを使えば、開発マシンにソフトウェアをインストールしなくても、色々な組み合わせが試せそうですね。

環境

docker-toolboxではなく、homebrewで別々にインストールしています。

  1. 開発ホストOS: OSX 10.11.4
  2. docker-machine: 0.7.0
  3. docker client: 1.11.1
  4. docker-compose: 1.7.0

コンテナ仕様

Dockerでは、1サービス/1コンテナが基本です。サービスとは、Webサーバ、データベースサーバなどを指します。

では、開発するWebアプリケーションは、どうすればよいでしょうか。Webサーバコンテナへ含めるべきでしょうか、それとも一つのサービスとして、コンテナを作成するように設計すべきでしょうか。

色々な組み合わせを試したいのであれば、Webアプリも一つのコンテナとする方が柔軟ではないでしょうか。

以下の3つのコンテナに分離します。アプリコンテナはデータボリュームコンテナとして作成します。

  1. データベースコンテナ: mysql5.6
  2. Webサーバコンテナ: apache2.4+php5.6
  3. アプリコンテナ: Laravel.5.2.31

Dockerホストの作成と起動

まず、Dockerホストをdockerという名前で、virtualboxで作成しておきます。

$ docker-machine create --driver virtualbox docker

$ docker-machine ls
NAME     ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER    ERRORS
docker   *        virtualbox   Running   tcp://192.168.99.100:2376           v1.11.1

dockerホストの環境変数を設定します。

eval $(docker-machine env docker)

これでDockerホストが準備できました。

Dockerfiles

コンテナのDockerfileは、ディレクトリを作成し、その配下に定義します。

docker/

├── mysql - データベースコンテナ
│   └── Dockerfile
├── web - Webサーバコンテナ
│   ├── Dockerfile
│   └── httpd-develop.conf
└── app - アプリコンテナ
    ├── Dockerfile
    ├── .dockerignore
    └── laravel

データベースコンテナ

公式のmysqlイメージから生成します。このイメージにタイムゾーンをJSTに設定するコマンドを追加しています。Dockerは既存のイメージを継承して、新しいイメージを作成できます。

mysql/Dockerfile

FROM mysql:latest
MAINTAINER kan
# timezone
RUN cp -p /usr/share/zoneinfo/Japan /etc/localtime

Webサーバコンテナ

元のイメージは軽量なAlpine Linuxを利用しています。イメージのサイズが小さくて済みます。軽量化に関して、以下の記事を参考にさせていただきました。

ref. Alpine Linux で軽量な Docker イメージを作る – Qiita

このコンテナでもタイムゾーンをJSTに設定し、Apache2とPHPをインストールします。タイムゾーンの変更方法は、以下の記事を参考にさせていただきました。

ref. Alpine Linux でタイムゾーンを変更する – Qiita

httpd-develop.confをconf.dへコピーします。内容は後述します。
EXPOSEで80ポートを公開します。
RUN命令はイメージ作成時に実行するコマンドを、CMD命令はコンテナを作成するとき実行するコマンドを指定します。

内容は、pottava/php:5.6のものを利用させていただきました。
このイメージを元に作成してもよかったのですが、インストールするPHP拡張モジュールなどがわかるようにしておきたかったので、こうしてあります。

イメージ作成時の(RUN)処理

  1. OSのタイムゾーン設定
  2. Apache2.4+PHP5.6のインストール
  3. PHPのタイムゾーン設定
  4. 80ポートの公開
  5. httpd-develop.confの追加(VirtualHost)

コンテナ作成時の(CMD)処理

  1. httpdの起動

web/Dockerfile

FROM gliderlabs/alpine:latest
MAINTAINER kan
# timezone for OS
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# install Apache2+PHP
RUN apk --no-cache add apache2 php-apache2 \
  php php-fpm php-json php-zlib php-xml php-pdo php-phar \
  php-openssl php-curl php-pdo_mysql php-mysql php-mysqli \
  php-gd mysql-client \
    && mkdir -p /run/apache2/
# timezone for PHP
RUN echo 'date.timezone = Asia/Tokyo' > /etc/php/conf.d/timezone.ini
COPY httpd-develop.conf /etc/apache2/conf.d/
EXPOSE 80
CMD ["httpd", "-D", "FOREGROUND"]

VirtualHostを定義したhttpd-develop.confを追加していますが、ローカルのhttpd-develop.confをコンテナの/etc/apache2/conf.d/httpd-develop.confにマウントすることで、追加することもできます。

web/httpd-develop.conf

<Directory /var/www/app/current/public>
  Require all granted
</Directory>

<VirtualHost *:80>
  DocumentRoot /var/www/app/current/public
</VirtualHost>

アプリコンテナ

アプリコンテナを作成する前に、Laravelのプロジェクトをcomposerでインストールしておきます。

$ cd app
$ composer create-project laravel/laravel --prefer-dist

アプリコンテナはデータボリュームコンテナとして作成します。DOC(data-only-containers)パタンは現在推奨されず、代わりに名前付きボリューム(named volumes)の利用が推奨されています。しかし、このようなアプリコンテナもDOCと呼ぶのかわかりません。

使い捨て(バックアップは可能)のボリュームなら、名前付きボリュームは簡単に利用できて、便利です。

アプリコンテナイメージもAlpine Linuxを元に作成しています。

app/Dockerfile

FROM gliderlabs/alpine:latest
MAINTAINER kan
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN mkdir -p /var/www/app
ADD laravel /var/www/app/current
RUN chown -R 1000 /var/www/app/current/storage
VOLUME /var/www/app/current

イメージ作成時の処理

  1. OSのタイムゾーン設定
  2. Webアプリディレクトリ生成(/var/www/app)
  3. LaravelアプリをWebアプリディレクトリへ追加(/var/www/app/current)
  4. storageディレクトリのオーナーをapacheユーザ(uid=1000)※に変更
  5. Webアプリディレクトリのマウントポイント生成

※必ずしもapacheユーザのuidが1000とは限りませんが、ウェブサーバコンテナのapacheユーザの値を採用しています。

3のADDでは、.dockerignoreに記述したファイルは追加されません。実行環境でデプロイしたくないファイル名を記述しておきます。下位ディレクトリごとに.dockerignoreが書けると便利なのですが、Dockerfileが存在するディレクトリに存在するもののみ有効なようです。

web/.dockerignore

laravel/.git
laravel/.gitattributes
laravel/.gitignore
laravel/composer.json
laravel/composer.lock
laravel/.env.example
laravel/readme.md
laravel/package.json
laravel/gulpfile.js
laravel/node_modules
laravel/tests

4は、WebサーバコンテナのApache(Laravelアプリ)がファイルを書き込めるようにオーナーをapacheに変更しています。

本当はこのstorageは名前付きボリュームで作成したかったのですが、storage下にディレクトリ構造作成をどこで行えばよいかわからず、このまま利用することにしました。Laravelがこのディレクトリに書き込むとき、ディレクトリがなければ、作成する仕様なら問題なかったのですが。

5は、Webサーバコンテナがマウントするためのものです。

コンテナの操作

これでコンテナイメージを作成する準備が整いました。今回はdocker-composeを利用するところまで行いますが、まずは単体でコンテナを操作し、手順や動作を確認しながら、docker-compose.ymlを作成します。

コンテナのイメージ作成

初めに、mysqlコンテナイメージをビルドします。イメージをビルドするには、少し時間が掛かりますが、そこからコンテナを起動するのは、一瞬です。

$ docker build -t mysql:develop ./mysql
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM mysql:latest
latest: Pulling from library/mysql
・・・
Successfully built d85c7659cb1d

-tは、コンテナイメージの名前を指定しています(name:tag)。

正常にビルドできれば、docker imagesでイメージを一覧します。mysqlのイメージが2つありますが、一つは元となったイメージで、developタグが付いているのが、今ビルドしたイメージです。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mysql               develop             d85c7659cb1d        11 minutes ago      374.1 MB
mysql               latest              63a92d0c131d        2 weeks ago         374.1 MB

同様に、Webサーバ、Webアプリのイメージもビルドしておきます。

$ docker build -t web:develop ./web
$ docker build -t app:develop ./app

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
app                 develop             9ad7118a4f52        9 minutes ago       28.68 MB
web                 develop             3c198f429f10        10 minutes ago      73.23 MB
mysql               develop             d85c7659cb1d        18 minutes ago      374.1 MB
mysql               latest              63a92d0c131d        2 weeks ago         374.1 MB
gliderlabs/alpine   latest              8944964f99f4        4 weeks ago         4.798 MB

さきほどと同じ理由で、gliderlabs/alpineイメージも作成されているのがわかりますね。また、gliderlabs/alpineから生成したイメージ(web,app)が非常にコンパクトであることがわかります。

コンテナの起動

では、順番にコンテナを起動していきましょう。最初にアプリコンテナを起動します。呆気無く起動するはずです。docker ps -aでプロセスを確認します。

$ docker create --name app app:develop true
83db6309d31e4502517472e614d9a07090e46835118e545564d3a11bfc1d894f

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
83db6309d31e        app:develop         "true"              10 minutes ago      Created                                 app

appという名前でコンテナが起動しています。COMMANDが”true”になっているのは、trueコマンド(0を返すだけのコマンド)を実行させたからです。createするときに何かコマンドがないといけないようなので、trueコマンドを指定しています。別にecho,lsでも何でもかまいません。

run命令でもいいのですが、このコンテナは特にデーモンがあるわけではないので、createだけで大丈夫です。恐らくrun命令はcreate+start命令ではないかと推測します。

次に、このアプリコンテナを実行するウェブサーバコンテナを起動します。コンテナを起動するときに、アプリコンテナのマウントポイントをマウントします。

$ docker run -d --volumes-from app --name web -p 80:80 web:develop
069c3fee9e49e8741402ad687a71a764870ce69a38804c0b0ddea284ed87e266

volumes-fromで、アプリコンテナの名前を指定します。-pは、開発ホストの80ポートをコンテナの80ポートへフォワードさせる指定です。

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                 CREATED             STATUS              PORTS                NAMES
069c3fee9e49        web:develop         "httpd -D FOREGROUND"   10 minutes ago      Up 22 seconds       0.0.0.0:80->80/tcp   web
83db6309d31e        app:develop         "true"                  17 minutes ago      Created                                  app

httpdが起動しているのが、わかります。curlコマンドで確認してみましょう。コンテナのIPアドレスは、docker-machine ip dockerでわかります。

$ curl $(docker-machine ip docker) 2> /dev/null | grep Laravel
        <title>Laravel</title>
                <div class="title">Laravel 5</div>

データベースコンテナを起動していませんが、Laravelのscaffordは特にデータベースがなくても動作しますので、割愛しています。

コンテナの停止

一度、コンテナを停止してみましょう。名前でもIDでもかまいません。

$ docker stop web

ひとつずつ停止するのもいいですが、shのコマンド置換を利用して全てを停止することもできます。

$ docker stop $(docker ps -aq)
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                 CREATED             STATUS                     PORTS               NAMES
069c3fee9e49        web:develop         "httpd -D FOREGROUND"   24 minutes ago      Exited (0) 8 minutes ago                       web
83db6309d31e        app:develop         "true"                  31 minutes ago      Created                                        app

コンテナの削除

必要ないコンテナを削除することもできます。コンテナを削除するには、コンテナを停止する必要があります。一旦、全てのコンテナを削除してみます。

$ docker rm -v $(docker ps -aq)
$ docker ps -a

-vオプションはボリュームもあわせて削除するオプションです。コンテナを削除する前にどのようなボリュームがあるか調べてみます。

$ docker volume ls
DRIVER              VOLUME NAME
local               de46a080b291ec4c9295589bf289e040997e89a6b256b83bc201e2e71cb5bd36

なんのボリュームかよくわからないですね。アプリコンテナのものなんですが、確認したいときは、inspect命令を利用するとよいでしょう。

$ docker inspect app
...
"Mounts": [
  {
    "Name": "de46a080b291ec4c9295589bf289e040997e89a6b256b83bc201e2e71cb5bd36",
    "Source": "/mnt/sda1/var/lib/docker/volumes/de46a080b291ec4c9295589bf289e040997e89a6b256b83bc201e2e71cb5bd36/_data",
    "Destination": "/var/www/app/current",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
],
...

ボリュームの情報を参照することができました。安心して削除できますね。

開発ホストのディレクトリをマウントしてコンテナ起動

LaravelアプリをDockerコンテナとして動作させることができました。しかし、ソースコードを修正するたびに、イメージを作成し直すのは、面倒ですね。

開発ホストのLaravelアプリディレクトリをマウントして、Webサーバコンテナを起動する方法もあります。

逆にプロダクション環境へデプロイするなら、イメージを作成する前に、最適なビルド(Ansibleやshellスクリプト)が必要になるでしょう。

もし、コンテナがあれば停止・削除しておきます。

$ docker create --name app app:develop true
$ docker run -d -v /Users/kan/sandbox/docker/app/laravel:/var/www/app/current --name web -p 80:80 web:develop

-vオプションでマウント(host directory path:container directory path)します。

これでは、PHPビルトインサーバとあまり変わりませんが、できるだけプロダクションに近い環境で開発したいときは、有効でしょう。

このとき、このディレクトリの所有者はだれになるのだろうと思って調べてみました。exec命令は、指定したコンテナでコマンドを実行します。

docker exec -it web ls -ld /var/www/app/current
drwxr-xr-x    1 apache   50             782 May  2 21:24 /var/www/app/current

apacheユーザ所有となっているので、わざわざ開発ホストのディレクトリのパーミッションを設定することはありませんでしたが、なぜ、このように設定されるのか、理解していません。

前述のData-only containers obsolete with docker 1.9.0?の発言では、volume createにはそのようなオプションがありそうですが、containerのVOLUME命令、-vオプションに同様なものを見つけられませんでした。

in 1.11 you can supply mount opts for the volume to use,
so if the underlying filesystem you are mounting supports uid/gid,
you can specify those... 
e.g. docker volume create --opt type=bindfs --opt o=uid=1000,gid=1000

コンポーズファイル

毎回同じ手順でdockerコマンドを使うのは苦痛ですね。ある程度定型化できれば、docker-composeを使って、まとめてしまいましょう。

よくみれば、いままでdockerコマンドで指定したオプションがそのまま書けることがわかります。

コンポーズファイルでは、データベースコンテナも起動しています。公式のmysqlイメージは非常に便利で、環境変数でデータベース、テーブル、ユーザを指定しておくと自動的に作成してくれます。

Laravelの環境設定も.envではなく環境変数で設定しています。ただ、コンテナでartisanコマンドを起動したいので、.envファイルも含めています。

docker-compose.yml

version: '2'
services:
  mysql:
    image: mysql:develop
    container_name: mysql
    build: ./mysql
    environment:
      - MYSQL_DATABASE=develop
      - MYSQL_USER=develop
      - MYSQL_PASSWORD=develop
      - MYSQL_ROOT_PASSWORD=develop
  app:
    image: app:develop
    container_name: app
    build: ./app
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_DATABASE=develop
      - DB_USERNAME=develop
      - DB_PASSWORD=develop
    command:
      - "true"
  web:
    image: web:develop
    container_name: web
    build: ./web
    ports:
      - 80:80
    links:
      - mysql
    volumes_from:
      - app:rw
#    command: [php, /var/www/app/current/artisan, migrate]

マイグレーションの実行をコメントアウトしてあります。どこかでマイグレーションしたかったのですが、ここではまだmysqlとの接続ができず、エラーになってしまいます。

実際やってみると、以下のようにWebサーバコンテナがExitしています。

$ docker-compose up -d
Name               Command               State     Ports
----------------------------------------------------------
app     true                             Exit 0
mysql   docker-entrypoint.sh mysqld      Up       3306/tcp
web     php /var/www/app/current/a ...   Exit 1

こんなときは、logs命令で確認します。

$ docker logs web
[PDOException]
  SQLSTATE[HY000] [2002] Connection refused

接続できるまでリトライするスクリプトを書けばいいのかなと思っていますが、今のところコンテナ起動後、手動で実行するようにしています。私の開発スタイルでは、自分でマイグレーションのタイミングを取りたいので、特に不便はありません。

$ docker exec -it web php /var/www/app/current/artisan migrate

イメージ作成からコンテナ実行

upで実行すれば、イメージがなければ作成しますが、手順として覚えておくとよいでしょう。

$ docker-compose build
$ docker-compose up -d

コンテナの停止と削除

docker-composeにもdockerコマンドと同様なコマンドがありますので、ここでは、コンテナ停止から削除、さらにイメージも削除する方法だけご紹介します。

$ docker-compose down -v --rmi all

運用デプロイメント

開発環境ならこの程度でもよさそうですが、プロダクションのデプロイメントでは、コンテナのリソース設定、マウントするボリュームのサイズや権限、バックアップ方法、モニタリングなど、考慮すべきことは多くありそうです。

大規模な運用環境では、Docker Swarmによるクラスタリングや、さらにKubernetes,Mesos+Chronos+Marathonなどでスケジューリング・リソース管理するそうです。Dockerのエコシステムは今後さらに拡張しそうですね。

最後に

まだscaffordを動作させただけなので、本格的な開発や動作において、誤りがあったり、何か問題が生じることがあるかもしれませんが、ご了承ください。

参考までに、この記事で作成したDockerfile,docker-compose.ymlはhttps://github.com/notice/docker-laravelへ公開してありますので、ご利用ください。