Laravel 12 の開発環境をdockerで実現する方法

Laravel 12でのWebアプリ開発環境を、docker上に実現する方法を紹介します。

# ゴール

- Windows、Mac、またはLinux上に、dockerを使ったLaravel 12開発環境を作る
- サーバーはnginx, php, mysqlを別にする
- Viteの利点であるページリロード無しでのCSS/JS更新を実現する
- Laravel MixではなくViteを使う
- Laravel UIパッケージを使う
- TailwindではなくBootstrapを使う

# 前提

- PHP 8の知識がある
- Laravel 12の知識がある
- Laravel 12に必要な環境構築ができる
- Dockerで簡単な環境構築ができる

本投稿ではLinux mintを使っているので、WindowsやMacで合わないところは読み替えてください。

# Dockerに各サーバーを準備する

目指すフォルダ構成はこんな感じにします。(Docker関連のみ)

```
l12dev (プロジェクトフォルダ)
├ docker
│ ├ mysql
│ │ └ (中身たくさん)
│ ├ nginx
│ │ ├ logs
│ │ ├ default.conf
│ │ └ Dockerfile
│ └ php
│ 　├ Dockerfile
│ 　└ php.ini
└ docker-compose.yml
```

## Dockerをインストールする

ホストOSに[Docker](https://www.docker.com/)をインストールした状態から開始。(個人、教育機関、中小企業は無償でDocker Desktopを使えます。大企業の場合Docker Desktopは有償なので注意。)

## プロジェクトフォルダにDocker用ファイルを作る

今回はドキュメントフォルダの下に``Qiita``フォルダを作り、``l12dev``フォルダを作ります。
作り終わったら、そのフォルダの中にDocker用ファイルを作っていきましょう。

### docker-compose.yml

まずは``docker-compose.yml``から。

```docker-compose.yml
services:
  l12dev-nginx:
    container_name: "l12dev-nginx"
    build:
      context: ./docker/nginx
    depends_on:
      - l12dev-app
    ports:
      - 80:80
    volumes:
      - ./:/src

  l12dev-app:
    container_name: "l12dev-app"
    build:
      context: ./docker/php
    depends_on:
      - l12dev-mysql
    volumes:
      - ./:/src
      - /src/node_modules
      - /src/vendor
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini

  l12dev-mysql:
    image: mariadb:11.7.2
    command: --max_allowed_packet=32505856
    container_name: "l12dev-mysql"
    volumes:
      - ./docker/mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=l12dev
    ports:
      - 3306:3306

  l12dev-redis:
    image: redis:alpine
    container_name: "l12dev-redis"
    ports:
      - 16379:6379
```

いくつかポイントを抑えます。

Webサーバーはnginxなので、appサーバー（PHPサーバー）を別に立ち上げてます。
→今回、後で出てくる説明で対象サーバーをわかりやすくするため意図的にWebサーバーとappサーバーを分けてます。Apache+PHPで同一サーバーにしても全く問題ありません。

Webサーバーはnginxで、ポート80をDocker外に見せて起動します。

AppサーバーはPHPが動作するようにし、ポートはDocker外に見せず、nginxからdocker内のネットワークで呼び出されて動作します。PHPの設定を指定しやすいように、php.iniだけdocker/phpフォルダで見えるようにしています。
また、``/src/node_modules``フォルダと``/src/vendor``フォルダはボリュームマウントから外してます。これは依存ライブラリ群をマウント対象から外すことで動作の高速化を狙ってします。Viteを使った開発環境ではその特性上★超重要★で、これをしないとページのロードに平気で1分とかかかります。

DBサーバーはMySQLの8.3を使いたいのですがDockerイメージに問題（2回目以降のDocker起動でファイル権限エラーが出る）があるのでMariaDBのいめーｊを採用し、デフォルトポートの3306をDocker外に見せて起動します。
ポート3306をDocker外に見せなくても動作はしますが、開発中にHeidiSQLやSequel Aceを使ってDBの中身を見るためには必要です。MySQLユーザー情報は、ユーザーIDがroot、パスワードがenvironmentで指定したMYSQL_ROOT_PASSWORDになります。
MySQL（とredis）はここでイメージを指定し、個別の``Dockerfile``を持たない設定とします。
max_allowed_packetオプションは指定しなくてもいいです。(が、指定したくなることが多いのでここに書いてます)

### nginxのDockerfile等

まずは手動で``プロジェクトフォルダ/docker/nginx``フォルダを作ります。

``nginx``の``Dockerfile``を作ります。
同時に、最低限必要な設定である``default.conf``を記述します。

```Dockerfile:Dockerfile
FROM nginx:1.28.0
COPY ./default.conf /etc/nginx/conf.d/default.conf
```

さらに、``nginx``の動作を設定する``default.conf``を作ります。

```default.conf
server {
    
    listen 80;
    server_name _;

    client_max_body_size 1G;

    root /src/public;
    index index.php;

    access_log /src/docker/nginx/logs/access.log;
    error_log  /src/docker/nginx/logs/error.log;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;    
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass l12dev-app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
        
}
```

開発を進める上で``nginx``の設定を変更したい場合はここに記載することになります。

### phpのDockerfile等

まずは手動で``プロジェクトフォルダ/docker/php``フォルダを作ります。

``php``の``Dockerfile``を作ります。
同時に、最低限必要な設定である``php.ini``を記述します。

```Dockerfile:Dockerfile
FROM php:8.3-fpm
RUN apt-get update \
    && apt-get install -y \
    git \
    zip \
    unzip \
    vim \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libmcrypt-dev \
    libpng-dev \
    libfontconfig1 \
    libxrender1

RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install gd
RUN docker-php-ext-install bcmath
RUN docker-php-ext-install pdo_mysql mysqli exif
RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer

ENV NODE_VERSION=22.15.0
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
ENV NVM_DIR=/root/.nvm
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
RUN node --version
RUN npm --version

WORKDIR /src
ADD . /src/storage
RUN chown -R www-data:www-data /src/storage
```

大事なポイントは、``Composer``と``Node``を入れている部分です。ComposerやNodeが古いと動作しませんので、アクティブなバージョンを指定します。ここではComposerは最新、Nodeは22.15.0以上を指定します。

開発中に``Node``のバージョンコントロールもしたい場合があるので、``NVM``も入れています。
``gd``などいくつかライブラリを入れていますが、このあたりは趣味（私の場合、自分がよく使うレンタルサーバーに寄せてる）なので好きなライブラリを入れましょう。

```php.ini
upload_max_filesize=256M
post_max_size=256M
```

上記では例として``upload_max_filesize``と``post_max_size``を指定していますが、特に設定がなければ空のファイルでもOKです。

## 実行テストする

ターミナル（WindowsならPowerShell）を起動し、プロジェクトフォルダに移動します。

たとえばLinux mintのTerminalを起動して、ドキュメントフォルダのQiitaフォルダに作った``l12dev``プロジェクトフォルダに移動した場合は：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/07e202a0-c603-45a3-a6f5-0a28c95129cb.png)

