Skip to main content

Give Your Laravel App 10 GB of Free Cloud Storage with Cloudflare R2

Set Up Cloudflare R2 in Laravel

Updated 11 min readMarkdown
Part of seriesLaravel
Give Your Laravel App 10 GB of Free Cloud Storage with Cloudflare R2

Did you know you can store files in the cloud with Cloudflare R2 and integrate it into your Laravel application?

Cloudflare R2 offers a generous free tier with 10 GB-month of storage per month, making it an excellent option for developers who want to store images, documents, and other application files without immediately paying for cloud storage.

Laravel supports several storage options, including local storage and Amazon S3. Another option is Cloudflare R2, an object storage service that supports the S3 API and doesn't charge for standard internet egress bandwidth.

In this tutorial, we'll configure Cloudflare R2 in Laravel from scratch. No previous Cloudflare R2 experience is required.

What we'll build

We'll connect a Laravel application to a Cloudflare R2 bucket using Laravel's filesystem system.

The process is straightforward:

  1. Create a Cloudflare R2 bucket.
  2. Generate R2 API credentials.
  3. Install the required Laravel package.
  4. Configure environment variables.
  5. Add an R2 disk to Laravel.
  6. Test the connection.
  7. Upload and manage files.
  8. Configure public file access.

You can follow these steps using an existing Laravel project.

Step 1: Create a Cloudflare account and R2 bucket

First, you need a Cloudflare account.

If you don't have one, visit Cloudflare and create an account.

Once logged in:

  1. Open your Cloudflare dashboard.
  2. Select your account.
  3. Navigate to R2 Object Storage.
  4. Click Create bucket.
  5. Enter a bucket name.

For this tutorial, we'll use:

laravel-assets

Choose your preferred location or jurisdiction settings if applicable, then create the bucket.

What is a bucket?

Think of a bucket as a container for your application's files.

For example, your bucket might eventually contain:

laravel-assets/
├── profile-images/
│   ├── user-1.jpg
│   └── user-2.jpg
├── products/
│   ├── product-1.png
│   └── product-2.png
└── documents/
    └── invoice.pdf

These are object keys and prefixes, not necessarily real folders. R2 stores objects, and slash-separated names make them appear organized.

Step 1 complete: You now have a bucket where Laravel can store files.

Step 2: Create R2 API credentials

Laravel needs permission to communicate with your bucket.

Cloudflare provides S3-compatible credentials for this purpose.

In your Cloudflare dashboard:

  1. Open R2 Object Storage.
  2. Find the R2 API token management section.
  3. Create an S3-compatible API token.
  4. Select the required read/write permissions.
  5. Restrict access to your laravel-assets bucket where possible.
  6. Create the token and copy the credentials.

You'll need these values:

Credential Purpose
Access Key ID Identifies your API credentials
Secret Access Key Signs requests to R2
Account ID Identifies your Cloudflare account

The S3 endpoint will look like this:

https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com

Replace YOUR_ACCOUNT_ID with your actual Cloudflare account ID.

Keep your secret access key safe. Do not publish it in GitHub or put it in frontend JavaScript.

For additional information, see the Cloudflare R2 API token guide.

Step 3: Open your Laravel project

Open your Laravel project in a terminal.

For example:

cd /var/www/my-laravel-app

If you're developing locally, navigate to your project's directory instead.

Check that Laravel and Composer are available:

php artisan --version
composer --version

If these commands work, you're ready to install the storage adapter.

Step 4: Install the S3 filesystem package

Laravel uses a filesystem library called Flysystem to communicate with different storage providers.

Cloudflare R2 supports the S3 API, so Laravel can use its S3 adapter.

Run this command inside your Laravel project:

composer require league/flysystem-aws-s3-v3 "^3.0"

Wait for Composer to finish installing the package.

You do not need to install a separate Cloudflare R2 package.

Step 5: Add your R2 credentials to .env

Open your Laravel project's .env file.

Add the following variables:

FILESYSTEM_DISK=r2

R2_ACCESS_KEY_ID=your_access_key_id
R2_SECRET_ACCESS_KEY=your_secret_access_key

R2_BUCKET=laravel-assets
R2_ENDPOINT=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com

Replace each placeholder with your actual values.

For example, R2_BUCKET must match the bucket you created in Step 1.

What does each variable mean?

  • FILESYSTEM_DISK=r2: Tells Laravel to use R2 as its default storage disk.
  • R2_ACCESS_KEY_ID: Your R2 access key.
  • R2_SECRET_ACCESS_KEY: Your R2 secret.
  • R2_BUCKET: Your bucket name.
  • R2_ENDPOINT: The S3 API endpoint for your Cloudflare account.

