Server GTM Hosting has moved to its own platform.

Visit Taglinko(opens in a new tab)
Applications

Keep Laravel Uploads After a Redeploy with Cloudflare R2

Store uploaded images in Cloudflare R2 so they survive every redeploy. Set up a bucket, an API key and eight variables, then check your Laravel code.

6 min readUpdated Nov 5, 2026FlyNode docs
Browse documentation

Need a hand?

Can't find your answer? Our team replies fast.

Contact support

Every time FlyNode redeploys your app, it builds a fresh copy of it. Anything uploaded through your admin panel (logos, category images, product photos) is lost after the redeploy. Your database (MySQL) is safe. Only the uploaded files disappear.

The fix: store your files in Cloudflare R2 instead of inside the app. Then you can redeploy as often as you like and your images stay. This guide is for Laravel apps and takes about 15 minutes.

R2 is free for up to 10 GB of storage per month, and Cloudflare does not charge for serving (downloading) the files.

Step 1: Turn on R2 in Cloudflare

  1. Go to dash.cloudflare.com and log in. If you don't have an account, create a free one.
  2. In the left menu, click R2 Object Storage.
  3. The first time you turn on R2, Cloudflare asks for a payment card. You won't be charged as long as you stay within the free limit.

Step 2: Create a bucket

A bucket is the folder your files will live in.

  1. Click Create bucket.
  2. Enter a Bucket name using lowercase letters and -, for example myshop-uploads.
  3. Leave Location set to Automatic.
  4. Click Create bucket.

Write down the bucket name. This is your AWS_BUCKET.

Step 3: Turn on a public URL for your images

To show images on your website, the bucket needs a public link.

  1. Open your bucket and go to the Settings tab.
  2. Under Public Development URL (r2.dev), click Enable. If asked to confirm, type allow.
  3. You'll see a link like this:
https://pub-1a2b3c4d5e6f.r2.dev

Write down this link. This is your AWS_URL.

Tip for live sites: the r2.dev link is fine for testing, but it is rate-limited. If your domain is on Cloudflare, go to Custom Domains → Connect Domain on the same Settings page, add something like cdn.yourdomain.com, and use that as your AWS_URL instead.

Step 4: Create an API key

Your app uses this key to upload files to R2.

  1. Go back to the main R2 Object Storage page.
  2. On the right, click API → Manage API tokens.
  3. Click Create API token.
  4. Fill in the form:
    • Token name: for example myshop-laravel
    • Permissions: Object Read & Write
    • Specify bucket(s): choose Apply to specific buckets only and select the bucket from Step 2
  5. Click Create API Token.
  6. The next page shows three values. Copy all three now, because the secret is shown only once.
