PHP
MySQL
laravel
docker
alpine

Laravel + PHP7.3 + MySQL5.7の環境をDocker(with Alpine Linux)で構築した(コード/解説付き)

概要

こんにちは。Firebaseに訓練された高専卒Webエンジニアの @mejileben です。

Dockerをずっと毛嫌いしていたので、ガッツリ環境構築にチャレンジしてみました。
勉強も兼ねてなので、Laradockは使いません。
前提として、本番運用ではなく個人開発環境としての運用を想定します。
また、複数人でチーム開発することも考えて、設定ファイル等を共有/理解しやすい形にしておければ尚良と思っています。

設定内容

ディレクトリ構成

.
├── Dockerfile_nginx
├── Dockerfile_php
├── docker-compose.yml
├── etc
│   ├── logs
│   │   └── nginx
│   │       ├── access.log
│   │       └── error.log
│   ├── mysql
│   │   ├── env
│   │   │   └── setting.env
│   │   └── mysql_conf
│   ├── nginx
│   │   └── conf.d
│   │       └── default.conf
│   └── php
│       ├── env
│       │   └── setting.env
│       └── php.ini
├── laravel
└── mnt
    └── mysql

以下、docker-compose.ymlから順に、NginxやMySQLも含めた各設定ファイルの内容を提示、解説していきます。

docker-compose.yml

docker-compose.yml
version: "3"

services:
  nginx:
    build:
      context: .
      dockerfile: ./Dockerfile_nginx
    volumes:
      - ./etc/logs/nginx:/etc/nginx/logs
      - ./etc/nginx/conf.d:/etc/nginx/conf.d
      - ./laravel:/var/www/laravel
    ports:
      - 8081:80
    links:
      - phpfpm
    depends_on:
      - phpfpm
  phpfpm:
    build:
      context: .
      dockerfile: ./Dockerfile_php
    volumes:
      - ./laravel:/var/www/laravel
    links:
      - mysql
    depends_on:
      - mysql
    env_file:
      ./etc/php/env/setting.env
  mysql:
    image: mysql:5.7
    volumes:
      - ./mnt/mysql:/var/lib/mysql
      - ./etc/mysql/mysql_conf:/etc/mysql/conf.d
    env_file:
      ./etc/mysql/env/setting.env
    ports:
      - 13306:3306

思想としては、NginxとかMySQLの設定ファイルはetcの中に詰め込んであげて、DockerのゲストOSにマウントしてあげます。
これがベストプラクティスかどうかは、運用してみて決めるかなとは思うのですが、実際問題Nginxの設定ファイルをGit管理したことってあまりなくて、もしetc/nginx配下をLaravelソースと一緒にGit管理できたら、運用面で知見が得られそう、とは思います。
あくまで個人開発環境としてのDockerを想定しているので、NginxのConfをGit管理すること自体に問題はないかなと感じています。

また、NginxのログファイルもホストOS./etc/logs/nginxにマウントするようにしています。ログの調査もこれで捗りますね。

localhostはポート番号80を取りたくないので8081番を使うように設定しました。

PHP

Dockerfile

WORKDIRをソースコードのrootにしておくと、あとでcomposerを使ってLaravelのセットアップをするときにわざわざDockerゲスト内に入らなくていいのでお得です。

phpのalpineを利用します。

Dockerfile_php
FROM php:7.3.0RC6-fpm-alpine3.8

# setting working directory to source code root
WORKDIR /var/www/laravel

# copy php.ini
COPY ./etc/php/php.ini /usr/local/etc/php/