We haven't configured the public URL yet. That's fine; first, let's establish a working connection.

Security tip: Your .env file should not be committed to Git. Make sure it remains excluded through .gitignore.

Step 6: Configure the R2 disk in Laravel

Now we need to tell Laravel how to use the credentials from .env.

Open:

config/filesystems.php

Find the disks array. Inside that array, add the following disk configuration:

'r2' => [
    'driver' => 's3',

    'key' => env('R2_ACCESS_KEY_ID'),
    'secret' => env('R2_SECRET_ACCESS_KEY'),

    'region' => 'auto',
    'bucket' => env('R2_BUCKET'),
    'endpoint' => env('R2_ENDPOINT'),

    'use_path_style_endpoint' => false,

    'throw' => true,
],

Make sure you add the r2 entry inside the existing disks array, not outside it.

Why are we using the s3 driver?

Although we're using Cloudflare R2, its API is compatible with the S3 protocol. Laravel can therefore use the existing S3 filesystem driver.

The region value is set to auto, which is the standard configuration for R2's S3 API.

The throw option allows filesystem write failures to throw exceptions, making problems easier to identify during development.

Save the file.

Step 7: Clear Laravel's configuration cache

Laravel can cache configuration values. If the application has cached its configuration, new environment variables may not be picked up until you clear or rebuild the cache.

Run:

php artisan config:clear

If you're deploying to production and use configuration caching, rebuild it after confirming your configuration is correct:

php artisan config:cache

Avoid printing your secret credentials into logs or terminal output while troubleshooting.

Step 8: Test the R2 connection

Before adding file uploads to your application, let's test the connection.

Laravel includes a tool called Tinker that lets you run Laravel code interactively.

Start it:

php artisan tinker

Inside Tinker, run:

Storage::disk('r2')->put(
    'test.txt',
    'Hello from Laravel and Cloudflare R2!'
);

If the operation succeeds, check whether the file exists:

Storage::disk('r2')->exists('test.txt');

You should receive:

true

Now retrieve the contents:

Storage::disk('r2')->get('test.txt');

Expected result:

"Hello from Laravel and Cloudflare R2!"

You can also open your R2 bucket in the Cloudflare dashboard and check whether test.txt appears in the object list.

If all three checks succeed, Laravel is communicating with R2.

When you've finished testing, you can remove the test file:

Storage::disk('r2')->delete('test.txt');

Step 9: Upload a file from Laravel

Now let's implement a practical example.

Imagine that your application has a form where users can upload profile pictures.

Create a controller if you don't already have one:

php artisan make:controller ProfileImageController

Open the controller and add:

<?php

namespace App\Http\Controllers;

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

class ProfileImageController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'image' => [
                'required',
                'image',
                'max:5120',
            ],
        ]);

        $path = Storage::disk('r2')->putFile(
            'profile-images',
            $request->file('image')
        );

        return response()->json([
            'message' => 'Image uploaded successfully.',
            'path' => $path,
        ]);
    }
}

This example validates that the uploaded file is an image and limits its size to 5 MB.

Laravel uploads the image to R2 under the profile-images prefix.

The returned path might look like:

profile-images/a1b2c3d4e5.jpg

The exact filename is generated by Laravel.

Add an upload route

In routes/web.php, add:

use App\Http\Controllers\ProfileImageController;
use Illuminate\Support\Facades\Route;

Route::post(
    '/profile-image',
    [ProfileImageController::class, 'store']
)->middleware('auth');

This example uses Laravel's authentication middleware. If your application uses an API authentication system, configure the appropriate middleware and route location.

The route accepts a POST request with an image file field.

Remember to protect upload endpoints, validate incoming files, and apply authorization appropriate to your application.

Step 10: Store the file path in your database

In a real application, you'll usually want to remember which file belongs to which user.

Instead of storing the full public URL, store the object key returned by Laravel.

For example:

profile-images/a1b2c3d4e5.jpg

You can save this value in a database column, such as profile_image_path.

Why store the path instead of the full URL?

Because your public domain or storage provider might change in the future. If you store only the object key, you can generate the URL using your current storage configuration.

For example:

$user->profile_image_path = $path;
$user->save();

This assumes the profile_image_path column exists on your user table and $user is the authenticated user.

Step 11: Configure public file access

By default, uploading an object to R2 does not automatically make it publicly accessible.

If you want profile pictures or product images to load directly in a browser, you need to configure public delivery.

Set up a custom domain

In the Cloudflare dashboard:

  1. Open R2 Object Storage.
  2. Select your bucket.
  3. Find the custom domain settings.
  4. Connect a domain or subdomain managed through Cloudflare.
  5. Complete the setup and wait until the domain is active.