ターミナルで、nginxとMySQLの起動に必要なフォルダを準備します。
※gitを使う場合、これらはignoreすべきで、gitからcloneした直後に実行すべきターミナルコマンドです。

```bash
# ログファイルを格納するディレクトリを作る（Gitを使う場合はこの``logs``フォルダをignoreすべき）
> mkdir ./docker/nginx/logs

# MySQLで使用するディレクトリを作る（Gitを使う場合はこの``mysql``フォルダをignoreすべき）
> mkdir ./docker/mysql
> touch ./docker/mysql/mysql.sock
```

これでDocker起動に必要なすべてのファイルが揃いました。起動しましょう。

```bash
> docker compose up -d
```

うまく行けば以下のように４つのコンテナが正常起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/c5a0512b-13bc-434b-8346-3ce74e332f96.png)

Webサーバーとして機能しているか確認するため、Chromeを開いて``http://localhost``にアクセスしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/843fc9b2-c451-4061-8178-5fb96b0761ab.png)

まだドキュメントルートに何もアップロードしてないので「File not found.」で正解です。サーバーは正しく起動できました。

# Laravelをインストールする

Laravelのインストール先は、appサーバー＝phpサーバーです。
appサーバーに入って、composerを使ってLaravelをインストールします。

## appサーバーのコンソールに入る

