Home About Skills Products Work
Projects Services Experience
Learn
Tutorials Courses Blogs Resources
Contact

How to Fix Laravel Storage 403 Forbidden on cPanel Using Symbolic Links

Advertisement

How to Fix Laravel Storage 403 Forbidden on cPanel Using Symbolic Links

If Laravel images or uploaded files are not loading from URLs such as:

https://yourdomain.com/storage/media/image.jpg

and the server returns 403 Forbidden, the issue is often related to how the Laravel storage directory is exposed through the domain's document root.

This guide explains how to correctly configure Laravel storage symlinks when your Laravel project is located outside the domain's public_html directory on cPanel.

The Scenario

In this setup, the Laravel application is located at:

/home/codewith/codewithbikesh

while the domain's document root is:

/home/codewith/public_html

The Laravel storage directory is:

/home/codewith/codewithbikesh/storage/app/public

The project structure looks like this:

/home/codewith/
│
├── public_html/                         ← Domain document root
│   ├── index.php
│   ├── .htaccess
│   └── storage -> ../codewithbikesh/public/storage
│
└── codewithbikesh/                      ← Laravel project
    ├── app/
    ├── bootstrap/
    ├── config/
    ├── routes/
    ├── public/
    │   └── storage -> ../storage/app/public
    │
    └── storage/
        └── app/
            └── public/
                └── media/
                    └── image.jpg

Why Does the 403 Error Happen?

Laravel normally creates this symbolic link:

public/storage
        ↓
storage/app/public

This allows files inside:

storage/app/public

to be accessed through:

/storage/...

However, in this particular cPanel configuration, the domain is not directly using codewithbikesh/public as its document root.

The domain is using:

/home/codewith/public_html

Therefore, when a visitor requests:

https://yourdomain.com/storage/media/image.jpg

Apache initially looks for:

/home/codewith/public_html/storage/media/image.jpg

But Laravel's storage symlink exists here:

/home/codewith/codewithbikesh/public/storage

These are two different locations.

Therefore, we need a second symbolic link inside public_html.

Step 1 — Go to the Laravel Project

Open your cPanel Terminal and navigate to the Laravel project:

cd ~/codewithbikesh

Step 2 — Remove the Existing Storage Link

Remove the existing Laravel storage link:

rm -rf public/storage
Warning: This removes only the symbolic link, not your uploaded files inside storage/app/public.

Step 3 — Create Laravel's Storage Link

Run:

php artisan storage:link --relative

You should see:

INFO  The [public/storage] link has been connected to [storage/app/public].

Verify it:

ls -la public/storage

Expected output:

public/storage -> ../storage/app/public/

If --relative Produces an Error

You may see:

RuntimeException

To enable support for relative links, please install the symfony/filesystem package.

If this happens, install the required package:

composer require symfony/filesystem

Then recreate the storage link:

rm -rf public/storage
php artisan storage:link --relative

Verify again:

ls -la public/storage

Step 4 — Verify the Uploaded File Exists

Before troubleshooting Apache or permissions, make sure the actual image exists.

Check the media directory:

ls -lah storage/app/public/media/

Or check a specific image:

ls -lah storage/app/public/media/IMAGE_NAME.jpg

For example:

ls -lah storage/app/public/media/QIQzYP4K0yMCeTdlMIfPonIrvf2J9A09MjNXQRMC.jpg

If the file exists, you should see something similar to:

-rw-r--r-- 1 codewith codewith 121K ... IMAGE_NAME.jpg

If the file does not exist, the problem is not the symbolic link. You need to check where Laravel is actually storing the uploaded file.

Step 5 — Check Directory Permissions

Check the permissions of the storage directories:

ls -ld storage
ls -ld storage/app
ls -ld storage/app/public
ls -ld storage/app/public/media

Typical directory permissions should be:

755

If necessary:

chmod 755 storage
chmod 755 storage/app
chmod 755 storage/app/public
chmod 755 storage/app/public/media

For individual image files, 644 is normally appropriate:

chmod 644 storage/app/public/media/IMAGE_NAME.jpg

Step 6 — Create the Important public_html Storage Link

This is the most important step when your domain's document root is public_html.

First, go to the domain document root:

cd ~/public_html

Remove an existing storage link or directory:

rm -rf storage

Now create a symbolic link:

ln -s ../codewithbikesh/public/storage storage

This creates:

/home/codewith/public_html/storage

which points to:

/home/codewith/codewithbikesh/public/storage

Step 7 — Verify the public_html Symlink

Run:

ls -la storage

You should see:

storage -> ../codewithbikesh/public/storage

Now verify the complete destination:

readlink -f storage

The expected result is:

/home/codewith/codewithbikesh/storage/app/public

This confirms that both symbolic links are working correctly.

Understanding the Complete Storage Flow