For example, you might use:

https://assets.example.com

Replace example.com with your own domain.

A custom domain provides a convenient address for serving public assets.

For private files, keep the bucket private instead of exposing all objects publicly.

See the Cloudflare R2 public bucket documentation.

Configure the public URL in Laravel

Once your custom domain is active, add this to .env:

R2_URL=https://assets.example.com

Then update the R2 disk configuration in config/filesystems.php:

'r2' => [
    'driver' => 's3',

    'key' => env('R2_ACCESS_KEY_ID'),
    'secret' => env('R2_SECRET_ACCESS_KEY'),

    'region' => 'auto',
    'bucket' => env('R2_BUCKET'),
    'endpoint' => env('R2_ENDPOINT'),
    'url' => env('R2_URL'),

    'use_path_style_endpoint' => false,
    'throw' => true,
],

Clear the configuration cache after making the changes:

php artisan config:clear

Generate the image URL

You can now generate a URL for the uploaded image:

$url = Storage::disk('r2')->url(
    'profile-images/a1b2c3d4e5.jpg'
);

The resulting URL should resemble:

https://assets.example.com/profile-images/a1b2c3d4e5.jpg

This assumes your custom domain is configured correctly and the object is publicly accessible.

Step 12: Generate temporary URLs for private files

Some files should not be publicly accessible, such as invoices, student documents, and internal reports.

For these files, keep the bucket private and generate a presigned URL for an authorized user.

Example:

$url = Storage::disk('r2')->temporaryUrl(
    'private/invoice.pdf',
    now()->addMinutes(10)
);

This generates a temporary link that is intended to expire after ten minutes.

Before using this in production, verify that the URL works with your R2 endpoint and signing configuration. Do not assume that every custom public domain supports presigned S3 requests.

A presigned URL acts as a temporary access credential. Anyone who obtains it can use it while it remains valid, so avoid exposing it unnecessarily.

See the Cloudflare R2 presigned URL documentation.

Step 13: Troubleshoot common errors

Here are a few common problems beginners may encounter.

InvalidArgument: Authorization

If the browser displays an XML response such as:

<Error>
    <Code>InvalidArgument</Code>
    <Message>Authorization</Message>
</Error>

Check whether you are opening the S3 API endpoint directly.

The endpoint:

https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com

is intended for S3 API operations. It is not automatically a public asset URL.

Use Laravel's filesystem adapter for authenticated operations, your configured custom domain for public files, or a correctly generated presigned URL for private files.

Also verify that your API credentials are correct and have the required permissions.

Access denied

Check that your token has the necessary read or write permissions and is authorized for the correct bucket.

For example, uploading files requires write permission, while retrieving objects requires read permission.

Laravel cannot find the R2 configuration

Check the following:

  • The disk configuration is inside the disks array.
  • The environment variable names match your configuration.
  • The bucket name and endpoint are correct.
  • You've cleared or rebuilt the configuration cache.

File uploads succeed but URLs do not work

Uploading a file and making it publicly accessible are separate tasks.

Confirm that your custom domain is active, your R2_URL is correct, and the object's access policy permits public reads.

Step 14: Production best practices

Before using R2 in a live application, follow these recommendations:

  • Validate all uploads on the server.
  • Restrict API credentials to the required bucket and permissions.
  • Keep private documents in private buckets or private access configurations.
  • Store object keys in your database instead of hard-coded URLs.
  • Use unique filenames to prevent accidental overwrites.
  • Handle storage exceptions and log failures safely.
  • Avoid exposing secret keys and presigned URLs.
  • Monitor storage usage, operation counts, and current pricing.

Cloudflare R2 removes standard egress bandwidth fees, but storage and operations can still incur costs.

Conclusion

You have now learned how to integrate Cloudflare R2 with Laravel from the beginning.

The process involves creating a bucket, generating credentials, installing the S3 adapter, configuring Laravel's filesystem disk, and testing uploads.

Once configured, you can use Laravel's familiar Storage facade to upload, retrieve, and delete files while keeping your application code independent of the underlying storage provider.

Official documentation

About the author

  • Hi there! I'm Anik Kumar Nandi, passionate programmer, strongly focus on backend technologies. I specialize in PHP and Laravel, and I'm always exploring new ways to enhance the robustness and efficiency of my code. In addition to PHP and Laravel, my skill set includes RESTful API development, JavaScript, jQuery, Bootstrap, Ajax, and WordPress customization. Also deeply interested in AWS. Feel free to check out my GitHub profile and my portfolio site to learn more about my work and projects. Let's connect and create something amazing together!

No comments yet. Be the first to comment.