```bash
> docker compose exec l12dev-app bash
```

以下のように入れたらOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/e87295ac-e607-4528-a24f-977ca1d059e2.png)

ここからは「`>`」がホストOS（ここではLinux mint）、「`$`」がDockerコンテナ内のOSとして表記します。
上記/src#の#部分が$だと思って読み替えてください。

## サブフォルダにLaravelをインストールする

Laravelをプロジェクトフォルダ（l12devフォルダ）にインストールします。
ただ、プロジェクトフォルダにはすでにdocker関連ファイルがあるので、以下のようにサブフォルダ「l12dev_tmp」に一旦インストールし、その後中身をプロジェクトフォルダに移動することにします。

まずサブフォルダを作ってそこにLaravelをインストール。ここではバージョン11を指定しています。

```bash
$ cd /src
$ mkdir l12dev_tmp
$ cd l12dev_tmp
$ composer create-project "laravel/laravel=12.*" . --prefer-dist
```

次に中身をプロジェクトフォルダに移動し、一時サブフォルダを削除。

```bash
$ cd /src
$ mv l12dev_tmp/* ./
$ mv l12dev_tmp/.* ./
$ rm l12dev_tmp -rf
```

このとき以下のように、`/.`、`/..`、`vendor`、`node_modules`のフォルダが移動できないというメッセージ以外に問題がなければ成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/0bb25c18-916d-4b8c-ba31-a1c610ad7ce2.png)

## 依存のインストール

サブフォルダからプロジェクトフォルダに移動したので、このフォルダで依存をインストールします。

```bash
$ cd /src
$ composer install
$ npm install
```

もし`npm install`のときに`npm ERR! Tracker "idealTree" already exists`のようなエラーが出たら、`npm cache clear --force`してから`npm install`しなおしてみましょう。

最終的に、フォルダ構成が以下のようになるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/33102c7e-f2e6-49f8-aa50-a52d073f8911.png)

ここまで``root``ユーザーでLaravelをインストールしましたが、``storage``フォルダ以下のフォルダはWeb用のユーザー``www-data``が書き込みできなくてはいけないので、書き込み権限を付与しておきます。
これをやっておかないと、実行テストをした時点で「The stream or file "/src/storage/logs/laravel.log" could not be opened in append mode」というエラーになります。

```bash
$ chmod -R guo+w storage
```

