なぜ書いた?
最近筆者はAWSのECS利用を見据えて、LaradocやLaravelSailを使わずにLaravel9のアプリをDocker化する作業を行いました。
その際にLaravel9のモノリシック・アーキテクチャのDocker化の記事がまだまだ少なく、新しいフロントエンドビルドツールのViteに対応した構築部分で苦労したため、備忘録の意味も含めて誰かの役に立てればと思い書きました。
ちなみにLaravel9+Vue(Vite使用)を使ったマイクロサービス(フロントを別フレームワークに切り出したアーキテクチャ)の作り方は下記の英語の記事が落ちていたので、せっかくなのでこちらも共有しておきます。
stackorverflowの記事
上記解決の元となった記事
※Laravel8については結構記事が落ちているので通しの手順などは他の方のものを見ていただければと思いつつ、局地的なお困りごとにはお力になれるかもしれない記事は以前書いたので、よかったら参考にしてみてください。(下記の記事になります)
[docker,laravel8,nginx(非Laradoc)] docker環境構築で躓いたところと解決した方法をまとめてみた
取り扱いのバージョン
Laravel9(vite,vue3,Inertia利用のモノリシック・アーキテクチャ)
PHP8.1
MySQL8.0
nginx1.23
完成形のアーキテクチャ図
手順
※注意:手元で動いたソースコードから説明に必要ない部分を適宜削除しているため、そのままコピーするだけだと動かない可能性があります。あくまで参考資料としてご利用ください。
1.すでに動いているLaravel9プロジェクトのディレクトリに移動
2.アーキテクチャ図を参考にdocker-compose.ymlを作成
version: "3"
volumes:
nginx-public: {} # fpmコンテナからnginxコンテナにpublicディレクトリのコピーする必要があるため、ローカルPCを経由してpublicディレクトリを保存させられるようにするためvolumesを用意します。
services:
nginx:
container_name: nginx
build:
context: .
dockerfile: nginx.Dockerfile
args:
fpm_host: fpm
depends_on:
- fpm
ports:
- 3334:80
volumes:
- ./logs:/var/log/nginx
- nginx-public:/var/www/public # publicディレクトリを一時保存場所のvolumesから呼び出しで/var/www/publicにコピー
fpm:
container_name: fpm
build:
context: .
dockerfile: fpm.Dockerfile # コンテナの設定が入り組むので後ほど手作りする
volumes:
- .:/var/www # Laravelプロジェクトをfpmコンテナにコピー
- nginx-public:/var/www/public # /var/www/publicディレクトリを一時保存場所のvolumesにコピー
depends_on:
- db
networks:
- default
db:
container_name: db
image: mysql/mysql-server:8.0
volumes:
- ./logs/mysql:/var/log/mysql
- ./local-docker/mysql/mysqld_charset.cnf:/etc/mysql/conf.d/mysqld_charset.cnf
- ./local-docker/mysql/initdb.d:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_ROOT_HOST: '%'
TZ: Asia/Tokyo
ports:
- 3306:3306
command: mysqld --innodb_use_native_aio=0
networks:
default:
name: network-name
3.同じくプロジェクトトップのディレクトリで各Dockerfileを作成
# ===マルチステージビルドを2回行う(phpとnode)===
FROM node:16 as node-build # nginx:1.23に対応しているのがnode16系だったので16で設定
COPY . /application
WORKDIR /application
RUN apt-get update\
&& npm install \
&& npm run build --force
FROM php:8.1-alpine as builder
# dockerパッケージインストール
RUN set -eux && \
apk update && \
apk add --update --no-cache \
autoconf gcc g++ make icu-dev libzip-dev libpng libpng-dev oniguruma-dev
RUN docker-php-ext-install pdo_mysql mbstring opcache zip fileinfo gd
RUN curl -SL http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz | tar -xz -C ~/ && \
rm /usr/bin/iconv && \
mv ~/libiconv-1.15 ~/libiconv && \
~/libiconv/configure --prefix=/usr/bin && \
make && make install && \
rm -rf ~/libiconv
# composerインストール
WORKDIR /app
COPY --from=composer:2.4 /usr/bin/composer /usr/bin/composer
RUN mkdir -p database/seeds && \
mkdir -p database/factories && \
mkdir -p tests
# ===マルチステージビルドここまで===
# 実際に稼働するコンテナ
FROM php:8.1-fpm-alpine
COPY --from=builder /usr/bin/composer /usr/bin/composer
COPY --from=builder /usr/bin/lib /usr/bin/lib
COPY --from=builder /usr/lib /usr/lib
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20210902 /usr/local/lib/php/extensions/no-debug-non-zts-20210902
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=node-build /application/public /var/www/public
ENV IS_FPM=1
COPY ["docker/fpm/start.sh", "/"]
RUN chmod +x /start.sh
COPY docker/fpm/php.ini /usr/local/etc/php/php.ini
COPY . /var/www
ARG APP_ENV=local
ENV APP_ENV=$APP_ENV
WORKDIR /var/www
EXPOSE 80
CMD ["/start.sh"]
FROM nginx:1.23-alpine
ARG fpm_host=localhost # 本番環境立ち上げではfpm_hostを準備しないことで、この値がlocalhostになる
ENV FPM_HOST=$fpm_host # ローカルではfpm(コンテナ名)として使う
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf.tmp
RUN envsubst '$FPM_HOST'< /etc/nginx/nginx.conf.tmp > /etc/nginx/nginx.conf
4.Dockerfile立ち上げに必要なファイルを作成
error_reporting = E_ERROR | E_WARNING | E_PARSE | E_NOTICE
display_errors = stdout
display_startup_errors = on
log_errors = on
error_log = /var/log/php/php-error.log
upload_max_filesize = 100M
memory_limit = -1
post_max_size = 100M
max_execution_time = 900
max_input_vars = 100000
extension_dir = /usr/local/lib/php/extensions/no-debug-non-zts-20210902
# セキュリティを向上させるため、PHPのバージョンをレスポンスヘッダに含めないようにする
expose_php = Off
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.language = "Japanese"
[opcache]
opcache.validate_timestamps = ${OPCACHE_VALIDATE_TIMESTAMPS}
opcache.revalidate_freq = 1
#!/bin/sh
set -e
# composerインストール
composer clear-cache
composer install
echo "Complete composer install !"
# Laravelのキャッシュをクリア
composer dump-autoload
php artisan clear-compiled
php artisan optimize
php artisan config:cache
php artisan cache:clear
php artisan route:clear
php artisan view:clear
# マイグレーション実行
php artisan migrate
# バージョン確認
php -v
echo "PHP Setup Complete."
# fpm起動
if [ $IS_FPM -eq 1 ] ; then
php-fpm
else
echo "Skip run fpm"
fi
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 80;
listen [::]:80;
root /var/www/public;
# Laravel公式推奨の(クリックジャッキングなどの)セキュリティ
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
root /var/www/public;
try_files $uri $uri/ /index.php$is_args$args;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log offc; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass ${FPM_HOST}:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
# アップロード上限36MB制限(35*1024^2 = 36.7MB)
client_max_body_size 35m;
# セキュリティを向上させるため、nginxのバージョンをレスポンスヘッダに含めないようにする
server_tokens off;
}
-- DBとユーザーを作成
CREATE DATABASE IF NOT EXISTS `local_db`;
CREATE USER 'local_db'@'%' IDENTIFIED BY 'password';
GRANT ALL ON local_db.* TO 'local_db'@'%';
[mysqld]
character_set_server=utf8
character_set_filesystem=utf8
collation-server=utf8_general_ci
init-connect='SET NAMES utf8'
init_connect='SET collation_connection = utf8_general_ci'
skip-character-set-client-handshake
5..envファイルを用意
APP_NAME=laravel
APP_ENV=local
APP_KEY=base64:xxxxxxxxxxxxxxxxx
APP_URL=http://0.0.0.0:3334
APP_TIMEZONE=Asia/Tokyo
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug # 本番でこのまま行くと危ないので注意
DB_HOST=db
DB_PORT=3306
DB_DATABASE=local_db
DB_USERNAME=local_db
DB_PASSWORD=xxxx
MIGRATION_ENV=local
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
STORAGE_NAME=s3
6.docker-compose buid を実行
7.docker-compose up を実行
大まかな流れとしては上記の通りとなります。何かの参考にしていただければと思います。
Laravel9のDocker構築で知っておく必要があるLaravel8との相違点
Laravel9のweb上での公開を意識した構築(nginxとの接続の構築)で一番注意することは、一言で言うとwebpack.mixとViteのコンパイルの仕方の違いになります。
Laravel8ではassetsのバージョン管理でwebpack.mix.jsonを使っており、そのwebpack.mix.jsonはpublicの外側で独立して動作できるようになっています。具体的にはpublic配下のファイル(ccsファイルなど)はresources配下のファイル名と一致するようになっています。(表示のタイミングでファイル名にidを結合する仕組みになっています。)
そのおかげで、fpmコンテナと独立してnginxコンテナでコンパイル(npm run prod
)をしてpublicディレクトリを用意しても動作させることができました。
しかし、Laravel9ではバージョン管理で使っているmanifest.jsonはpublic配下に入っており、public配下のファイル名がコンパイル(npm run build
)のタイミングでid込みのものに変更されてしまいます。(おそらく処理速度向上のために事前にファイル名変換処理を済ませておいていると思われます。)
そのためLaravel8と同じように"fpmコンテナもnginxコンテナもpublicにコンパイルする大元のresorucesは一緒だから、nginxコンテナを作るタイミングで別途publicディレクトリを準備する形で進めよう"と試みると失敗します。
詳しく説明すると、fpmコンテナ内にあるmanifest.jsonを読み取ったnginxが、nginxコンテナ内のfpmコンテナとは中身のファイル名が異なっているしまっているpublicディレクトリからファイルを探してしまいます。その結果最終的に読み取ったファイル名と一致するものはないと判断し真っ白な画面を表示させてしまいます。
上記の内容から、Viteを使ったモノリシック・アーキテクチャをnginxを使って表示する場合は、nginxコンテナのpublicディレクトリはfpmコンテナ(laravelが入っているコンテナ)と全く同じものを準備しなければならないということを知っておく必要があります。
またそのほかに、Laravel9(Vite)ではnpm run build
だとキャッシュが消せないので、npm run build --force
としなければならない点も注意が必要です。
(Laravel8(webpack.mix)はプロジェクトにキャッシュを持たないため、npm run prod
だけでよかったため。)
2022/12/15追記
qiitaを眺めていたら、参考になる下記の記事に出会いました。
Docker環境のLaravel 9 + VITEでハマったこと
まだ手元で試せていないですが、この記事から察するに、Viteはserver: {host: true}
を指定すると、5173portのエクスポートとリッスンをViteが独立して接続して、同じくserver: {host: true}
になっている場合にpublicディレクトリを同期していると思われます。
なので今回の記事のように無理やりコピーで同期しているような振る舞いをさせるというより、nginxコンテナにVite関連のフォルダを格納して5173portを使えるようにして、fpmコンテナとnginxコンテナ内のVite同士に勝手に同期し合ってもらう形の方がVite開発者の想定した挙動になっていると思われます。
本番環境のみの想定なら自分記事のものでも十分動かせるとは思うのですが、デプロイが毎度全て上げ直しになるか、fpmコンテナとnginxコンテナのpublicディレクトリのバージョン管理を手動でしなければならないところが辛みだったのですが、上記がうまくいけばおそらく毎回fpmコンテナを上げ直すだけで良くなりそうな予感がするので、皆さんはこちらで構築された方が良い気がします。