Help us understand the problem. What is going on with this article?

最強のLaravel開発環境をDockerを使って構築する【新編集版】

お急ぎの方は 使い方 からお読みください。

概要

Docker, Docker Compose を使って、nginx, php-fpm, MySQLの実行環境(LEMP)を構築して、
最強のLaravel開発環境を構築する記事です。

リポジトリ

スターをもらえると嬉しいです☺️

対象読者

  • PHP, Linuxを完全に理解している方
  • Laravelを愛する心の持っている方
  • Dockerの知識をある程度持っている方

前提

入れておいてね

[mac] $ git --version
git version 2.26.2
[mac] $ docker --version
Docker version 19.03.8, build afacb8b
[mac] $ docker-compose --version
docker-compose version 1.26.0, build d4451659

Docker Content Trust(DCT)を有効にする

~/.bashrc~/.zshrc に追記する。

~/.bashrc
export DOCKER_CONTENT_TRUST=1

DCTは、Dockerイメージを「なりすまし」と「改ざん」から保護するセキュリティ機能です。

  • Docker イメージへ発行者のデジタル署名を付ける
  • イメージの利用時(pull など)に「発行者」と「イメージが改ざんされていないこと」を検証する

push, build, create, pull, run のコマンド実行時に自動で機能します。

コンテナ構成

下記の3つのコンテナで構成します。
LEMP環境を作成します。

├── app
├── web
└── db

よく利用されるシンプルな構成だと思うのでこの構成にしました。
プロジェクトに合わせてご自由にカスタムしてください👍

app コンテナ

  • アプリケーションサーバーのコンテナ
  • PHPのバージョンは7.4系を利用する
    • 7.2系は2020年11月にサポートが切れるので注意
  • Laravel 7.x サーバ要件 を満たす
  • php, composer のベースイメージを利用

web コンテナ

  • アプリケーションサーバーのコンテナ
  • HTTPリクエストを受けて、HTTPレスポンスを返す
  • phpファイルへのアクセスはappコンテナに投げる
  • nodeイメージからyarnコマンドをインストールして、アセットファイルのビルドも担当する
  • nginx, node のベースイメージを利用

db コンテナ

  • データベースサーバーのコンテナ
  • MySQLのバージョンは8.0系を利用する
    • 5.7系は2020年10月にサポートが切れるので注意
    • 本番環境でAmazon Auroraを使用する場合は現時点で5.7互換までしかサポートしていないので注意
  • mysql のベースイメージを利用

ディレクトリ構成

.
├── backend # Laravelプロジェクトのルートディレクトリ
└── infrastructure
     ├── Makefile
     ├── docker
     │   ├── mysql
     │   │   ├── Dockerfile
     │   │   └── my.cnf
     │   ├── nginx
     │   │   ├── Dockerfile
     │   │   └── default.conf
     │   └── php
     │       ├── Dockerfile
     │       ├── bash
     │       │   ├── .bash_history => Bashのコマンド履歴を残すため
     │       │   └── psysh => tinkerのコマンド履歴を残すため
     │       ├── php-fpm.d
     │       │   └── zzz-www.conf => unixドメインソケットの設定ファイル
     │       └── php.ini
     └── docker-compose.yml

使い方

composer create-project で新規作成したい場合と composer install で環境構築したい場合がありますので、それぞれ説明します。
Makefile を見てもらえると何やってるか分かりやすいです。

A. Laravelプロジェクトの新規作成

[mac] $ git clone git@github.com:ucan-lab/docker-laravel.git
[mac] $ cd docker-laravel/infrastructure
[mac] $ make create-project
[mac] $ make install-recommend-packages

http://127.0.0.1

以上の4ステップでLaravelの新規プロジェクトの環境構築は完了です。
make install-recommend-packages ではオススメの開発パッケージをインストールしています。

B. Laravelプロジェクトのインストール

backend ディレクトリに既にLaravelがインストール済みの場合はこちらの方法になります。

[mac] $ git clone git@github.com:ucan-lab/docker-laravel.git
[mac] $ cd docker-laravel/infrastructure
[mac] $ make init

http://127.0.0.1

以上の3ステップで既存のLaravelプロジェクトの環境構築は完了です。

Tips

Makeコマンド、Makefileについて

docker-compose コマンドは入力するには長く、プロジェクト毎にエイリアス設定するのはとても面倒です。
Makefile を用意することで、短いコマンドでコマンドが実行できるようになります。

用意している make コマンドと元のdocker-composeコマンドを併記してご紹介します。

基本

# コンテナを作成する
[mac] $ make up
[mac] $ docker-compose up -d