[Laravel公式のstorage利用手順](https://laravel.com/docs/10.x/filesystem)にも指定のあるように、storageのシンボリックリンクを設置します。

```bash
$ php artisan storage:link
```

## LaravelとDBを接続する

Laravel12では、セッションの保存先がデフォルトでDBになっています。これにより、DB設定をしてセッション保存先テーブルを準備しておかないとLaravelのフロントが正常に起動できません。DB設定は``/src/.env``ファイルに``DB_``で始まる変数が記述部分にあるので、デフォルトの記述をコメントアウトするなどして以下のように記述します。

```.env
# DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

DB_CONNECTION=mysql
DB_HOST=l12dev-mysql
DB_PORT=3306
DB_DATABASE=l12dev
DB_USERNAME=root
DB_PASSWORD=root
```

さらに、セッションの保存先テーブルを含むデフォルトのテーブルを作ります。

```bash
$ php artisan migrate
```

## 実行テストする

WebサーバーにアクセスするとLaravelが動作するはずなので、Chromeを開いて``http://localhost``にアクセスしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/506267d8-163c-40ad-9546-e01cf7653d50.png)

# Laravel UIパッケージとBootstrapを使う

この記事ではログイン画面等を簡単に構成するためのAuthスキャフォルディングを使う準備として、Laravel UIおよびBootstrapを導入します。

## Laravel UIの導入

```bash
$ composer require laravel/ui
```

## Bootstrapの導入

```bash
php artisan ui bootstrap
```

# Viteを使う

この記事ではViteを使ってCSSを変更し、その変更がフロントに反映されるまでを実装してみます。

## 依存のインストール

上ですでに実行していますが、再掲しておきます。再実行しても問題ありません。

```bash
$ npm install
```

## Viteを起動する

次に、Viteを起動します。

```bash
$ npm run dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/0c9934af-ee9b-4029-99fb-d25327f4dce9.png)

起動成功したので、``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/ed91f6fd-c2bc-42ae-ae31-cfad68214c71.png)

CSSが一切適用されていない表示になります。

実は最初に``http://localhost``がうまく表示されていたのはTailwindが動作していたからで、ViteとBootstrapを導入し、Viteを起動（```npm run dev```）してTailwindではなくViteが有効になったことで表示が変わっています。

Viteが有効になったのにCSSが全く適用されていない根本的な原因は、Viteが正常動作できていないことでCSSとJavaScriptが適用できない状態であることです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/3aa9edef-39e0-4b26-8b1e-1043ac622740.png)

ブラウザのコンソールログにいくつかエラーが出ています。これは、Viteから提供されるはずのファイルが読めない状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/6916589b-8d15-4d0e-8073-65871826ca0a.png)

通信先は``[::1]:5173``です。これが失敗しているのは、以下が原因です。
- そもそもポート5173がDocker外に開かれていない
- Viteは``Local:``は見ているが``Network:``は見ていない

先程Viteを起動したときに``Local: http://localhost:5173/``と表示されているのがローカル（appサーバー）に反応するという意味です。
``Network: use --host to expose``となっているのは、ネットワーク経由でもアクセスするなら``--host``オプションをつけろという意味です。

## appサーバーのポート5173を見せる

Docker外にいるブラウザから、appサーバー内にいるViteが見えるよう、ポート5173を見せるため、``docker-compose.yml``にポートを追記します。

```docker-compose.yml
...
  l12dev-app:
    container_name: "l12dev-app"
    build:
      context: ./docker/php
    depends_on:
      - l12dev-mysql
    ports:
      - 5173:5173
    volumes:
      - ./:/src
      - /src/node_modules
      - /src/vendor
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini
...
```

``ports:``が追記されました。これでDocker外から``localhost:5173``にアクセスすると、appサーバーのポート5173につながります。

また、``/docker/php/Dockerfile``の先頭にもEXPOSEの設定が必要です。

```Dockerfile:Dockerfile
FROM php:8.3-fpm
EXPOSE 5173
RUN apt-get update \
...
```

Dockerfileへの変更を反映するには、Dockerコンテナを再起動する必要があります。
Viteがまだ実行中であれば、`Ctrl + C`で停止してからターミナルを抜けて、Dockerコンテナを再起動することになります。
この時、``composer``と``npm``のinstallコマンドを再実行することに注意してください。理由はこの記事の下部に書きます。

```bash
$ exit
> docker compose down
> docker compose up -d --build
> docker compose exec l12dev-app bash
$ composer install
$ npm install
```

## --host オプションを追加する

--host オプションは、Vite起動オプションなので``vite.config.js``または``npm run dev``の中身を変更することになります。どちらの方法でも大丈夫ですが、あとで``vite.config.js``は追記するので``vite.config.js``側に設定を集約するほうが後々楽でしょう。

``vite.config.js``で設定する場合は、以下のように``server`` ``host``を設定します。

```vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        ...
    ],
    server: {
        host: true,
    },
});
```

``package.json``で設定する場合は、以下のように--hostオプションを追記します。

```package.json
{
    "private": true,
    "scripts": {
        "dev": "vite --host",
        "build": "vite build"
    },
    ...
}
```

Viteを起動（すでに起動中の場合はCtrl+Cで停止してから再度起動）します。

```bash
$ npm run dev
```

今度は``Network:``も見ていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/1cfb3bb6-c713-49e4-9367-148885c83a4c.png)

もう一度、``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/61c47567-fada-4180-a2ed-aa44a1d7f38d.png)

一見CSSが適用されていないように見えますが、1行目は``<h1>``であるのに標準テキストのように見えるように変更するCSSが適用されています。

なお、まだコンソールにエラーが出てViteが正常動作していない場合は、[過去記事](https://qiita.com/hitotch/items/2e816bc1423d00562dc2#--host-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)を参照してhmrの設定も試してください。

ただ、これはbootstrapが適用された``resources/sass/app.scss``ではなく、``resources/css/app.css``が読み込まれている状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/cf150768-3e69-4238-b148-4d585f6c8652.png)

``resources/sass/app.scss``を読み込ませるために、``resources/views/welcome.blade.php``で、``'resouces/css/app.css'``の指定を``'resources/sass/app.scss'``に修正します。

```php:resources/views/welcome.blade.php
        <!-- Styles / Scripts -->
        @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
            @vite(['resources/sass/app.scss', 'resources/js/app.js'])
        @else

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/e7f24bbc-dbed-4111-9594-19abbaab71f1.png)

