# Docker1.11でLaravel5.2アプリをコンテナ化
Laravelのウェブアプリ開発時も、できるだけプロダクション環境に近い環境がある方が、安心ですよね。 Webサーバもapacheだけでなく、nginx+php-fpmで、データベースもMySQLではなく、PostgreSQLでも動作させてみたいと思うこともあります。特にミドルウェアが指定されていない開発の場合、ベストな組み合わせを探りたいという気持ちも湧きます。
Dockerを使えば、開発マシンにソフトウェアをインストールしなくても、色々な組み合わせが試せそうですね。
# 環境
docker-toolboxではなく、homebrewで別々にインストールしています。
- 開発ホストOS: OSX 10.11.4
- docker-machine: 0.7.0
- docker client: 1.11.1
- docker-compose: 1.7.0
# コンテナ仕様
Dockerでは、1サービス/1コンテナが基本です。サービスとは、Webサーバ、データベースサーバなどを指します。
では、開発するWebアプリケーションは、どうすればよいでしょうか。Webサーバコンテナへ含めるべきでしょうか、それとも一つのサービスとして、コンテナを作成するように設計すべきでしょうか。
色々な組み合わせを試したいのであれば、Webアプリも一つのコンテナとする方が柔軟ではないでしょうか。
以下の3つのコンテナに分離します。アプリコンテナはデータボリュームコンテナとして作成します。
- データベースコンテナ: mysql5.6
- Webサーバコンテナ: apache2.4+php5.6
- アプリコンテナ: 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)処理
- OSのタイムゾーン設定
- Apache2.4+PHP5.6のインストール
- PHPのタイムゾーン設定
- 80ポートの公開
- httpd-develop.confの追加(VirtualHost)
# コンテナ作成時の(CMD)処理
- 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と呼ぶのかわかりません。 使い捨て(バックアップは可能)のボリュームなら、名前付きボリュームは簡単に利用できて、便利です。
ref. Data-only containers obsolete with docker 1.9.0?
ref. Manage data in containers
アプリコンテナイメージも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
# イメージ作成時の処理
- OSのタイムゾーン設定
- Webアプリディレクトリ生成(/var/www/app)
- LaravelアプリをWebアプリディレクトリへ追加(/var/www/app/current)
- storageディレクトリのオーナーをapacheユーザ(uid=1000)※に変更
- 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-&amp;gt;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へ公開してありますので、ご利用ください。