# 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](https://dash.cloudflare.com/) 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:

```text
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](https://developers.cloudflare.com/r2/api/s3/tokens/).

## Step 3: Open your Laravel project

Open your Laravel project in a terminal.

For example:

```bash
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:

```bash
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:

```bash
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:

```dotenv
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:

```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'),

    '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:

```bash
php artisan config:clear
```

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

```bash
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:

```bash
php artisan tinker
```

Inside Tinker, run:

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

If the operation succeeds, check whether the file exists:

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

You should receive:

```text
true
```

Now retrieve the contents:

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

Expected result:

```text
"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:

```php
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:

```bash
php artisan make:controller ProfileImageController
```

Open the controller and add:

```php
<?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:

```text
profile-images/a1b2c3d4e5.jpg
```

The exact filename is generated by Laravel.

### Add an upload route

In `routes/web.php`, add:

```php
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:

```text
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:

```php
$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](https://developers.cloudflare.com/r2/buckets/public-buckets/).

### Configure the public URL in Laravel

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

```dotenv
R2_URL=https://assets.example.com
```

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

```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:

```bash
php artisan config:clear
```

### Generate the image URL

You can now generate a URL for the uploaded image:

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

The resulting URL should resemble:

```text
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:

```php
$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](https://developers.cloudflare.com/r2/api/s3/presigned-urls/).

## 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:

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

Check whether you are opening the S3 API endpoint directly.

The endpoint:

```text
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

- [Cloudflare R2](https://developers.cloudflare.com/r2/)
- [R2 S3 API compatibility](https://developers.cloudflare.com/r2/api/s3/)
- [R2 API tokens](https://developers.cloudflare.com/r2/api/s3/tokens/)
- [R2 public buckets](https://developers.cloudflare.com/r2/buckets/public-buckets/)
- [R2 presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)
- [Laravel filesystem documentation](https://laravel.com/docs/12.x/filesystem)
