前置き
初投稿です。
ハマったので備忘録的に記事を書きます。
同様の現象だったり、他にもこんなエラーが起きたよって人がいれば解決策の共有をしてもらえると助かります。
誤解を生みそうな部分や、間違えている部分のご指摘いただけると助かります。
前提
- ホストはUbuntu mate25.x
- WindowsからVirtualBoxでLinux起動
- Docker環境構築済みかつLaravelが動いている
- LEMP環境かつseleniumがWebアプリサーバとは別
- Laravel 11以降
- Laravel Dusk使用
- Vite使用
結論
先に結論というか、重要なポイントを簡単にピックアップします。
ハマってる人にはこれで伝わるはず。
-
Laravel 11でのdusk実行時の挙動
過去のLaravel(laravel 10以前)では dusk実行時、.env
は.env.dusk.local
の内容で上書きをしていた。
laravel 11(dusk)では上書きが行われない。
明示的に上書きをする必要がある。
shellを作成の上、実行すると事故が無いです。後述(CSRFトークン不一致の項) -
viteサーバ
hot/public(ホットリロード)がある場合、セレニウムからcss/jsが読み込めない。
npm run devをしている場合、dusk実行時は停止する必要がある。 -
CSRFトークン不一致
Laravel 11でのdusk実行時の挙動のせいで.env
のAPP_URL
が一致していないと起きる。
これはサーバーとして正しい仕様ですが、cookieの取得ができなくて初めて気がつくと思います -
seleniumでアクセスするURLはサービス名に合わせる
以下詳細です。
長くなりすぎたので適宜開閉できるようにしました。
必要なら開いて確認してください。
構成図
アプリケーション構成を表示
項目 | バージョン例 | 説明 |
---|---|---|
Laravel | 11.x | PHPフレームワーク |
PHP | 8.3.2.1 | Laravel実行用 |
MySQL | 8.0.36 | データベース |
Nginx | 1.24 | Webサーバー |
Node.js | v18.20.8 | フロントエンドビルド |
Composer | 2.8.9 | PHPパッケージ管理 |
OS | Ubuntu 25.02 | 開発ベース環境 |
Docker | 28.1.1 | コンテナ実行環境 |
Docker Compose | 2.34 | コンテナオーケストレーション |
システム構成を表示
ChatGPTくんに丸投げ
Docker関連
必要なら展開していってください。
docker-compose.ymlは重要です。
Docker関連のファイル例を表示
dockerFile
必要パッケージを入れたり、作業用のユーザーを作っています。
本来ならユーザーの存在チェックがあったほうがいいですが、書き忘れてます。
イメージ作り直す度にユーザーも作り直されるので問題は基本起きないはずです。
展開
FROM php:8.3-fpm
# UID/GIDを引数で受け取る(デフォルト1000)
ARG UID=1000
ARG GID=1000
ARG USERNAME=appuser
ARG GROUPNAME=appuser
# 必要パッケージのインストール
RUN apt-get update && apt-get install -y \
git \
unzip \
curl \
vim \
libzip-dev \
mariadb-client \
zlib1g-dev \
sudo \
gnupg
# wait-for-it.sh をコンテナ内にコピー
# COPY ../../wait-for-it.sh /usr/local/bin/wait-for-it.sh
# RUN chmod +x /usr/local/bin/wait-for-it.sh
# PHP拡張モジュールのインストール
RUN docker-php-ext-install zip pdo_mysql
# Node.js 18.x をインストール
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm vite vue
# Laravel用ユーザー作成 + sudo権限付与
RUN groupadd -g ${GID} ${GROUPNAME} \
&& useradd -u ${UID} -g ${GROUPNAME} -m ${USERNAME} \
&& echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Composer インストール
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
&& php composer-setup.php \
&& php -r "unlink('composer-setup.php');" \
&& mv composer.phar /usr/local/bin/composer
# 作業ディレクトリ作成と権限調整
WORKDIR /var/www/laravel-project
RUN chown -R ${USERNAME}:${GROUPNAME} /var/www
# 実行ユーザー切替
USER ${USERNAME}
docker-compose.yml
ポイントはサービス名です。(db,appといった)
サービス名とアクセスする時の名前は一致している必要があります。
あと色々とヘルスチェックしていますが、これは基本やらなくていいです。
起動(up)の整合性を取る場合は記載してください。
展開
services:
db:
image: mysql:8.0.36
container_name: db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- ./docker/db/data:/var/lib/mysql
- ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
- ./docker/db/init:/docker-entrypoint-initdb.d
ports:
- "${MYSQL_PORT}:3306"
app:
container_name: app_laravel
build:
context: .
dockerfile: docker/php/Dockerfile
args:
UID: ${UID}
GID: ${GID}
user: "${UID}:${GID}"
volumes:
- .:/var/www
- ~/.docker_bash_history:/appuser/.bash_history
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh:ro
command: ["/usr/local/bin/wait-for-it.sh", "db:3306", "--", "php-fpm"]
depends_on:
- db
ports:
- 5173:5173
nginx:
image: nginx
container_name: nginx
volumes:
- .:/var/www
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh:ro
working_dir: /var/www
command: ["/usr/local/bin/wait-for-it.sh", "app:9000", "--", "nginx", "-g", "daemon off;"]
depends_on:
- app
ports:
- "${APP_PORT}:80"
phpmyadmin:
container_name: test_phpmyadmin
image: phpmyadmin
environment:
PMA_USER: ${PMA_USER}
PMA_PASSWORD: ${PMA_PASSWORD}
depends_on:
- db
ports:
- "${PMA_PORT}:80"
selenium:
image: selenium/standalone-chrome:latest
container_name: selenium
depends_on:
- nginx
command: ["/usr/local/bin/wait-for-it.sh", "nginx:80", "--", "/opt/bin/entry_point.sh"]
ports:
- 4444:4444
- 7900:7900
- 5900:5900
privileged: true
volumes:
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh:ro
- /dev/shm:/dev/shm
shm_size: 2g
default.conf
展開
server {
listen 80;
# server_name nginx localhost 127.0.0.1;
root /var/www/laravel-project/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 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;
fastcgi_param HTTP_HOST $host;
# HostヘッダーをLaravelに渡す
# fastcgi_param HTTP_HOST $host;
# fastcgi_param HTTP_HOST $server_name; # または $host
# Cookieを渡す
# fastcgi_param HTTP_COOKIE $http_cookie;
}
}
インストール手順
すでにlaravelは動いていて、composeで入れるだけの前提
インストール手順を表示
- docker
seleniumを追加 追加方法はdefault.conf
のselenium
をコピー
一度dockerを停止の上再度立ち上げ
docker compose down
docker compose up --build
>UID=1000 GID=1000 docker compose --env-file .env up -d --build
念の為dockerの立ち上げ確認
docker ps
docker compose container ls
# seleniumが追加されていることを確認する。
appサーバに入る
docker compose exec app bash
appサーバ内で以下コマンド(php artisanが通る場所)
composer require --dev laravel/dusk
php artisan dusk:install
# tests/Browser ディレクトリと、DuskTestCase.php が作成される。
apt-getで必要なパッケージを入れる。
頻繁に環境を作り直す場合は、dockerfileに移動したり、シェルにしたほうがいいです。
私はシェルにしています。
#!/bin/bash
# sudoで実行
apt-get update && apt-get install -y \
libglib2.0-0 \
libnss3 \
libgconf-2-4 \
libfontconfig1 \
libxss1 \
libappindicator1 \
libasound2 \
fonts-liberation \
xdg-utils \
wget \
unzip \
iputils-ping \
net-tools # debug
ファイルの修正
.env.dusk.local
新規作成
.envをコピーして、以下必要な箇所だけ書き換えする。
viteの変更は結果的に無駄だったのでいらないです。
ポイントはコメントがついているサービス名です。
これを、docker-compose.ymlのサービス名と合わせます。
# -------------------
# dusk起動に必要な最小構成
APP_ENV=dusk.local
APP_URL=http://nginx # nginxコンテナ名
# APP_URL=http://app # appコンテナ名
DB_HOST=db # dbコンテナ名
DB_PORT=3306
DB_DATABASE=laravel_db
DB_USERNAME=root
DB_PASSWORD=password
# SESSION_DOMAIN=null
SESSION_DOMAIN=null
SESSION_DRIVER=file
SESSION_SECURE_COOKIE=false
# DuskがリモートSelenium使う設定
DUSK_DRIVER=remote
VITE_APP_NAME=vite
# 開発時のviteサーバ
#VITE_DEV_SERVER_URL=http://app:5173
FRONTEND_URL=http://app:3000
# sanctum
# SANCTUM_STATEFUL_DOMAINS=nginx:8000,nginx:3000,nginx
SANCTUM_STATEFUL_DOMAINS="localhost,127.0.0.1,nginx"
# ------------------
以下略
作成後、.env.dusk.localを.envに上書きしますが、バックアップを取った上で上書きしてください。
何かあった時に戻せるようにしておけば大丈夫です。
DuskTestCase.php
以下を修正
~~~~~~~~~~~~~
#[BeforeClass]
public static function prepare(): void
{
if (! static::runningInSail()) {
// static::startChromeDriver(['--port=9515']); // これを削除
}
}
protected function driver(): RemoteWebDriver
{
$options = (new ChromeOptions)->addArguments([
'--window-size=1920,1080',
'--disable-search-engine-choice-screen',
'--disable-smooth-scrolling',
'--no-sandbox', // 追記
'--disable-gpu', // 追記
'--headless=new', // 追記 古いセレニウムの場合は=newでエラーになる可能性あり
]);
return RemoteWebDriver::create(
'http://selenium:4444/wd/hub', // ここをseleniumのサービス名/wd/hubにする
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
~~~~~~~~~~~~~
動作確認
app,nginx,seleniumでcurl等で疎通ができることを確認する。
app
念の為コンテナに入るところから始めます。他も同様
# コンテナに入る
docker compose exec app
# nginx
curl -i http://nginx
# selenium
curl -i http://selenium:4444
nginx
# コンテナに入る
docker compose exec nginx
# app
curl -i http://app:9000
# selenium
curl -i http://selenium:4444
selenium
docker compose exec selenium
# nginx
curl -i http://nginx
# app
curl -i http://app:9000
テスト実施
スクリーンショットは必ず取ったほうがいいです。
とりあえず実行方法についてだけ記載します。
# 全テストの実行
php artisan dusk --env=dusk.local
# 特定のテストの実行
php artisan dusk tests/Browser/ExampleTest.php --env=dusk.local
# 特定のメソッドのみ実行
php artisan dusk --filter testBasicExample --env=dusk.local
# .env.dusk.stagingを作った場合
php artisan dusk --env=dusk.staging
実行前にキャッシュクリアやseederの実行をしておきます。
発生した問題とその対応
地の文多めです。後で直すかも。
- css,jsがseleniumから読み込めない
展開
seleniumとの疎通確認後、スクショをみてファッっとなったやつ
原因は npm run devをしているときに php artisan duskを実行したため。
仕様で,public/hotがある場合はそっちを見に行く。
つまり、 app側からは見えるけど、selenium側からは見えないよ!って状態になる。
npm run dev中でも動かせるよう解決しようとしたけど、実現が難しそうなのでduskを実行時はnpm run buildをして、publicのcss,jsを参照するようにした。
問題点として、ホットリロードが効かないので、ブラウザテストで問題があって修正をする場合、手動でビルドが必要になる。逆に言うとそれくらい。
- CSRFトークン不一致
展開
formのテストを作った時に発生。
原因はapp側のserver nameとseleniumから要求する際のserver nameが違っていたため。
テスト実行時、.env.dusk.local
はartisan dusk
起動で自動的に読み込まれる。(はずだが、明示的に--env=dusk.localを指定したほうがいい)
appからseleniumに対しては.env.dusk.local
の設定が使用される。
seleniumからnginxにアクセスする際は.envが使われるため、.env
と.env.dusk.local
のAPP_NAME
は一致している必要がある。
流れとしてはこう。
app→selenium .env.dusk.local
を使う
selenium→nginx .env
を使う
.env.dusk.local
はなんのための設定?って思うよね。
ワイトもそう思います。
実際の運用時はコピー用として使うことにしました。
.envはlocalhost:8000で開発している人が多そうだから頻出しそうな問題。
対応策として、.envを.env.dusk.localで上書きする。
laravel-projec直下に以下のシェルを用意するといいです。
作成後、実行権限を付与してください。(chmod +x ****.sh)
ブラウザテスト前に.envの複製をとって、.env.dusk.localで.envを上書きする。
prepare_dusk_env.sh
展開
#!/bin/bash
# プロジェクトのルートディレクトリに移動
cd /var/www/laravel-project/
# .env ファイルのバックアップパス
ENV_BACKUP=".env.bak"
# テスト用の .env ファイルパス
ENV_DUSK=".env.dusk.local"
# 実際の .env ファイルパス
ENV_CURRENT=".env"
echo "Duskテスト用の.envファイルを準備します..."
# 1. .env ファイルをバックアップ
if [ -f "$ENV_CURRENT" ]; then
echo "既存の .env を $ENV_BACKUP にバックアップします。"
cp "$ENV_CURRENT" "$ENV_BACKUP"
else
echo "既存の .env ファイルが見つかりません。バックアップはスキップします。"
fi
# 2. .env.dusk.local を .env にコピー
if [ -f "$ENV_DUSK" ]; then
echo "$ENV_DUSK を $ENV_CURRENT にコピーします。"
cp "$ENV_DUSK" "$ENV_CURRENT"
# SANCTUM_STATEFUL_DOMAINS の警告を避けるため、空文字列に置き換え
# sed -i はmacOSとLinuxでオプションが異なる場合があるため、互換性を考慮
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' 's/^SANCTUM_STATEFUL_DOMAINS=null$/SANCTUM_STATEFUL_DOMAINS=""/g' "$ENV_CURRENT"
else
# Linux (GNU sed)
sed -i 's/^SANCTUM_STATEFUL_DOMAINS=null$/SANCTUM_STATEFUL_DOMAINS=""/g' "$ENV_CURRENT"
fi
else
echo "エラー: $ENV_DUSK が見つかりません。中断します。"
exit 1
fi
# 3. Laravelの設定キャッシュをクリア
echo "Laravelの設定キャッシュをクリアします..."
php artisan config:clear
php artisan cache:clear
php artisan route:clear
php artisan view:clear
echo "Duskテスト用の.envファイルの準備が完了しました。"
ブラウザテスト後に.envをもとに戻す
展開
#!/bin/bash
# プロジェクトのルートディレクトリに移動
cd /var/www/laravel-project/
# .env ファイルのバックアップパス
ENV_BACKUP=".env.bak"
# 実際の .env ファイルパス
ENV_CURRENT=".env"
echo "Duskテスト後の.envファイルをクリーンアップします..."
# 1. .env ファイルを元に戻す
if [ -f "$ENV_BACKUP" ]; then
echo "バックアップした .env を元に戻します。"
mv "$ENV_BACKUP" "$ENV_CURRENT"
# 元に戻した後のキャッシュクリアも重要
echo "Laravelの設定キャッシュを再度クリアします..."
php artisan config:clear
php artisan cache:clear
else
echo "バックアップした .env ファイルが見つかりません。"
# 元々.envファイルが存在しなかったがDusk用に作成された場合
if [ -f "$ENV_CURRENT" ]; then
echo "Dusk用に作成された .env を削除します。"
rm "$ENV_CURRENT"
fi
fi
echo "Duskテスト後の.envファイルのクリーンアップが完了しました。"
テスト用に、ドメインを確認するルートを追加したりで確認できます。
CSRF mismatchが発生した時は仕込んで確認してみてください。
web.php
Route::get('/debug-domain', function () {
// return response('Domain: ' . var_export(config('session.domain'), true));
return response('Session Domain: ' . var_export(config('session.domain'), true) . ' | App URL: ' . config('app.url'));
});
Route::get('/debug-sanctum-domains', function () {
return response()->json(config('sanctum.stateful'));
});
Route::get('/debug-app-url', function () {
return response()->json([
'app_url_env' => env('APP_URL'),
'parsed_app_url_host' => parse_url(env('APP_URL'), PHP_URL_HOST),
'base_url_config' => config('app.url'),
]);
テスト例
public function testSunctumDomainCheck(): void
{
$this->browse(function (Browser $browser) {
\Log::info('[Dusk] 現在のURL: ' . $browser->driver->getCurrentURL()); // ログ例
$browser->visit('http://nginx/debug-sanctum-domains'); // http://nginx は環境に合わせてください
// ページが完全にロードされるのを待つ (重要)
$browser->pause(1000); // 1秒待機 (または waitForText など)
// ページの生のソースコードをダンプ
$pageSource = $browser->driver->getPageSource();
dump($pageSource);
// 必要であれば、JSONとしてパースし、'nginx'が含まれているか確認
// ただし、生のJSON文字列がページソースとして取得されるため、
// parse_urlで得られるホスト名が配列に含まれているか確認する方が確実です
$jsonResponse = json_decode(strip_tags($pageSource), true); // HTMLタグを除去してからJSONパース
dump($jsonResponse);
// Nginxドメインが配列に含まれていることをアサート
$this->assertContains('nginx', $jsonResponse);
$browser->screenshot($this->getScreenShotPath('sanctum-domain-check', 1));
});
}
- ログイン確認機能において、ログイン済みとして扱われる
展開
仕様らしいです。
browse() はクロージャごとにブラウザインスタンスを使い回す(ブラウザの新規起動ではない)
→ 前のセッションを維持したまま次のクロージャが動く
ログイン系のテスト前にログアウト処理を入れて対応しました。
laravel breezeを使ってる人なら/logoutにpostすればOK.
もしくはsessionを強制的に削除したりと対応は色々可能
- seleniumのイメージが古い
展開
極端に古い場合、通るはずのテストが通らない可能性があります。
selenium/standalone-chrome:latest
を使うか、新しめのバージョンを明示的に指定しましょう。
ログを仕込んでみて、原因不明なエラーだったり挙動をした場合は更新するに限ります。
直接関係のない雑備忘録
- selenium導入後にURLのポートが消える
展開
例: localhost:8000→localhostになる。
明確な原因がわかっていないが、今までLaravel,nginxがよろしく解釈してくれていた部分がしてくれなくなったぽい。
対応は強制的に.env
のAPP_URL
を使用するようにした。
# Providers/AppServiceProvider.php
public function boot(): void
{
// ポートが欠落することがある為対応
if (config('app.url')) {
URL::forceRootUrl(config('app.url')); // これを追加
}
}
- .env.dusk.localについて
展開
.env.dusk.local はartisan dusk起動で自動で読み込まれる。
個別に指定する場合は以下
# .env.dusk.stagingを作った場合
php artisan dusk --env=dusk.staging
github
専用の環境ではないので確認しにくいかもです。
https://github.com/git-syuu2449/laravel_docker
以上です。