The final configuration looks like this:

Browser
   │
   │ https://yourdomain.com/storage/media/image.jpg
   ↓
/home/codewith/public_html/storage
   │
   │ Symlink
   ↓
/home/codewith/codewithbikesh/public/storage
   │
   │ Symlink
   ↓
/home/codewith/codewithbikesh/storage/app/public
   │
   ↓
media/image.jpg

This is the key concept behind the fix.

Step 8 — Test the Image URL

After creating the symlink, test the actual image URL from the server:

curl -I https://yourdomain.com/storage/media/IMAGE_NAME.jpg

A successful response should look like:

HTTP/2 200

or:

HTTP/1.1 200 OK

You can then open the same URL directly in your browser:

https://yourdomain.com/storage/media/IMAGE_NAME.jpg

Real Example

Suppose the uploaded image is:

storage/app/public/media/QIQzYP4K0yMCeTdlMIfPonIrvf2J9A09MjNXQRMC.jpg

The public URL becomes:

https://codewithbikesh.com/storage/media/QIQzYP4K0yMCeTdlMIfPonIrvf2J9A09MjNXQRMC.jpg

The request travels through:

https://codewithbikesh.com/storage/
             ↓
/home/codewith/public_html/storage
             ↓
/home/codewith/codewithbikesh/public/storage
             ↓
/home/codewith/codewithbikesh/storage/app/public
             ↓
media/QIQzYP4K0yMCeTdlMIfPonIrvf2J9A09MjNXQRMC.jpg

Quick Fix for Future Projects

If you encounter the same setup again, these are the essential commands:

cd ~/codewithbikesh

rm -rf public/storage

php artisan storage:link --relative

Then create the additional link from the domain document root:

cd ~/public_html

rm -rf storage

ln -s ../codewithbikesh/public/storage storage

Verify:

readlink -f storage

Expected:

/home/codewith/codewithbikesh/storage/app/public

Finally test:

curl -I https://yourdomain.com/storage/media/IMAGE_NAME.jpg

Troubleshooting Checklist

If the image is still returning 403 Forbidden, check these items in order:

1. Confirm the image exists

ls -lah storage/app/public/media/

2. Confirm Laravel's storage link

ls -la ~/codewithbikesh/public/storage

3. Confirm the public_html link

ls -la ~/public_html/storage

4. Confirm the final destination

readlink -f ~/public_html/storage

It should point to:

/home/codewith/codewithbikesh/storage/app/public

5. Check permissions

Directories:

755

Files:

644

6. Test with curl

curl -I https://yourdomain.com/storage/media/IMAGE_NAME.jpg

7. Check cPanel/Apache errors

If everything above is correct but you still receive 403, check:

  1. cPanel → Metrics → Errors
  2. Apache error logs
  3. .htaccess
  4. Symlink restrictions
  5. Directory permissions
  6. File permissions
  7. Hosting provider security rules

Important Notes

Don't delete storage/app/public

Your uploaded files are stored there.

The command:

rm -rf public/storage

is intended to remove the symbolic link, not your actual uploaded files.

Don't repeatedly recreate a working symlink

If:

readlink -f ~/public_html/storage

already returns:

/home/codewith/codewithbikesh/storage/app/public

then the symlink is working. Move on to checking permissions, Apache configuration, or the actual HTTP response.

Don't run a full Composer update unnecessarily

If storage is the only problem, avoid:

composer update

A full dependency update can change many packages and potentially introduce compatibility problems.

Only install the required package if Laravel specifically reports that symfony/filesystem is missing:

composer require symfony/filesystem

Conclusion

Laravel's storage:link normally solves public file access, but cPanel setups where the domain document root is public_html and the Laravel project is located elsewhere can require an additional symbolic link.

The final setup is:

public_html/storage
        ↓
codewithbikesh/public/storage
        ↓
codewithbikesh/storage/app/public
        ↓
media/image.jpg

Once this chain is correctly established, URLs such as:

https://yourdomain.com/storage/media/image.jpg

can be served directly by the web server.

The most important lesson is:

Always check the actual domain document root. The storage symlink must ultimately be reachable from the directory Apache is serving.


Laravel for Beginners: Setting Up Your First Project

Laravel for Beginners: Setting Up Your First Project

A complete first-day guide to Laravel — installation, folder structure, your first route, and your first migration.

Building a REST API with Laravel: A Complete Guide

Building a REST API with Laravel: A Complete Guide

API routes, resources, Form Request validation, correct status codes, and Sanctum auth — a real, production-reasonable Laravel API.

Laravel Eloquent Relationships Explained with Real Examples

Laravel Eloquent Relationships Explained with Real Examples

One-to-many, many-to-many, polymorphic, and the N+1 query trap that catches almost every Laravel developer at least once.

Esc