# コンテナを破棄する
[mac] $ make down
[mac] $ docker-compose down

# コンテナを再作成する
[mac] $ docker-compose down && docker-compose up -d

# コンテナ、イメージ、ボリュームを破棄する
[mac] $ make destroy
[mac] $ docker-compose down --rmi all --volumes

# コンテナ、ボリュームを破棄する
[mac] $ make destroy-volumes
[mac] $ docker-compose down --volumes

# コンテナ、イメージ、ボリュームを破棄して再構築
[mac] $ make remake
[mac] $ docker-compose down --rmi all --volumes && \
        mkdir -p ./docker/php/bash/psysh && \
    touch ./docker/php/bash/.bash_history && \
    docker-compose up -d --build && \
    docker-compose exec app composer install && \
    docker-compose exec app cp .env.example .env && \
    docker-compose exec app php artisan key:generate && \
    docker-compose exec app php artisan storage:link && \
    docker-compose exec app php artisan migrate:fresh --seed
  • コンテナを破棄するとコンテナ内のデータは破棄されます。
  • イメージを破棄するとDockerイメージのビルドが必要です。
  • ボリュームを破棄すると名前付きボリュームで管理しているデータベースの中身が消えます。

appコンテナに入る

[mac] $ make app
[mac] $ docker-compose exec app bash

webコンテナに入る

[mac] $ make web
[mac] $ docker-compose exec app ash

webコンテナ内のyarnを実行する

web コンテナにyarnコマンドをインストールしています。

[mac] $ make yarn
[mac] $ docker-compose exec web yarn

[mac] $ make yarn-dev
[mac] $ docker-compose exec web yarn dev

[mac] $ make yarn-watch
[mac] $ docker-compose exec web yarn watch

ただし、コンテナ内のyarnを実行するとめちゃくちゃ遅いので、あまり現実的ではありません。
開発時は素直にローカルのyarnを使用した方が良いです。

MacにNode,npm,yarnをインストールする記事を書きました。

dbコンテナに入る

[mac] $ make db
[mac] $ docker-compose exec db bash

dbコンテナのMySQLに接続する

[mac] $ make sql
[mac] $ docker-compose exec db bash -c 'mysql -u $$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE'

Laravelのマイグレーションを実行する

# migrate
[mac] $ make migrate
[mac] $ docker-compose exec app php artisan migrate

# migrate:fresh
[mac] $ make fresh
[mac] $ docker-compose exec app php artisan migrate:fresh --seed

# db:seed
[mac] $ make seed
[mac] $ docker-compose exec app php artisan db:seed

テストの実行

[mac] $ make test
[mac] $ docker-compose exec app php artisan test

解説

.env

infrastructure/.env
COMPOSE_PROJECT_NAME=docker-laravel

.env ファイルはDocker Composeの環境変数ファイルです。(Laravelの.envとは別物です)

COMPOSE_PROJECT_NAME

https://docs.docker.com/compose/reference/envvars/#compose_project_name

プロジェクト名を設定します。
コンテナの起動時にサービス名と共に付加されます。

今回の場合ですと docker-laravel_app_1docker-laravel_db_1 といったコンテナ名が割り当てられます。
未指定の場合は basename つまりディレクトリ名(infrastructure)がデフォルト値になります。
複数の環境を作る際にコンテナ名がバッティングしないように設定します。

docker-compose.ymlcontainer_name のオプションでコンテナ名を直接指定はできますが、他のプロジェクトとバッティングしやすくなるので、 COMPOSE_PROJECT_NAME を利用した方が良いです。

docker-compose.yml

infrastructure/docker-compose.yml
version: "3.8"
volumes:
  db-store:
  php-fpm-socket:
services:
  app:
    build: ./docker/php
    volumes:
      - php-fpm-socket:/var/run/php-fpm
      - ../backend:/work/backend
      - ./docker/php/bash/.bash_history:/root/.bash_history
      - ./docker/php/bash/psysh:/root/.config/psysh

  web:
    build: ./docker/nginx
    ports:
      - 80:80
    volumes:
      - php-fpm-socket:/var/run/php-fpm
      - ../backend:/work/backend

  db:
    build: ./docker/mysql
    volumes:
      - db-store:/var/lib/mysql
    ports:
      - 3306:3306

app, web, db の3つのコンテナを定義しています。
ミニマムな構成を考えれば appdb コンテナだけあれば十分ですが、本番に近い構成と利便性を考慮してこの構成にしています。

version

https://docs.docker.com/compose/compose-file

version: "3.8"

Docker Compose のバージョンを指定します。

volumes

