Laravel 12 + Docker + Vue でSPAローカル環境構築。これで良い？

### はじめに
Laravel 12とVue.jsを使って、SPA（シングルページアプリケーション）開発をしたいなと思い、環境構築をしてみました。勉強にもなると思い、ドッカーで構築しましたので紹介してみます。
うまくいかなかった点も含めて手順を紹介します。
本当にこれで良いのかなという気持ちもあるので、気になる点があればコメントをいただけますと幸いです！

### 前提
当記事は、ubuntuやドッカーデスクトップのインストールが完了していることが前提となっています。


### 技術スタック
  - **環境**: windows11、WSL2、Ubuntu、docker、docker-desktop
  - **バックエンド**: Laravel 12
  - **フロントエンド**: Vue.js (Vite)
  - **認証**: Laravel Breeze
  - **Webサーバー**: Nginx
  - **PHP**: 最新版
  - **データベース**: MySQL 8.0
  - **メールテスト**: MailHog
  - **バージョン管理**: Git / GitHub (SSH接続)

### Step 1: 目指す構成と設定ファイル
まずは最終的なゴールである、プロジェクトの構成と設定ファイルを準備します。

---

#### ファイル構成

```plaintext
.
├── .gitignore
├── docker/
│   ├── nginx/
│   │   └── default.conf
│   └── php/
│       ├── Dockerfile
│       └── php.ini
│        └── php.ini.prod
├── src/
│    └── (ここにLaravelプロジェクトが入る)
├── .env                            # Docker Compose用
├── docker-compose.yml              # 開発環境のベース
├── docker-compose.staging.yml      # ステージング用の差分設定ファイル（リリースを見据えて）
├── docker-compose.production.yml   # 本番用の差分設定（リリースを見据えて）
├── README.md
```

---

#### 各設定ファイル

**docker-compose.yml**
```yaml
services:
  # Nginx サーバー
  nginx:
    image: nginx:latest
    container_name: laravel_nginx
    ports:
      - "8080:80" # ホストの8080番ポートをコンテナの80番に接続
    volumes:
      - ./src:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - php

  # PHP
  php:
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
    container_name: laravel_php
    ports:
      - "5173:5173" 
    volumes:
      - ./src:/var/www/html
    environment:
      - "DB_CONNECTION=mysql"
      - "DB_HOST=mysql"
      - "DB_PORT=3306"
      - "DB_DATABASE=${DB_DATABASE}"
      - "DB_USERNAME=${DB_USERNAME}"
      - "DB_PASSWORD=${DB_PASSWORD}"

  # MySQL データベース
  mysql:
    image: mysql:8.0
    container_name: laravel_mysql
    ports:
      - "33066:3306" # ホストの33066番ポートをコンテナの3306番に接続
    environment:
      - "MYSQL_DATABASE=${DB_DATABASE}"
      - "MYSQL_USER=${DB_USERNAME}"
      - "MYSQL_PASSWORD=${DB_PASSWORD}"
      - "MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}"
    volumes:
      - mysql_data:/var/lib/mysql

  # MailHog (メール送受信テスト用)
  mailhog:
    image: mailhog/mailhog
    container_name: laravel_mailhog
    ports:
      - "8025:8025" # Web UI
      - "1025:1025" # SMTP

volumes:
  mysql_data:
```

---

**nginx/default.conf**
```conf
server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;
    index index.php index.html;

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

    location ~ \.php$ {
        fastcgi_pass php:9000; # サービス名'php'の9000番ポートへ
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}
```
---

**php/Dockerfile**

```dockerfile
# 最新のPHP-FPMイメージをベースにする
FROM php:8.4-fpm

# 必要なライブラリをインストール
RUN apt-get update && apt-get install -y \
    git \
    unzip \
    libzip-dev \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    nodejs \
    npm

# PHP拡張機能をインストール
RUN docker-php-ext-install pdo_mysql pcntl bcmath gd zip

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

# 作業ディレクトリを設定
WORKDIR /var/www/html
```

---

**php/php.ini**
こちらの記事などを参考にしました
https://qiita.com/ucan-lab/items/0d74378e1b9ba81699a9
https://otonan-syusyoku.work/archives/1753

```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 = /var/log/php/php-error.log
default_charset = UTF-8

[Date]
date.timezone = Asia/Tokyo

[mysqlnd]
mysqlnd.collect_memory_statistics = on

[Assertion]
zend.assertions = 1

[mbstring]
mbstring.language = Japanese
```

