Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13: Display an Image from the Storage Folder (storage:link)

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13: Display an Image from the Storage Folder (storage:link)

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 7, 2026
Laravel 13: Display an Image from the Storage Folder (storage:link)

To display an image from the storage folder in Laravel 13, store the file on the public disk, run php artisan storage:link once, and then use Storage::url($path) or asset('storage/' . $path) as the image source. Files in storage/app/public are not reachable by the web server until that symbolic link exists. This guide targets Laravel 13 and PHP 8.3 or newer.

Why can't the browser open files in storage/app?

Only the public directory is served by your web server. The storage/app/private and storage/app/public folders sit outside it. The storage:link command creates a symbolic link from public/storage to storage/app/public, so files in the second location get a public URL while the rest of storage stays private.

Prerequisites

  • Laravel 13 on PHP 8.3 or newer.
  • Permission to create symbolic links on your server or dev machine. On Windows, run the terminal as administrator the first time if the command fails.

Step 1: Create the symbolic link

Code
php artisan storage:link

This reads the links array in config/filesystems.php. By default, public_path('storage') points to storage_path('app/public'). Run it once per environment, including after each fresh deployment to a new server or container, because a symlink is not stored in version control.

Step 2: How do you upload and save the file?

JavaScript
<?php

namespace App\Http\Controllers;

use App\Models\Photo;
use Illuminate\Http\Request;

class PhotoController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'title' => ['required', 'string', 'max:150'],
            'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
        ]);

        $path = $request->file('image')->store('photos', 'public');

        Photo::create([
            'title' => $request->input('title'),
            'path' => $path,
        ]);

        return back()->with('status', 'Photo uploaded.');
    }
}

The store() method generates a unique file name and returns the relative path, such as photos/Xk3f...jpg. Save that path in the database, not the full URL, so you can change your domain or move to cloud storage later.

Step 3: Show the image in Blade

Shell
{{-- resources/views/photos/index.blade.php --}}
@foreach ($photos as $photo)
    <figure>
        <img src="{{ Storage::disk('public')->url($photo->path) }}"
             alt="{{ $photo->title }}"
             width="320" loading="lazy">
        <figcaption>{{ $photo->title }}</figcaption>
    </figure>
@endforeach

You can also write asset('storage/' . $photo->path). Storage::url() is the better choice because it follows the disk configuration. If you switch the disk to S3 later, the same view code still works.

How do you set the correct URL?

The public disk builds URLs from APP_URL plus /storage. If images show a wrong host or port, such as http://localhost when you run on another port, fix APP_URL in .env and clear the config cache:

Code
php artisan config:clear

Can you add a fallback when the file is missing?

PHP
@php
    $exists = $photo->path && Storage::disk('public')->exists($photo->path);
@endphp

<img src="{{ $exists ? Storage::disk('public')->url($photo->path) : asset('images/placeholder.png') }}"
     alt="{{ $photo->title }}">

How do you serve private files?

Files that only logged-in users should see belong on the local disk, not the public one. Serve them through a controller that checks permissions:

JavaScript
public function show(Photo $photo)
{
    $this->authorize('view', $photo);

    return Storage::disk('local')->response($photo->path);
}

For temporary access to private files on S3-compatible disks, Laravel can create temporary URLs with Storage::temporaryUrl().

How do you delete or replace the image?

Deleting the database row does not delete the file. Remove the file when you replace or delete a record, otherwise unused uploads fill the disk over time.

JavaScript
public function update(Request $request, Photo $photo)
{
    $request->validate(['image' => ['required', 'image', 'max:2048']]);

    Storage::disk('public')->delete($photo->path);

    $photo->update([
        'path' => $request->file('image')->store('photos', 'public'),
    ]);

    return back();
}

public function destroy(Photo $photo)
{
    Storage::disk('public')->delete($photo->path);
    $photo->delete();

    return back();
}

How do you test uploads without touching real files?

Laravel can fake a disk, so tests never write to your real storage folder.

JavaScript
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

it('stores an uploaded photo', function () {
    Storage::fake('public');

    $this->actingAs(User::factory()->create())
        ->post('/photos', [
            'title' => 'Sunset',
            'image' => UploadedFile::fake()->image('sunset.jpg'),
        ])
        ->assertRedirect();

    $photo = Photo::first();

    Storage::disk('public')->assertExists($photo->path);
});

The fake disk has no symlink, so this test checks storing and validation only. Use a manual browser check to confirm the public URL works.

Common mistakes

  • Not running php artisan storage:link, which gives a 404 for every image.
  • Saving the file on the default local disk but looking for it under the public URL. Pass 'public' as the disk name.
  • Storing the complete URL in the database instead of the relative path.
  • Wrong APP_URL, which produces links to the wrong host.
  • Copying the project to a new server with an old, broken symlink. Delete public/storage and run the command again.
  • Allowing any file type. Always validate with image and mimes.

How do you verify it works?

  1. Confirm public/storage exists and opens storage/app/public.
  2. Upload a test image and check that it appears in storage/app/public/photos.
  3. Open the URL printed in the page source in a new tab. It should show the image.
  4. Run php artisan tinker and call Storage::disk('public')->url('photos/example.jpg') to inspect the generated URL.

FAQ

Why do I get a 403 or 404 for /storage/file.jpg?

The most common reasons are a missing symlink, a wrong disk, or incorrect permissions on the folder. Re-run storage:link and check the file exists.

Does storage:link work in Docker?

Yes, but run it inside the container, or in your entrypoint script, so the link points to the paths inside the container.

Can I use a different link location?

Yes. Add entries to the links array in config/filesystems.php and run the command again.

Is it safe to expose storage/app/public?

Yes for files meant to be public. Never put private documents there.

References

Article Index & Topic Tags
LaravelBladeStorageFilesystem

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.