Windowsで動いたのにUbuntuのサーバで動かない
Laravelのweb.php側に画像アップロード機能を実装しようとした時に長時間も躓いた理由について
Laravelの routes/api.php
との挙動の違い
まず、Laravelには 窓口として大きく分けて2つのファイルがあります。
routes/web.php
と routes/api.php
です。
Laravelでは routes/web.php と routes/api.php が「外部からの入り口(窓口)」 になっており、そこからコントローラやミドルウェア、モデルなどに処理がつながっていく構造です。
routes/web.php → フロントエンドからも使う「画面表示用(ビューを返す)」ルート
routes/api.php → バックエンドのAPIとして使う「JSONレスポンス専用」ルート
routes/web.php
主な用途
ブラウザでアクセスする「ウェブページ」向けのルートを定義。
HTMLを返したり、ビューを返す処理に使われます。
特徴
web ミドルウェアグループが自動で適用される。
(例:セッション、CSRFトークン、Cookie 暗号化など)
セッションや認証機能を使う通常のWebアプリで利用。
Vue.jsやReactなどのSPAを使う場合でも、初期画面を返すルートはこちらに置くことが多い。
routes/api.php
主な用途
JSONレスポンスを返す「APIエンドポイント」向けのルートを定義。
フロントエンド(Vue, React, モバイルアプリなど)や外部サービスとの通信で利用されます。
特徴
api ミドルウェアグループが自動で適用される。
(例:セッションレス、ステートレスなトークン認証)
デフォルトで /api/ プレフィックスが付与される。
(例:/api/users)
セッションやCSRFを使わない代わりに、トークンやPassport/Sanctumなどで認証を行う。
まず、僕は api.php
でVueから画像のアップロードは試したことがあり
その時は、アップロードに失敗した時には storage/log/laravel.log
に何かしら原因が詳細に書かれていました。
よくあるのがphpの設定ファイルの設定不足
1. php.ini の設定関連
-
file_uploads が Off
file_uploads = On にする必要があります。 -
アップロードサイズ制限
upload_max_filesize が小さい
post_max_size が upload_max_filesize より小さい場合も失敗します。 -
メモリ不足
memory_limit に引っかかる場合、ファイル処理が止まります。
パーミッション関連
storage ディレクトリや public ディレクトリに書き込み権限がない
Linuxなら chmod -R 775 storage/ bootstrap/cache が推奨。
所有者がWebサーバーユーザー(例: www-data, apache, nginx)でない
chown -R www-data:www-data storage/ public/ のように変更が必要。
Laravel 側の設定やコード
これは起きたことが無いですが設定で拒絶していたら注意が必要です
config/filesystems.php
の設定ミス
disk の設定(public, s3 など)が正しくない。
シンボリックリンクが未作成
php artisan storage:link をしていないと public/storage から参照できない。
バリデーション制限
mimes:jpg,png などで対象外の拡張子が弾かれる。
Web.phpでは以下のコマンドの実行で解決できました
# Ubuntu/Debian 系の例(プロジェクトのdirectoryから実行)
sudo mkdir -p storage/app/public/spec-images
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
今回はapi.phpでもある権限/所有者
関連が原因で
storage/app/public/spec-images に Webサーバ実行ユーザ(例:www-data, nginx, apache) が書き込めない。というものでした。
Laravelログのデバッグ処理を追加してやっと気づけました。
[2025-09-06 00:08:25] local.INFO: spec-image saved {"savedPath":false,"abs":"/var/www/specification_manager/storage/app/public/","url":"http://192.168.X.XXX:8000/storage/spec-images/gazou.jpg"}
savedPath: false
= 書き込み失敗
abs が ディレクトリ(…/storage/app/public/) を指しているのは、savedPath が false のため path(false) が「ディスクのルート」を返しているだけ
つまり putFileAs('spec-images', …) が失敗 しています(例外にならず false を返しているパターン)
以下のファイルのログで判別できるようになりました。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class SpecImageController extends Controller
{
public function index()
{
$dir = storage_path('app/public/spec-images');
$files = is_dir($dir) ? array_values(array_filter(scandir($dir), fn($f) =>
!in_array($f, ['.','..']) && is_file("{$dir}/{$f}")
)) : [];
$images = array_map(fn($f) => asset("storage/spec-images/{$f}"), $files);
return view('spec-images.index', compact('images'));
}
public function store(Request $request)
{
$validated = $request->validate([
'image' => ['required','image','mimes:jpeg,jpg,png,webp','max:5120'], // 5MB
]);
$file = $validated['image']; // UploadedFile
if (!$file) {
return back()->with('status', 'ファイルが取得できませんでした。');
}
// 念のためディレクトリ作成(存在してもOK)
Storage::disk('public')->makeDirectory('spec-images');
// 一意なファイル名
$ext = $file->getClientOriginalExtension();
$name = now()->format('Ymd_His') . '_' . Str::random(8) . '.' . $ext;
// ★ ディスクを明示して保存(失敗なら例外が飛ぶ)
$savedPath = Storage::disk('public')->putFileAs('spec-images', $file, $name);
// 物理パスと公開URLをログ&画面で確認
$abs = Storage::disk('public')->path($savedPath); // storage/app/public/spec-images/xxx
$url = asset('storage/spec-images/'.$name); // public/storage/spec-images/xxx
Log::info('spec-image saved', ['savedPath' => $savedPath, 'abs' => $abs, 'url' => $url]);
return back()->with('status', "アップロードOK: {$savedPath}");
}
}
この関数の下記のログです
Log::info('spec-image saved', ['savedPath' => $savedPath, 'abs' => $abs, 'url' => $url]);
簡単な確認方法として
Vscodeなどでマウス操作でフォルダが削除失敗してpermissionでエラー起きると権限が原因の可能性が高いです
マウス操作で削除出来るようにするには以下のコマンドで書き込みの権限を持てるように変更するといいでしょう。
対処方法(Linux 環境想定)
所有者を Web サーバーユーザーに変更
sudo chown -R www-data:www-data /var/www/プロジェクトフォルダ/storage
sudo chown -R www-data:www-data /var/www/プロジェクトフォルダ/bootstrap/cache
書き込み権限を付与
sudo chmod -R 775 /var/www/プロジェクトフォルダ/storage
sudo chmod -R 775 /var/www/プロジェクトフォルダ/bootstrap/cache
storageフォルダの権限の設定を常にLaravelで動くようにするには
下記のコマンドを実行してみてください(Laravelのプロジェクト直下で)
最重要だったのは chown と chmod
→ public disk not writable
を解消した決め手。
mkdir
は「保存先が無ければ」だけでよい。
storage:link
は最初の1回だけ(すでに存在していれば不要)。
✅ まとめると:
新しいフォルダ作成するたびに必要になるのか?
結論
- 毎回手で権限直す必要はありません。
- 最初に
storage
とbootstrap/cache
の所有権・権限を正しく設定しておけば、 その配下に Laravel
が新しいフォルダを作っても、Web サーバユーザ(例:
www-data
)が所有者になるので、そのまま書き込みできます。
今回のケースで必要だったのは?
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R ug+rwx storage bootstrap/cache
これを一度やれば、以後は Laravel
が勝手に作るサブフォルダも書き込み可能 になります。
例外的に毎回必要になるケース
-
最初に chown/chmod をしていない
→
新しく作られたフォルダの所有者が「root」や「user」になり、www-data
で書けなくなる。 -
チーム開発で別ユーザが直接フォルダを作る
→ そのユーザの所有になってしまい、Web サーバから書けなくなる。 -
ホストとDockerで UID/GID が食い違う
→ ホスト側で作ると権限がずれて、コンテナのwww-data
が書けない。
恒久対策(チーム開発・本番向け)※ここ大事!!
storage に setgid を付けておく:
sudo find storage bootstrap/cache -type d -exec chmod g+s {} \;
→ これで、新しいフォルダも自動で同じグループ権限を継承します。
SELinux 有効な環境なら、最初に chcon をしておけば毎回不要:
sudo chcon -R -t httpd_sys_rw_content_t storage bootstrap/cache
✅ まとめ
- 一度
chown
/chmod
をしておけば、新しいフォルダを作っても
毎回は不要。 - ただし「root や別ユーザが手でフォルダを作る」と再発する。
- Laravel や Web サーバが自動で作る場合は問題なし。
👉 ちなみに、今後は Storage::makeDirectory()
でフォルダを作るようにすれば、Laravel が www-data
権限で作るので確実に安全です。
これで「public ディスク書き込み不可」問題が解消されました。
今後、新しいフォルダを Laravel 側で Storage::makeDirectory()
などで作っても、この権限設定を最初にやっておけば 毎回 chmod/chown は不要 です。
cd /var/www/specification_manager
# 所有権を www-data に
sudo chown -R www-data:www-data storage bootstrap/cache
# 読み/書き/実行 (ディレクトリ入れるための実行ビット)
sudo chmod -R ug+rwx storage bootstrap/cache
# 以後作るサブフォルダも同グループ権限を継承するように setgid を付与(任意だがおすすめ)
sudo find storage bootstrap/cache -type d -exec chmod g+s {} \;
✅ 一度設定すればOKな自動化(最短版)
cd /var/www/specification_manager
# 1) 所有権を www-data に統一(初回だけ)
sudo chown -R www-data:www-data storage bootstrap/cache
# 2) ディレクトリ=2775(g+s=setgid 付き)、ファイル=664
sudo find storage bootstrap/cache -type d -exec chmod 2775 {} \;
sudo find storage bootstrap/cache -type f -exec chmod 0664 {} \;
# 3) setgid 再付与(以後に作られるフォルダも同グループ継承)
sudo find storage bootstrap/cache -type d -exec chmod g+s {} \;
# 4) デフォルトACL(“今後作られるファイル/フォルダ”にも自動で rwX を付与)
sudo setfacl -R -m u:www-data:rwX,g:www-data:rwX storage bootstrap/cache
sudo setfacl -dR -m u:www-data:rwX,g:www-data:rwX storage bootstrap/cache
# 5) ログファイルを用意(念のため)
sudo -u www-data bash -lc 'touch storage/logs/laravel.log && chmod 664 storage/logs/laravel.log'
🔁 クリック一発にするなら(任意)
A. スクリプト化(おすすめ)
/var/www/specification_manager/scripts/fix_storage_perms.sh を作成して実行権限を付与。
scripts/fix_storage_perms.sh
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/specification_manager
sudo chown -R www-data:www-data storage bootstrap/cache
sudo find storage bootstrap/cache -type d -exec chmod 2775 {} \;
sudo find storage bootstrap/cache -type f -exec chmod 0664 {} \;
sudo find storage bootstrap/cache -type d -exec chmod g+s {} \;
sudo setfacl -R -m u:www-data:rwX,g:www-data:rwX storage bootstrap/cache
sudo setfacl -dR -m u:www-data:rwX,g:www-data:rwX storage/bootstrap/cache
sudo -u www-data bash -lc 'touch storage/logs/laravel.log && chmod 664 storage/logs/laravel.log'
echo "OK"
権限付与コマンド
sudo chmod +x scripts/fix_storage_perms.sh
以後は実行コマンドで実行
scripts/fix_storage_perms.sh