**php/php.ini.prod**
```ini
zend.exception_ignore_args = on
expose_php = off
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = off
display_startup_errors = off
log_errors = on
error_log = /var/log/php/php-error.log
default_charset = UTF-8

[Date]
date.timezone = Asia/Tokyo

[mysqlnd]
mysqlnd.collect_memory_statistics = off

[Assertion]
zend.assertions = -1

[mbstring]
mbstring.language = Japanese

[opcache]
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 4000
opcache.validate_timestamps = 0
opcache.huge_code_pages = 0
opcache.preload = /var/www/preload.php
opcache.preload_user = www-data
```

---

### Step 2: 環境構築とアプリケーションのセットアップ

設定ファイルが準備できたら、コマンドを実行して環境を構築していきます！

```bash
# Dockerコンテナをビルドして起動
docker-compose up -d --build

# Laravelプロジェクトを作成 (srcが空の場合)
docker-compose exec php composer create-project laravel/laravel .

# LaravelとBreezeをインストール
docker-compose exec php composer require laravel/breeze --dev
docker-compose exec php php artisan breeze:install vue

# 依存関係のインストールと初期設定
docker-compose exec php npm install
docker-compose exec php php artisan key:generate
docker-compose exec php php artisan migrate
```

ここまでは、多くのチュートリアルに書かれている手順です。
次はつまずいた点を紹介します！

---

### Step 2: エラーとの格闘

ここからは、私が実際に遭遇したエラーと、その解決に至るまでの思考のプロセスを共有します。

#### 試練1：`Permission denied` - ディレクトリ書き込み権限の壁

最初に遭遇したのは、Laravelが`storage`ディレクトリに書き込めないという、おなじみのパーミッションエラーでした。
http://localhost:8080/ へアクセスすると以下のエラーが発生した。


  - **発生したエラー**
  ```
  file_put_contents(...): Failed to open stream: Permission denied
  ```
  
  - **原因**
  Dockerコンテナ内のWebサーバーの実行ユーザー(`www-data`)と、マウントされたディレクトリの所有者(ホストPCのユーザー)が異なるため。
  - **解決策**
  以下のコマンドを実行して、`chown -R www-data:www-data` コンテナ内のstorageディレクトリとbootstrap/cacheディレクトリ、およびその中身すべての所有者を、ユーザーwww-data・グループwww-dataに再帰的に変更する。
  ```
  docker-compose exec php chown -R www-data:www-data storage bootstrap/cache
  ```

  **余談**
  ビルド時に自動で実行してもらえるようにファイルを修正した方がいいかなと思い、以下の対応も考えましたが、また別の機会に試してみます。
  1. コンテナ起動時にディレクトリ所有者を自動で変更する`entrypoint.sh`スクリプトを用意。
  2. Dockerfileに設定を加える。

---

#### 試練3：`reading 'user'` - 存在しないユーザー情報

画面は表示されたものの、今度はVueコンポーネントが`user`情報を読み取れずにクラッシュ。

  - **エラー**
  ```
  TypeError: Cannot read properties of undefined (reading 'user')
  ```
  - **原因**
  LaravelからVueへユーザー情報を渡すための「仲介役」である`HandleInertiaRequests`ミドルウェアが有効になっていなかった。
  - **解決策**
  `bootstrap/app.php`の`withMiddleware`に`\App\Http\Middleware\HandleInertiaRequests::class`を追加し、認証情報をフロントエンドと共有できるようにしました。
  ```php
  <?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        // ここから
        $middleware->web(append: [
            \App\Http\Middleware\HandleInertiaRequests::class,
        ]);
        // ここまでを追加
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

  ```

### Step 4: GitHubへのプッシュとSSH設定

最後に、安全に開発を進めるためのGitHub設定です。

1.  **`.gitignore`の作成**: `.env`ファイルや`node_modules`、`vendor`といった不要で機密なファイルがアップロードされないよう、設定します。
2.  **SSHキーの設定**: 毎回パスワードを入力する手間を省き、安全に接続するためにSSHキーを生成し、GitHubに登録しました。
`git remote set-url`で接続方式をHTTPSからSSHに切り替えるのを忘れずに！

### まとめ

こうして、ようやくLaravel + Docker開発環境が完成しました！
これで良いのかなという感じなので、気になるところがあればぜひコメントいただけますと幸いです！