ためしに画面の背景を黄色にしてみましょう。``resources/sass/app.scss``に以下を追記します。

```scss:resources/sass/app.scss
body{
    background-color: yellow;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/00cebb78-c041-4e93-9e0d-85c15bb71025.png)

LinuxとMacでは即座に背景が黄色になります。

## WindowsでCSSの変更が即座に反映されない問題の対策

しかし、Windowsでは``app.scss``への変更が即座にフロントに反映されません。これではViteの最重要機能「CSS保存で瞬時に反映」が実現できていません。
問題は、appサーバー内でViteがファイル編集イベントに反応できていないことで起こっています。この問題はWSL2に関連する問題のようで、[Vite公式でも言及されています](https://vitejs.dev/config/server-options.html)。

こういった場合、Viteをポーリングモードで動作させることができます。Viteの設定ファイルにポーリングモードで動作するよう``usePolling``を記述します。

```vite.config.js
...
    server: {
        host: true,
        watch: {
          usePolling: true,
        },
    },
});
```

# Dockerコンテナの操作におけるノウハウ

## Dockerコンテナの起動と終了

起動

```bash
> docker compose up -d
```

リビルドと起動

```bash
> docker compose up -d --build
```

停止

```bash
> docker compose down
```

## Dockerコンテナ起動時に毎回実行すべきコマンド

今回は、動作を高速にするために``/src/node_modules``フォルダと``/src/vendor``フォルダはボリュームマウントから外してます。
これにより、``composer``や``npm``でインストールされた依存ライブラリはDockerコンテナを終了するごとに消えてしまいます。したがって、``docker compose up``するときは毎回直後に依存ライブラリをインストールする必要があります。

```bash
> docker compose up -d
> docker compose exec l11dev-app bash
$ composer install
$ npm install
```

面倒なので自動実行できるようにしましょう。appサーバーのDockerfile（docker/php/Dockerfile）の最後に以下の行を入れれば解決です。修正後のDocker起動で、`--build`オプションを忘れずにつけましょう。

```Dockerfile:Dockerfile
ENTRYPOINT [ "bash", "-c", "composer install; npm install; exec php-fpm" ]
```

# Viteで警告が出る

最新のSASSとBootstrapの組み合わせで、``Deplication warning [color-functions]: red() is deplicated.``のような警告が大量発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/063d1209-2e97-47c6-ad81-225ef8979846.png)

根本対策はBootstrapに任せるとして、現状は以下のように警告をオミットするのがよさそうです。

```vite.config.js
...
    css: {
        preprocessorOptions: {
            scss: {
                api: 'modern-compiler', // or "modern"
                silenceDeprecations: ['mixed-decls', 'color-functions', 'global-builtin', 'import']
            }
        }
    },
```

以上です！お疲れ様でした！