https://docs.docker.com/compose/compose-file/#volumes

volumes:
  db-store:
  php-fpm-socket:

名前付きボリュームをマウントしています。
データベース(db-store)のデータはコンテナを破棄しても残しておきたいのでボリュームとして定義しています。
unixソケット(php-fpm-socket)のボリュームは app コンテナと web コンテナで共用したいのでマウントしてます。

services.app

https://docs.docker.com/compose/compose-file/#build

  app:
    build: ./docker/php
    volumes:
      - php-fpm-socket:/var/run/php-fpm
      - ../backend:/work/backend
      - ./docker/php/bash/.bash_history:/root/.bash_history
      - ./docker/php/bash/psysh:/root/.config/psysh

app(アプリケーションサーバ)コンテナの定義です。PHPを実行します。
build./docker/php/Dockerfile を指しています。Dockerfileの詳細は後述します。
volumes で名前付きボリュームとホストパスをコンテナにマウントしてます。
../backend:/work/backend Laravelのソースコードをマウントしてます。
他はunixソケット、シェル周りで使うものをマウントしてます。(コマンド履歴はコンテナを破棄しても残したいので)

services.web

https://docs.docker.com/compose/compose-file/#ports

  web:
    build: ./docker/nginx
    ports:
      - 80:80
    volumes:
      - php-fpm-socket:/var/run/php-fpm
      - ../backend:/work/backend

web(ウェブサーバ)コンテナの定義です。PHP以外の静的コンテンツを返却します。
ports ポートを公開します。(HOST:CONTAINER)
nginxのデフォルトのポート番号は 80 番です。

services.db

  db:
    build: ./docker/mysql
    volumes:
      - db-store:/var/lib/mysql
    ports:
      - 3306:3306

db(データベース)コンテナの定義です。
MySQLのデフォルトのポート番号は 3306 番です。

docker/php/Dockerfile

https://docs.docker.com/engine/reference/builder

FROM php:7.4-fpm-buster
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

# timezone environment
ENV TZ=UTC \
  # locale
  LANG=en_US.UTF-8 \
  LANGUAGE=en_US:en \
  LC_ALL=en_US.UTF-8 \
  # composer environment
  COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer \
  # Laravel environment
  DB_CONNECTION=mysql \
  DB_HOST=db \
  DB_DATABASE=laravel_local \
  DB_USERNAME=phper \
  DB_PASSWORD=secret

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

RUN apt-get update && \
  apt-get -y install git libicu-dev libonig-dev libzip-dev unzip locales && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  locale-gen en_US.UTF-8 && \
  localedef -f UTF-8 -i en_US en_US.UTF-8 && \
  mkdir /var/run/php-fpm && \
  mkdir /var/log/php && \
  docker-php-ext-install intl pdo_mysql mbstring zip bcmath && \
  composer config -g process-timeout 3600 && \
  composer config -g repos.packagist composer https://packagist.jp && \
  composer global require hirak/prestissimo

COPY ./php-fpm.d/zzz-www.conf /usr/local/etc/php-fpm.d/zzz-www.conf
COPY ./php.ini /usr/local/etc/php/php.ini

WORKDIR /work/backend

FROM 命令

https://docs.docker.com/engine/reference/builder/#from

ベースイメージを指定します。(IMAGE:TAG)
PHPはDocker Hubに公式イメージが用意されています。
よほど特殊な環境を構築したい場合を除いては公式イメージをベースイメージとするのが良いでしょう。

https://hub.docker.com/_/php

FROM php:7.4-fpm-buster

PHPイメージの指定は <image>:<verion>-<variant>-<os> こんな風にタグ付けされていきます。

PHP の version

PHPのタグバージョンは 7.4 のマイナーバージョンまで指定するのがベターです。
パッチバージョンは後方互換性が保証されるので、なるべく最新を当てておきたいです。

PHPのバージョンのリリース頻度ですが、マイナーバージョンのアップデートは年に1回(11〜12月頃)行われています。
パッチバージョンのリリースは月1程度です。このPHPのサポート期間としては初回リリースから3年間です。
執筆時点でサポートされているPHPのバージョンは 7.2 以降となっています。
特に理由がなければ 7.4 を採用してください。

PHP の variant

PHPのイメージタグの種類として cli, apache, fpm, zts の4種類が用意されています。

  • cli(Apacheを経由せず、直接コマンドライン上で実行)
    • ウェブサーバ用途ではない
    • 使い捨てコンテナ
    • 他のイメージを構築するためのベースイメージ
  • apache
    • ウェブサーバ(Apache mpm_prefork) + アプリケーションサーバ(Apache mod_php)の構成
    • PHPと組み合わせたDebianのApache httpdが含まれている
  • fpm(FastCGI Process Manager)
  • zts(phpがZend Thread Safetyでビルドされている)
    • マルチスレッド環境に対応するためのPHPの機構です
    • PHPはシングルスレッド環境(NTS)で動かす方が一般的です。

