[laravel]Dockerマルチステージビルドで環境構築をやってみる

## はじめに
Dockerで環境構築していると、開発環境のみ必要なライブラリやパッケージなども多く、
イメージサイズに悩まされることがあります。
そんな時、ステージを分けて必要なものだけに絞った構築ができることを知りました。
今回のテーマは「Laravel環境をDockerマルチステージビルドで環境構築したい」です！

## ディレクトリ
nginx,php,mysqlという環境で構築していきます。
細かい環境構築の内容については、他の記事を参考にしてください。

dockerディレクトリ配下にコンテナ別でファイルを管理。
laravelアプリ自体のファイルはwwwディレクトリ配下に配置。
```
.
├── README.md
├── docker-compose.yml
├── .env
├── docker
│   ├── mysql
│   │   └── my.cnf
│   ├── nginx
│   │   └── default.conf
│   └── php
│       ├── Dockerfile
│       └── php.ini
└── www
    ├── README.md
    ├── app
    ・
    ・
    ・
```


## docker-compose.yml と　.envを準備
マルチステージビルドは、ymlファイルでtargetを指定することでどのステージをビルドするか選択することができます。
まずはdocker-compose.ymlを準備し、php>build>targetの部分をenvファイルから指定できるようにしていきます。

``` yml:docker-compose.yml
services:
    php:
        container_name: ${PROJECT_NAME:?err}-php
        build:
            dockerfile: ./docker/php/Dockerfile
            target: ${BUILD_STAGE_TARGET:?err} # ここで指定
        ports:
            - ${DEV_HOST_PORT:?err}:5173
        volumes:
            - ./docker/php/php.ini:/usr/local/etc/php/php.ini
            - ./www:/var/www
            - vendor-store:/var/www/vendor #vendor volume mount
            - node_modules-store:/var/www/node_modules #node_modules volume mount
            - storage-framework-store:/var/www/storage/framework #storage volume mount

    nginx:
        image: ${NGINX_IMAGE:?err}
        container_name: ${PROJECT_NAME:?err}-nginx
        ports:
            - ${WEB_HOST_PORT_HTTP:?err}:80
            - ${WEB_HOST_PORT_HTTPS:?err}:443
        volumes:
            - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
            - ./www/public:/var/www/public
        depends_on:
            - php

    mysql:
        image: ${MYSQL_IMAGE:?err}
        ports:
            - ${DB_HOST_PORT:?err}:3306
        volumes:
            - ./docker/mysql/my.cnf:/etc/my.cnf
            - db-store:/var/lib/mysql
        environment:
            - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?err}
            - MYSQL_DATABASE=${MYSQL_DATABASE:?err}
            - MYSQL_USER=${MYSQL_USER:?err}
            - MYSQL_PASSWORD=${MYSQL_PASSWORD:?err}
            - TZ=Asia/Tokyo

volumes:
    db-store:
    vendor-store: #vendor
    node_modules-store: #node_modules
    storage-framework-store: #storage
```

実際は、開発/テスト/ステージング/本番などいくつかに分かれることが多いと思いますが、
今回はお試しということで開発（development）と本番（production）の２つにしています。

```shell:.env
PROJECT_NAME=test-app

# php
BUILD_STAGE_TARGET=development  # development / production
DEV_HOST_PORT=5173


# nginx
NGINX_IMAGE=nginx:alpine
WEB_HOST_PORT_HTTP=80
WEB_HOST_PORT_HTTPS=443


# db
MYSQL_IMAGE=mysql:8.0
DB_HOST_PORT=3306
MYSQL_ROOT_PASSWORD=app1
MYSQL_DATABASE=app1
MYSQL_USER=app1
MYSQL_PASSWORD=app1
```

## Dockerfile
・nginxとmysqlは公式のイメージを使い、マルチステージビルドに対応するのはphpのDockerfileとなります。
・今回は３つのステージを作成
### 1. builderステージ
Laravelのビルドに必要なベース部分を構築するパッケージをインストール
ソースコードもコピーして```composer installまで```行います。 

### 2. developmentステージ
そのままbuilderステージを引き継いで、開発に必要なパッケージを追加
デバッグ用にXdebugと、フロントエンド開発に必要なのでnode.jsを追加
```npm install```まで行います。
ここまでであれば、普通に開発環境のDockerfileと変わりません。

### 3. productionステージ
ここからがマルチステージビルドの目的になります。
・ベースイメージから作り直し
・ランタイムに必要なパッケージだけに絞ってインストール
・必要なファイルはビルドした①のステージからコピーする
こうすることでビルド時だけ使われる余計なファイルを含まない環境を構築してイメージ容量を削減できるというわけです。


```Dockerfile:Dockerfile
# builderステージ
FROM php:8.2-fpm-alpine as builder

# 必要なパッケージのインストール
RUN apk update && apk add --no-cache \
    oniguruma-dev \
    libxml2-dev \
    libzip-dev \
    bash \
    vim \
    git

# PHP拡張のインストール
RUN docker-php-ext-install -j$(nproc) \
    pdo_mysql \
    bcmath \
    zip

# gd拡張をインストール
# 必要なパッケージを追加:libpng-dev freetype-dev libjpeg-turbo-dev libwebp-dev
RUN apk add --no-cache \
    libpng-dev \
    freetype-dev \
    libjpeg-turbo-dev \
    libwebp-dev
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
    && docker-php-ext-install gd

# アプリケーションのソースコードをコピー
WORKDIR /var/www
COPY ./www /var/www

# Composerのインストール
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Composerを使用して依存関係をインストール
RUN composer install --no-dev --optimize-autoloader \
    && composer dump-autoload --optimize


# ====================================
# developmentステージ
FROM builder as development

# Xdebugのインストール
RUN apk add --no-cache linux-headers $PHPIZE_DEPS \
    && pecl install xdebug \
    && docker-php-ext-enable xdebug
# 設定ファイルをコピー
COPY ./docker/php/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini

# node.jsのインストール
RUN apk add --no-cache nodejs npm
# npmパッケージのインストール
RUN npm install

# www-dataユーザーに所有権を変更
RUN chown -R www-data:www-data /var/www


# ====================================
# productionステージ
FROM php:8.2-fpm-alpine as production

# 必要なランタイムパッケージのみをインストール
RUN apk add --no-cache \
    oniguruma \
    libxml2 \
    libzip \
    libpng \
    freetype \
    libjpeg-turbo \
    libwebp \
    bash \
    vim \
    git

# ビルダーステージから必要なファイルをコピー
COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
COPY --from=builder /usr/bin/composer /usr/bin/composer
COPY --from=builder /var/www /var/www
WORKDIR /var/www

# www-dataユーザーに所有権を変更
RUN chown -R www-data:www-data /var/www
```


## さいごに
- productionステージで最低限の内容にしたことでイメージサイズがかなり小さくできました！
　development : 675.75MB
　production : 176.44MB
 
- 環境毎にDockerfileを分けたりする必要もなく、それぞれ必要なものだけにしておけるのでとても便利でした👏
- Laravelの記事をあまり書いていませんでしたが、これからは少しずつ書いていこうと思います。


## 参考

https://qiita.com/come2ry/items/98b0bb70e29821b52ba0

https://rarejob-tech-dept.hatenablog.com/entry/2020/07/31/190000

https://docs.docker.jp/develop/develop-images/multistage-build.html