Laravel『Storage Link Missing』の原因と対処法

Laravel『Storage Link Missing』の原因と対処法

Laravelの「Storage Link Missing」は、storage/app/public に保存したファイルをブラウザから配信するためのシンボリックリンク(public/storage)が存在しない、壊れている、またはWebサーバ設定によりリンク先へ到達できないときに起きる。典型的には「画像URLは /storage/… になっているのに 404」「Storage::url() が /storage/… を返すが表示されない」「本番デプロイ後だけ表示されない」などの形で発生する。まずは “public/storage が存在して storage/app/public を指しているか” と “Webサーバが public 配下を正しく公開しているか” を機械的に確認する。

症状と発生条件(典型例)

発生条件の典型:
・Storage::disk(‘public’)->put(…) で保存した画像が表示されない
・/storage/xxxx.jpg にアクセスすると 404 / 403
・php artisan storage:link を実行していない(または実行できない)
・デプロイで public/storage が消える(リリースディレクトリ切替方式)
・Windows環境や権限制限でシンボリックリンクが作れない
・Nginx/Apache が symlink を辿れない設定になっている
・DOCUMENT_ROOT が public ではなくプロジェクト直下で、/storage がそもそも公開されていない

まず確認:public/storage の存在とリンク先(最重要)

公開リンクの正体はこれ。
・リンク:public/storage
・実体:storage/app/public
確認コマンド例。

ls -la public | grep storage
ls -la storage/app | grep public

期待状態:
・public/storage が存在する
・それが storage/app/public を指している(シンボリックリンク)

基本の対処:storage:link を実行してリンクを作る

未作成ならこれで作る。

php artisan storage:link

作成後、ブラウザで /storage に置いたファイルへアクセスして確認する。

原因1:リンクが存在しない(デプロイ後に消える/作っていない)

発生条件:
・初回セットアップで storage:link を忘れた
・Capistrano等のリリース切替で public/ が毎回作り直される
・CI/CDで storage:link を実行していない
対処:デプロイ手順に storage:link を組み込むか、shared領域でリンク/実体を管理する。
(リリース切替方式なら public/storage を shared に向ける運用が多い)

原因2:リンクはあるが “壊れている”(指している先が違う/相対パス崩れ)

発生条件:
・手動でリンクを作り、リンク先が誤っている
・ディレクトリ構成を変えたのにリンクを作り直していない
・コンテナ内パスとホスト側パスの不一致
対処:一度削除して作り直す。

rm -f public/storage
php artisan storage:link

(Windowsの場合は rm の代わりに手動削除や PowerShell コマンドになる)

原因3:WebサーバのDOCUMENT_ROOTが public になっていない

Laravelは基本的に public をドキュメントルートにする前提。
発生条件:
・/var/www/html(プロジェクト直下)を公開している
・/public が公開されておらず、/storage へ到達できない
対処:Nginx/Apache のルートを public に変更する。
Nginx例(概念)。

rm -f public/storage
php artisan storage:link

Apache例(概念)。

DocumentRoot /path/to/project/public

この層がズレていると、storageリンク以前に全体の公開パスが崩れる。

原因4:保存先ディスク/パスが想定と違う(publicディスクを使っていない)

発生条件:
・disk(‘local’) に保存しているのに /storage で見ようとしている
・put の保存先が storage/app/public ではない
対処:公開したいものは public ディスク(storage/app/public)へ保存する。

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

public function upload(Request $request)
{
    $path = $request->file('image')->store('images', 'public'); // storage/app/public/images/...
    $url  = Storage::disk('public')->url($path);               // /storage/images/...

    return response()->json(['path' => $path, 'url' => $url]);
}

この形なら “保存先” と “公開URL” が一致しやすい。

原因5:権限/所有者の問題で読み込みできない(403/500)

発生条件:
・storage/app/public 配下の権限がWebサーバユーザーで読めない
・アップロードしたファイルのパーミッションが厳しすぎる
・SELinux等でアクセスが拒否される
対処:
・storage/app/public のディレクトリ/ファイルがWebサーバから読める状態にする
・アップロード時の権限、umask、所有者を確認
権限確認例。

ls -la storage/app/public
ls -la storage/app/public/images | head

(運用ポリシーに合わせて所有者/グループ/権限を調整する)

原因6:シンボリックリンクが作れない環境(Windows/制限されたホスティング)

発生条件:
・Windowsで管理者権限/開発者モード無し
・共有ホスティングで symlink が禁止
対処:
・Windowsなら管理者で実行、または開発者モード等の要件を満たす
・symlink禁止環境なら、public 配下へ実体をコピーする運用に切り替える(自動同期)
例:アップロード時に public/uploads に保存して直接配信(アプリ設計の変更が必要)

サンプル:storageリンク前提の画像表示(Blade)

publicディスクに保存したpathをDBに持ち、表示時は Storage::url() を使う。

<img src="{{ Storage::url($user->avatar_path) }}" alt="avatar">

avatar_path が images/xxx.jpg なら、/storage/images/xxx.jpg になる。

チェックリスト(上から順に確認する)

1) public/storage が存在し、storage/app/public を指すシンボリックリンクになっているか
2) php artisan storage:link を実行済みか(壊れていれば public/storage を消して作り直したか)
3) WebサーバのDOCUMENT_ROOTが project/public になっているか(publicが公開されているか)
4) 保存先が public ディスク(storage/app/public)になっているか(localに保存していないか)
5) /storage/… へのアクセスが 404 か 403 かで層を切り分けたか(404=到達不可、403=権限/設定)
6) デプロイ方式で public/storage が毎回消えていないか(shared運用/デプロイ手順に組み込む)
7) symlink禁止環境ではないか(Windows/共有ホスティング)