今回はnginxを使ってるのでfpmを選択します。

PHP の コンテナOS

コンテナOSとしては buster, stretch, alpine の3種類が用意されています。

  • buster (Debian 10) (405MB)
  • stretch (Debian 9) (493MB)
  • alpine (Alpine Linux) (83.5MB)

alpineイメージサイズの小ささには驚くのですが、
タイムゾーンの設定するのにも手間がかかったりメンテナンスも面倒で初心者にオススメはできないという考えに至り buster を選択しています。

他のコンテナOSを使いたい場合は自作しましょう...。

SHELL 命令

https://docs.docker.com/engine/reference/builder/#shell

SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

コマンドのシェル形式に使用されるデフォルトのシェルをオーバーライドできます。
Linuxのデフォルトのシェルは ["/bin/sh", "-c"]

おまじないです。。

RUN 命令でコマンドをワンライナーにしているのですが、
それだと失敗した際にどこで落ちたのか分かりづらいためオプションを付けています。

ARG 命令

https://docs.docker.com/engine/reference/builder/#arg
Dockerのビルド時に引数として変数を定義できます。

ENV 命令

https://docs.docker.com/engine/reference/builder/#env

ENV 命令は環境変数を設定します。
ARG 命令と違ってこちらはコンテナ起動時にもこの値は使用できます。

# timezone environment
ENV TZ=UTC \
  # locale
  LANG=en_US.UTF-8 \
  LANGUAGE=en_US:en \
  LC_ALL=en_US.UTF-8 \
  # composer environment
  COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer \
  # Laravel environment
  DB_CONNECTION=mysql \
  DB_HOST=db \
  DB_DATABASE=laravel_local \
  DB_USERNAME=phper \
  DB_PASSWORD=secret

\ をつなげてワンライナーにしています。
こうすることでイメージレイヤー数を減らすことができます。

Laravelの環境変数について

Laravelの .env の内容を書き換えるという手順を省くためサーバー環境変数を設定しています。
ちなみにLaravelの .env よりもサーバー環境変数の値の方が優先されます。

DB_HOST=db について

DB_HOST はデータベースのホストを指します。
未指定の場合は 127.0.0.1 と設定されているので app コンテナを見に行ってしまいます。
そのため、 db とサービス名を指定しています。

※サービス名(db) と コンテナ名(docker-laravel_db_1) は意味が異なるのでご注意ください。

COPY 命令(マルチステージビルド)

https://docs.docker.com/engine/reference/builder/#copy

新しいファイルまたはディレクトリをコンテナのパスにコピーします。

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

--from オプションを付けると別のイメージのファイルを指定できます。(マルチステージビルド)
composer のインストールがとてもシンプルになっています。

RUN 命令

https://docs.docker.com/engine/reference/builder/#run

RUN 命令は現在のイメージの上に任意のコマンドを実行した結果をコミットします。
ワンライナーで実行することによって、イメージレイヤ数を減らすことができます。

RUN apt-get update && \
  apt-get -y install git libicu-dev libonig-dev libzip-dev unzip locales && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  locale-gen en_US.UTF-8 && \
  localedef -f UTF-8 -i en_US en_US.UTF-8 && \
  mkdir /var/run/php-fpm && \
  mkdir /var/log/php && \
  docker-php-ext-install intl pdo_mysql mbstring zip bcmath && \
  composer config -g process-timeout 3600 && \
  composer config -g repos.packagist composer https://packagist.jp && \
  composer global require hirak/prestissimo

を行ってます。

COPY 命令

https://docs.docker.com/engine/reference/builder/#copy

新しいファイルまたはディレクトリをコンテナのパスにコピーします。

COPY ./php-fpm.d/zzz-www.conf /usr/local/etc/php-fpm.d/zzz-www.conf
COPY ./php.ini /usr/local/etc/php/php.ini

ホスト側に置いているPHPの設定ファイルのコピーをしているだけです。
PHPの設定ファイルも少し解説します。

./docker/php/php-fpm.d/zzz-www.conf
[www]
listen = /var/run/php-fpm/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0666

nginxとphp-fpm間の通信ですが、TCPUNIXドメインソケットで通信します。
UNIXドメインソケットの方が遥かにスループットが優れているためこちらを採用しました。