# install pdo, etc...
RUN apk update && apk add --no-cache \
  freetype-dev libjpeg-turbo-dev libpng-dev libmcrypt-dev \
  git vim unzip tzdata \
  libmcrypt-dev \
  libltdl \
  && docker-php-ext-install pdo_mysql mysqli mbstring gd iconv \
  && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
  && apk del tzdata \
  && rm -rf /var/cache/apk/*

# install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

このDockerfileは、先のdocker-compose.ymlにてphpfpmdockerfile: ./Dockerfileで指定しているファイルになります。

やっていることは、PHPのalpineのDockerイメージに対して、php.iniをホストOSからコピーして配置してあげて、あとはalpine linuxのパッケージマネージャであるapkを使ってもろもろパッケージを入れています。
最後にLaravelのセットアップのためにPHPのパッケージマネージャであるcomposerも入れておきます。

alpine linuxapkというパッケージマネージャを使っているのを知らず、しばらく悪戦苦闘しましたが、Alpine Linux入門 -内部構造とapkでパッケージインストール編-の記事を参考に、コマンドの叩き方を調べたのと、いくつか記事を見ている中で、これくらいのパッケージをapk addすればいいやろ、というのを入れるようなDockerfileになっています。

補足(Alpine Linuxにおけるタイムゾーンの設定について)

Alpine Linuxを使った場合、タイムゾーンを日本(東京)に変更する設定がdocker-compose.ymlからできないらしいです。手元ではできませんでした。

そこで、Alpine Linux でタイムゾーンを変更するを見て、Dockerfiletzdataapk addした上で、用済みになった部分を全てdeleteする形で、Asia/Tokyoの時刻に合わせるようにしました。

あとで、Nginxのほうでもこの対策は実施していますが、どちらかというとアクセスログなどで時刻を参照するNginxのほうがタイムゾーンの設定は必要ですね。

これだけでもQiitaなりMediumに書けそうなくらい罠ですねw

補足(mcryptについて)

mcryptっていうパッケージも調べた範囲で出てきたので入れようとしたのですがエラーで入らず・・・不要だと思っておきます。

php.ini

php.iniには最低限必要なものを。

php.ini
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"
setting.env
DB_HOST=mysql
DB_DATABASE=mejileben
DB_USERNAME=root
DB_PASSWORD=pass

Nginx

先程も書きましたが、タイムゾーンの設定のために、tzdataを入れています。

Dockerfile

Dockerfile_nginx
FROM nginx:1.15.7-alpine

RUN apk --update add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata && \
    rm -rf /var/cache/apk/*

default.conf

laravelのrootディレクトリは、docker-compose.ymlの設定で、ゲストOSの./laravelにマウントしています。
後ほど、コンテナに入り込んで/var/www/laravelにLaravelをセットアップします。

開発環境のつもりなので、そこまで本気のセッティングはしないでおきます。

ここで設定するaccess_log/error_logはホストOSにもマウントしているので見ることができるようになるはずです。

default.conf
server {
    listen       0.0.0.0:80;
    server_name  localhost;
    charset      utf-8;

    root /var/www/laravel/public;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    index index.php;

    location / {
        access_log  /etc/nginx/logs/access.log main;
        error_log   /etc/nginx/logs/error.log;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass  phpfpm:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include       fastcgi_params;
    }
}

補足(Nginx✕Alpine Linuxについて)

Alpine Linux で Docker イメージを劇的に小さくするあたりを参考にして、もうすこしゴリゴリにDockerfileを書いても良いかもしれません。

MySQL

setting.env
MYSQL_ROOT_PASSWORD=pass
MYSQL_DATABASE=mejileben

config用のディレクトリもマウントしているけど、特に今の時点で置くものがないので置いてないです。

docker-compose up

ここまでできたら、docker-compose upを叩きます。

[mejileben]$ docker-compose up
Starting laravel_docker_mysql_1_aad144ffc82c ... done
Creating laravel_docker_phpfpm_1_526cdb24bb99 ... done
Creating laravel_docker_nginx_1_2244d1f9f74f  ... done
Attaching to laravel_docker_mysql_1_aad144ffc82c, laravel_docker_phpfpm_1_1353a5b38614, laravel_docker_nginx_1_96e7d440124a
mysql_1_aad144ffc82c | Initializing database
phpfpm_1_1353a5b38614 | [21-Dec-2018 03:04:11] NOTICE: fpm is running, pid 1
phpfpm_1_1353a5b38614 | [21-Dec-2018 03:04:11] NOTICE: ready to handle connections
mysql_1_aad144ffc82c | 2018-12-21T03:04:10.371603Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
...

エラーパターンとして、このようなログが出たことがありました。

[mejileben]$ docker-compose up  
Starting laravel_docker_mysql_1_aad144ffc82c ... error

ERROR: for laravel_docker_mysql_1_aad144ffc82c  Cannot start service mysql: b'OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:402: container init caused \\"rootfs_linux.go:58: mounting \\\\\\"/Users/saitoyus/Documents/dev/laravel_docker/etc/mysql/mysql_conf\\\\\\" to rootfs \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged\\\\\\" at \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged/etc/mysql/conf.d\\\\\\" caused \\\\\\"not a directory\\\\\\"\\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type'

ERROR: for mysql  Cannot start service mysql: b'OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:402: container init caused \\"rootfs_linux.go:58: mounting \\\\\\"/Users/saitoyus/Documents/dev/laravel_docker/etc/mysql/mysql_conf\\\\\\" to rootfs \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged\\\\\\" at \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged/etc/mysql/conf.d\\\\\\" caused \\\\\\"not a directory\\\\\\"\\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type'
ERROR: Encountered errors while bringing up the project.

この場合、Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected typeというログの通り、docker-compose.ymlで指定しているマウントするディレクトリが、存在しないというのが原因です。
僕の場合、

docker-compose.yml
      - ./etc/mysql/mysql_conf:/etc/mysql/conf.d

のmysql_confが、フォルダではなくファイルになってしまっていたことが原因でした。
vimのNERDTreeから作ったのでコマンドミスしてファイルになってしまってましたw(言い訳)。

mysqlを起動したことで、ホストOSの./mnt/mysqlに中身ができています。

[mejileben]$ tree mnt -L 2                              (git)-[master]
mnt
└── mysql
    ├── auto.cnf
    ├── ca-key.pem
    ├── ca.pem
    ├── client-cert.pem
    ├── client-key.pem
    ├── ib_buffer_pool
    ├── ib_logfile0
    ├── ib_logfile1
    ├── ibdata1
    ├── ibtmp1
    ├── mysql
    ├── performance_schema
    ├── private_key.pem
    ├── public_key.pem
    ├── server-cert.pem
    ├── server-key.pem
    ├── sys
    └── mejileben

5 directories, 14 files

Laravel用のソースコードセットアップ

起動が完了したら、自身のlocalhost上でLaravelが起動しているはずですが・・・
http://localhost:8081
を開くと、Not Foundになってしまいます。ページが開けません。

docker-compose upしているシェルの画面を開くと、

nginx_1_96e7d440124a | 172.18.0.1 - - [21/Dec/2018:03:05:07 +0000] "GET / HTTP/1.1" 403 555 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "-"
nginx_1_96e7d440124a | 2018/12/21 03:05:07 [error] 6#6: *1 directory index of "/var/www/laravel/public/" is forbidden, client: 172.18.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost"
phpfpm_1_1353a5b38614 | 172.18.0.4 -  21/Dec/2018:03:05:08 +0000 "GET /index.php" 404

などと出ていることが確認できます。
※ここでは-dをつけずに起動していますが、普段はdocker-compose up -dすることのほうが多いです。

これはLaravelの肝心のソースコードが入っていないことが原因なので、Laravelのソースコードを入れましょう。

docker-compose upしていることで、もうコンテナ上でゲストOSが起動しているので、そこの中に入ってあげて、Laravelのソースコードをcomposer使って入れてあげる流れになります。

当然ですが、ここでホストOSにcomposerをinstallしてしまうとDockerの意味がなくなってしまいます。

ですので、立ち上げたphpfpmコンテナに入り込んで、その内部でLaravelをセットアップしましょう。
Dockerfileにて、WORKDIRを/var/www/laravelにしているので、ホストOSから直接docker-compose execで実行できます。
このコマンドはWORKDIRで実行されるため、コマンド末尾の./var/www/laravelを指します。

[mejileben]$ docker-compose exec phpfpm composer create-project laravel/laravel .
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Installing laravel/laravel (v5.7.19)
  - Installing laravel/laravel (v5.7.19): Loading from cache
Created project in .
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 86 installs, 0 updates, 0 removals
  - Installing vlucas/phpdotenv (v2.5.1): Loading from cache
  - Installing symfony/css-selector (v4.2.1): Loading from cache
  - Installing tijsverkoyen/css-to-inline-styles (2.2.1): Loading from cache
  - Installing symfony/polyfill-php72 (v1.10.0): Loading from cache
  - Installing symfony/polyfill-mbstring (v1.10.0): Loading from cache
...

ここでうっかりプロジェクト名を指定すると、その名前のディレクトリにソースコードがセットアップされて、NginxからするとNot Foundになってしまうので注意です。
あと、冒頭でrootユーザーでInstallするな!って怒られていますが、僕は気にしませんでしたw

さて、このインストールが終わった頃に、ホストOSのほうでtreeコマンドしてみると

[mejileben]$ tree laravel -L 1                         (git)-[master]
laravel
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── package.json
├── phpunit.xml
├── public
├── readme.md
├── resources
├── routes
├── server.php
├── storage
├── tests
├── vendor
└── webpack.mix.js

10 directories, 8 files

見事にLaravel関連ファイルが入っていることが確認できます。
こうやって環境構築自体はDocker内でやって、実態のあるファイルは自分のOSにマウントするというのをやると、Docker使っている感が出ますね〜

この状況で
http://localhost:8081
を開くと、

image.png

無事にLaravelのページが表示されました!

次は

npmのコンテナも入れて、Nuxt.jsの環境構築もしていきたいと思います!

参考記事

Laravel+MySQL+NginxでさくっとDocker開発立ち上げる
Alpine Linux で Docker イメージを劇的に小さくする
Alpine Linux でタイムゾーンを変更する
Alpine Linux入門 -内部構造とapkでパッケージインストール編-