16.

Laravel storage フォルダの権限設定とブラウザからのアクセス方法(symlink)

編集
この記事の要点
  • Laravel の storage/app/public/初期状態ではブラウザからアクセス不可
  • php artisan storage:linkpublic/storagestorage/app/public のシンボリックリンク作成
  • 権限不足エラー: chmod -R 775 storage bootstrap/cache + chown www-data:www-data
  • アップロードは $request->file('photo')->store('photos', 'public')
  • URL は asset('storage/photos/xxx.jpg') または Storage::url($path)

Laravel の storage ディレクトリ構成

storage/
├── app/
│   ├── public/         ← public ディスク。ブラウザ公開ファイル(要 symlink)
│   └── private/        ← 非公開ファイル(ログイン後ダウンロード等)
├── framework/
│   ├── cache/          ← フレームワークキャッシュ
│   ├── sessions/       ← セッションファイル(file driver)
│   ├── testing/
│   └── views/          ← Blade コンパイル済キャッシュ
└── logs/
    └── laravel.log     ← アプリケーションログ

ブラウザから直接アクセスできるのは public/ 配下のみ。storage/app/public/ を公開するにはシンボリックリンクを張ります。

storage:link コマンド

# プロジェクトルートで
php artisan storage:link

# 出力
# The [public/storage] link has been connected to [storage/app/public].

# 実体確認
ls -la public/storage
# lrwxrwxrwx 1 user user 33 May 17 12:00 public/storage -> /path/to/storage/app/public

これで storage/app/public/photos/cat.jpghttp://example.com/storage/photos/cat.jpg でアクセス可能になります。

権限設定

Web サーバー(Apache / Nginx)から書き込みできないと「Permission denied」エラー:

# 所有者を Web サーバーユーザーに
sudo chown -R www-data:www-data storage bootstrap/cache

# 書き込み権限
sudo chmod -R 775 storage bootstrap/cache

# 一般ユーザーも所属させたい(開発時)
sudo usermod -aG www-data $USER
# ログアウト → 再ログイン

# SELinux 環境(CentOS / RHEL)
sudo chcon -R -t httpd_sys_rw_content_t storage bootstrap/cache
sudo setsebool -P httpd_unified 1

ファイルアップロード処理

// app/Http/Controllers/PhotoController.php
public function store(Request $request)
{
    $request->validate([
        'photo' => 'required|image|max:5120',  // 5MB
    ]);

    // 方法1: 自動生成ファイル名で storage/app/public/photos/ に保存
    $path = $request->file('photo')->store('photos', 'public');
    // → "photos/abc123.jpg"

    // 方法2: ファイル名指定
    $path = $request->file('photo')->storeAs(
        'photos',
        'user_' . auth()->id() . '.jpg',
        'public'
    );

    // 方法3: Storage Facade
    use Illuminate\Support\Facades\Storage;
    Storage::disk('public')->putFileAs(
        'photos',
        $request->file('photo'),
        'cat.jpg'
    );

    return view('photo.show', ['url' => Storage::url($path)]);
}

表示・URL 生成

use Illuminate\Support\Facades\Storage;

// DB に保存した相対パス: "photos/abc123.jpg"
$photo = Photo::find(1);  // path = "photos/abc123.jpg"

// 公開 URL
$url = Storage::url($photo->path);
// → "/storage/photos/abc123.jpg"

// asset() でも同じ
$url = asset('storage/' . $photo->path);

// 絶対パスが必要なとき
$url = Storage::disk('public')->url($photo->path);
// → "http://example.com/storage/photos/abc123.jpg"

// ファイルが存在するか
if (Storage::disk('public')->exists($photo->path)) { ... }

// 削除
Storage::disk('public')->delete($photo->path);
{{-- Blade --}}

非公開ファイル(ログイン認証付ダウンロード)

// 非公開ファイルは storage/app/ 直下に保存(symlink 不要)
$path = $request->file('contract')
    ->storeAs('contracts/' . auth()->id(), 'contract.pdf');
// → storage/app/contracts/123/contract.pdf (public からアクセス不可)