./docker/php/php.ini
zend.exception_ignore_args = off
expose_php = on
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL
display_errors = on
display_startup_errors = on
log_errors = on
error_log = /dev/stderr
default_charset = UTF-8

[Date]
date.timezone = ${TZ}

[mysqlnd]
mysqlnd.collect_memory_statistics = on

[Assertion]
zend.assertions = 1

[mbstring]
mbstring.language = Japanese

php.iniの各設定に関しては別記事にまとめていますので、そちらをご参照ください。

docker/nginx/Dockerfile

./docker/nginx/Dockerfile
FROM node:12.16.1-alpine as node
FROM nginx:1.18-alpine
SHELL ["/bin/ash", "-oeux", "pipefail", "-c"]

ENV TZ=UTC

RUN apk update && \
  apk add --update --no-cache --virtual=.build-dependencies g++

COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /opt /opt
COPY ./default.conf /etc/nginx/conf.d/default.conf

WORKDIR /work/backend

ベースイメージとしてDocker Hub公式イメージを使用します。

FROM 命令 node

FROM 命令 nginx

COPY 命令

node のベースイメージから nodeyarn をコピーしています。

COPY --from=node /opt /opt

/opt/yarn-v1.22.0 と yarn パッケージがインストールされています。
もし npm を使用したい場合は下記のように置き換えてみてください。

COPY --from=node /usr/local/lib /usr/local/lib

/usr/local/lib/node_modules/npm と npm パッケージがインストールされています。

それからnginxの設定ファイルをコピーしています。

/docker/nginx/default.conf
access_log /dev/stdout main;
error_log /dev/stderr warn;

server {
    listen 80;
    root /work/backend/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.html index.htm index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

nginxの設定ファイルはLaravelの公式ドキュメントを参考にしています。
https://readouble.com/laravel/7.x/ja/deployment.html

root でドキュメントルートを設定しているのと、
fastcgi_pass でunixドメインソケットを指定しています。

docker/mysql/Dockerfile

FROM mysql:8.0

ENV TZ=UTC \
  MYSQL_DATABASE=laravel_local \
  MYSQL_USER=phper \
  MYSQL_PASSWORD=secret \
  MYSQL_ROOT_PASSWORD=secret

COPY ./my.cnf /etc/my.cnf

ベースイメージとしてDocker Hub公式イメージを使用します。

https://hub.docker.com/_/mysql

環境変数と設定ファイルのコピーだけ行っています。

MySQL8.0

PHP 7.1.16, 7.2.4 以降のバージョンから新しい認証方式 caching_sha2_password にも対応するようなりましたのでMySQL8.0系の導入敷居は下がったかなと思います。

my.cnf

my.cnf の説明です。

/docker/mysql/my.cnf
[mysqld]
character_set_server = utf8mb4
collation_server = utf8mb4_0900_ai_ci

# timezone
default-time-zone = SYSTEM
log_timestamps = SYSTEM

# Error Log
log-error = mysql-error.log

# Slow Query Log
slow_query_log = 1
slow_query_log_file = mysql-slow.log
long_query_time = 1.0
log_queries_not_using_indexes = 0

# General Log
general_log = 1
general_log_file = mysql-general.log

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4

MySQL8.0からデフォルトの文字コードは utf8mb4 文字列照合順序は utf8mb4_0900_ai_ci となっています。
日本語を扱う場合はMySQL8.0.1で新しく導入された日本語用の照合順序 utf8mb4_ja_0900_as_cs_ks を設定することを推奨します。

https://tmtms.hatenablog.com/entry/201805/mysql-innovation-day-tokyo

  • utf8mb4_ja_0900_as_cs_ks
    • utf8mb4 utf8で、マルチバイトを4バイトとする文字コードになります。
    • ja 日本語用
    • 0900 Unicodeのバージョン 9.00
  • as Accent Sensitiveの略称。アクセント違いは異なる文字
    • 「は」と「ぱ」は異なる文字として評価される。
  • cs Case Sensitiveの略称。大文字小文字は異なる文字
    • 「あ」と「ぁ」は異なる文字として評価される。
  • ks Kana Sensitiveの略称。カタカナひらがなは異なる文字
    • 「あ」と「ア」は異なる文字として評価される。

関連記事

ucan-lab
Backend Developer at ROLO. I love PHP and I'm focusing on Laravel, Docker, GraphQL.
https://u-can.pro
miraito-inc
システムデザインを中心に置いた開発により高品質で使いやすいシステムを提供いたします。業務システム構築、アプリ開発、コンサルティングまで幅広く手がけています。
https://miraito-inc.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした