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
- Go to dash.cloudflare.com and log in. If you don't have an account, create a free one.
- In the left menu, click R2 Object Storage.
- 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.
- Click Create bucket.
- Enter a Bucket name using lowercase letters and
-, for examplemyshop-uploads. - Leave Location set to Automatic.
- 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.
- Open your bucket and go to the Settings tab.
- Under Public Development URL (r2.dev), click Enable. If asked to confirm, type
allow. - 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 yourAWS_URLinstead.
Step 4: Create an API key
Your app uses this key to upload files to R2.
- Go back to the main R2 Object Storage page.
- On the right, click API → Manage API tokens.
- Click Create API token.
- 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
- Token name: for example
- Click Create API Token.
- The next page shows three values. Copy all three now, because the secret is shown only once.
| Shown in Cloudflare as | Variable name in FlyNode |
|---|---|
| Access Key ID | AWS_ACCESS_KEY_ID |
| Secret Access Key | AWS_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 onlyhttps://<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
- Open your app in the FlyNode dashboard and go to the Variables tab.
- 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.
- 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
| Variable | Value (example) | Where to get it |
|---|---|---|
FILESYSTEM_DISK | s3 | Type exactly this |
AWS_ACCESS_KEY_ID | a1b2c3d4e5... | Step 4 |
AWS_SECRET_ACCESS_KEY | f6g7h8i9j0... | Step 4 |
AWS_DEFAULT_REGION | auto | Type exactly this |
AWS_BUCKET | myshop-uploads | Step 2 |
AWS_ENDPOINT | https://abc123.r2.cloudflarestorage.com | Step 4 |
AWS_URL | https://pub-1a2b3c4d5e6f.r2.dev | Step 3 |
AWS_USE_PATH_STYLE_ENDPOINT | true | Type 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 plainstore(). - If the
s3disk inconfig/filesystems.phphas'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
- Upload a new image to a category from your admin panel.
- 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. - In Cloudflare, go to R2 → your bucket → Objects and check that the file is there.
- 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 see | Cause and fix |
|---|---|
| 500 error when uploading | Check the variables for typos, especially AWS_ENDPOINT and the keys. Remove any spaces around the values. |
Class "League\Flysystem\AwsS3V3\..." not found | Step 5 was skipped, or composer.lock wasn't pushed. |
x-amz-acl ... not implemented | Your code uses storePublicly() or 'visibility' => 'public'. See the end of Step 7. |
Access Denied / 403 when uploading | Make 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 broken | Make 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 happened | Make sure you clicked Save and redeploy (not Save only) and that the deploy has finished. |
Security
- Never push
AWS_SECRET_ACCESS_KEYto 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.