// コントローラーで認証 + ダウンロード
public function download($id)
{
    $doc = Document::findOrFail($id);
    if ($doc->user_id !== auth()->id()) abort(403);

    return Storage::download($doc->path);
    // または
    return Storage::response($doc->path);
}

カスタムディスク追加

// config/filesystems.php
'disks' => [
    // 既存
    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL').'/storage',
        'visibility' => 'public',
    ],

    // 追加: 画像専用ディスク
    'images' => [
        'driver' => 'local',
        'root' => storage_path('app/public/images'),
        'url' => env('APP_URL').'/storage/images',
        'visibility' => 'public',
    ],

    // S3
    's3' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url' => env('AWS_URL'),
    ],
],

本番デプロイ時の注意

  • php artisan storage:linkデプロイのたびに必要。デプロイスクリプトに含める
  • Symlink が動かないホスティング(一部のシェアードホスティング)では .htaccess で代用
  • S3 / Cloudflare R2 等のクラウドストレージに切り替えるとサーバー間共有 / バックアップが楽
  • storage 配下を Git 管理しない(.gitignorestorage/app/public/*

FAQ

Q: php artisan storage:link がエラーになる
A: 既に public/storage が存在すると失敗します。rm public/storage してから再実行。

Q: アップロード後にファイルが見えない
A: ① symlink 未作成 → storage:link、② 権限不足 → chmod 775、③ public/storage がディレクトリとして実在(symlink でない)→ 削除して再作成。

Q: 大きなファイル(数百 MB)が保存できない
A: php.iniupload_max_filesize / post_max_size / memory_limit を増やす。Web サーバー側のサイズ制限(Nginx client_max_body_size)も。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. インストールと設定
  2. クイックスタート & チュートリアル(初心者向け)
  3. クイックスタート & チュートリアル(中級者向け)
  4. ルーティング
  5. Bladeテンプレート(ビュー/レイアウト)
  6. コントローラー
  7. マイグレーションとテーブル定義
  8. データベースの設定
  9. Eloquentモデル (ORM)
  10. SQLとクエリビルダー
  11. バリデーション
  12. .envファイルの設定値へのアクセス
  13. 動作環境による分岐処理
  14. configフォルダ配下の設定値へのアクセス
  15. assetヘルパーを利用したpublicフォルダへのアクセス
  16. storageフォルダへのアクセス
  17. アプリケーション名の変更
  18. メンテナンス
  19. ログイン画面(認証システム)の作成
  20. ログインの必須化
  21. ログインユーザー情報の取得
  22. ルートの認証化
  23. 本番サーバーへのデプロイ方法
  24. 多言語化
  25. csrf_field
  26. ファイルのダウンロード
  27. CSVのアップロードおよび読み込み(maatwebsite/excel)
  28. ページタイトルの設定
  29. コマンド一覧
  30. エラー一覧
  31. SQLの実行ログ出力方法
  32. キャッシュのクリア
  33. Selectの結果の最初もしくは最後に任意の値を追加する方法
  34. ajaxでPOST通信する際の注意点
  35. ソーシャルログインの実装
  36. セッション情報の確認
  37. ログイン、ユーザー登録、パスワードリセット後のリダイレクト先の変更方法
  38. redirectやreturn viewにメッセージを付与する方法
  39. クッキー(cookie)の設定と取得
  40. クラスの再読み込み
  41. csrfの有効時間を変更する方法
  42. ViewComposerを用いてviewに共通の値を付与する方法
  43. View::shareを用いて共通の値を各ビューに渡す方法
  44. ミドルウェアを用いた処理の共通化
  45. Middleware内でAuth::check()などを使用する方法
  46. Controller以外でリダイレクトする方法
  47. セッションの値の取得/保存/更新/削除
  48. $requestの値を変更する方法
  49. 常時SSL化
  50. ページング(ページネーション)をする方法
  51. vue.jsとの連携
  52. Vue.jsと連携するSPA実行環境構築
  53. .envの値をvue.jsで参照する方法
  54. vue.jsを本番環境にリリースする方法
  55. could not find driver(Windows, MySQL編)