Shown in Cloudflare asVariable name in FlyNode
Access Key IDAWS_ACCESS_KEY_ID
Secret Access KeyAWS_SECRET_ACCESS_KEY
S3 endpoint (for example https://abc123.r2.cloudflarestorage.com)AWS_ENDPOINT

If the endpoint ends with your bucket name (.../myshop-uploads), remove that part. Keep only https://<id>.r2.cloudflarestorage.com.

Step 5: Add the S3 package to Laravel

On your computer, open your project folder and run:

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

Then push to GitHub:

git add composer.json composer.lock
git commit -m "Add S3 storage driver for R2"
git push

Step 6: Add the variables in FlyNode

  1. Open your app in the FlyNode dashboard and go to the Variables tab.
  2. Click Paste a .env, paste the block below with your own values, and click Add these. You can also add them one by one with Add variable.
  3. Click Save and redeploy. The new values take effect when the deploy finishes, which usually takes a few minutes.
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_DEFAULT_REGION=auto
AWS_BUCKET=your-bucket-name
AWS_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
AWS_URL=https://pub-your-id.r2.dev
AWS_USE_PATH_STYLE_ENDPOINT=true
VariableValue (example)Where to get it
FILESYSTEM_DISKs3Type exactly this
AWS_ACCESS_KEY_IDa1b2c3d4e5...Step 4
AWS_SECRET_ACCESS_KEYf6g7h8i9j0...Step 4
AWS_DEFAULT_REGIONautoType exactly this
AWS_BUCKETmyshop-uploadsStep 2
AWS_ENDPOINThttps://abc123.r2.cloudflarestorage.comStep 4
AWS_URLhttps://pub-1a2b3c4d5e6f.r2.devStep 3
AWS_USE_PATH_STYLE_ENDPOINTtrueType exactly this

Click Save and redeploy once and wait for that deploy to finish. Don't start a second deploy on top of it. Don't put spaces around the values.

Step 7: Check your code

Look at how your code saves images, for example in the store and update functions of your CategoryController.

Case A: no changes needed

If your code saves images like this:

$path = $request->file('image')->store('categories');

and shows them like this:

<img src="{{ Storage::url($category->image) }}">

then you're done. Because you set FILESYSTEM_DISK=s3, this code now uses R2 automatically.

Case B: your code says 'public', so change one word

Before:

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

After:

$path = $request->file('image')->store('categories', 's3');

Where the image is shown, if you have asset('storage/'.$category->image), change it to:

Storage::disk('s3')->url($category->image)

Case C: your code uses move(public_path(...)), so change two places

Saving, before:

$name = time().'.'.$request->image->extension();
$request->image->move(public_path('uploads/categories'), $name);
$category->image = $name;

Saving, after:

$category->image = $request->file('image')->store('uploads/categories', 's3');

Showing the image, before:

<img src="{{ asset('uploads/categories/'.$category->image) }}">

Showing the image, after:

<img src="{{ Storage::disk('s3')->url($category->image) }}">

Avoid these in every case

  • Don't use storePublicly(). Use plain store().
  • If the s3 disk in config/filesystems.php has 'visibility' => 'public', remove that line.

R2 doesn't support this "public" setting, and uploads fail when it is there. Your images are already public through the URL from Step 3.

If you changed any code, push again. FlyNode deploys the new code when you push.

Step 8: Test it

  1. Upload a new image to a category from your admin panel.
  2. Right-click the image and choose Copy image address. If the link starts with https://pub-....r2.dev/... (or your custom domain), the setup works.
  3. In Cloudflare, go to R2 → your bucket → Objects and check that the file is there.
  4. Final check: redeploy once from FlyNode. When the deploy finishes, make sure the image is still there.

Old images: anything uploaded before this setup was already lost. Upload those images again.

Troubleshooting

What you seeCause and fix
500 error when uploadingCheck the variables for typos, especially AWS_ENDPOINT and the keys. Remove any spaces around the values.
Class "League\Flysystem\AwsS3V3\..." not foundStep 5 was skipped, or composer.lock wasn't pushed.
x-amz-acl ... not implementedYour code uses storePublicly() or 'visibility' => 'public'. See the end of Step 7.
Access Denied / 403 when uploadingMake sure the API token has Object Read & Write and the correct bucket selected. Create a new token if needed.
Upload works, but the image is brokenMake sure the public URL from Step 3 is turned on and AWS_URL is correct. Open the image link in your browser to check.
Image links still start with /storage/...Your code still uses the public disk or asset(). See Step 7.
I changed a variable but nothing happenedMake sure you clicked Save and redeploy (not Save only) and that the deploy has finished.

Security

  • Never push AWS_SECRET_ACCESS_KEY to GitHub. Keep it only in FlyNode's Variables tab.
  • If your key leaks, go to Cloudflare → R2 → Manage API tokens, delete the old token, create a new one, and update the values in FlyNode.

Moving a Laravel app to FlyNode for the first time? Start with Migrate a Laravel App and Its MySQL Database to FlyNode.

Still stuck?

Open a support ticket from your dashboard and include the container name.

Contact support