<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq" -->

---
title: Laravel S3 Image Proxy: Clean URLs with CDN Caching
description: A walkthrough of replacing raw S3 object storage URLs on a Laravel blog with clean, proxied paths served through the application. Covers building a centralized...
canonical: https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Laravel S3 Image Proxy: Clean URLs with CDN Caching | daily.dev
og:description: A walkthrough of replacing raw S3 object storage URLs on a Laravel blog with clean, proxied paths served through the application. Covers building a centralized...
og:url: https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq
og:image: https://api.daily.dev/og/posts/g2PUalDwq.png
og:image:alt: Laravel S3 Image Proxy: Clean URLs with CDN Caching
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel S3 Image Proxy: Clean URLs with CDN Caching

**[Daniel Petrica](https://daily.dev/sources/danielpetrica)** · 4 min read · 0 upvotes · 0 comments

## Summary

A walkthrough of replacing raw S3 object storage URLs on a Laravel blog with clean, proxied paths served through the application. Covers building a centralized URL helper (MediaUrlBusiness), a proxy controller that streams files from S3 using readStream() instead of buffering the whole file into memory, setting a 24-hour Cache-Control header for Cloudflare and browser caching, and adjusting route/middleware setup so the custom cache header isn't overridden. Also flags a security caveat: anything in the private bucket becomes reachable through the proxy if the path is guessed, so that bucket should only hold public-facing assets.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://danielpetrica.com/posts/how-to-replace-raw-s3-urls-with-a-laravel-image-proxy-and-keep-your-cdn-cache>

## Questions this post answers

### How do I serve images from a private S3 bucket through Laravel without exposing raw storage URLs?

Build a proxy controller that catches requests like /storage/{disk}/{path}, validates the disk against an allowed list, resolves the full S3 key, and streams the file using $disk->readStream() combined with response()->stream(), setting Content-Type, Content-Length, and a Cache-Control header manually. This avoids exposing the storage provider and bucket name in generated URLs.

_daily.dev surfaces practical Laravel storage patterns like this for developers hardening media pipelines._

### Why does Laravel's Storage::download() or streamDownload() use too much memory for large files?

Storage::download() and streamDownload() buffer the entire file into memory before sending it to the client, which wastes memory on large images or media files. Switching to $disk->readStream() paired with response()->stream() and fpassthru() streams the file directly from S3 to the client without buffering, avoiding the memory spike.

_Developers optimizing Laravel file delivery track streaming techniques like this on daily.dev._

### What Cache-Control header should I use to cache proxied S3 images at the CDN and browser level?

Set Cache-Control: public, max-age=86400, immutable on the streamed response so that both Cloudflare and browsers cache the image aggressively for 24 hours. Middleware that sets a default Cache-Control header should be adjusted to skip responses that already define their own header, so it doesn't override this tuned caching policy.

_Teams tuning CDN caching for Laravel media can follow patterns like this via daily.dev._

## Similar posts on daily.dev

- [Laravel S3 Image Proxy: Clean URLs with CDN Caching](https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-mxuxmu0hk) · Daniel Petrica · 7 upvotes · 1 comments
- [How We Cached Laravel News at the Edge with Fast Laravel](https://daily.dev/posts/how-we-cached-laravel-news-at-the-edge-with-fast-laravel-rbgw6huba) · Laravel News · 5 upvotes · 0 comments
- [A complete guide to HTTP caching](https://daily.dev/posts/a-complete-guide-to-http-caching-glkmld7xp) · Programming Digest · 97 upvotes · 3 comments
- [Speeding Up Laravel News With Cloudflare](https://daily.dev/posts/speeding-up-laravel-news-with-cloudflare-uyivtzspn) · Laravel News · 7 upvotes · 0 comments

---

Tags: [#php](https://daily.dev/tags/php), [#laravel](https://daily.dev/tags/laravel), [#aws-s3](https://daily.dev/tags/aws-s3)

[View this post on daily.dev](https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Laravel S3 Image Proxy: Clean URLs with CDN Caching","url":"https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq"},"datePublished":"2026-09-29T12:32:28.802Z","dateModified":"2026-09-29T12:33:06.526Z","description":"A walkthrough of replacing raw S3 object storage URLs on a Laravel blog with clean, proxied paths served through the application. Covers building a centralized...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/2890ded314dec073056675ce0ce58c98?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/2890ded314dec073056675ce0ce58c98?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Daniel Petrica","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Daniel Petrica","logo":"https://media.daily.dev/image/upload/s--jkfwlnJz--/f_auto,q_auto/v1774964393/logos/danielpetrica?_a=BAMAMiWQ0","url":"https://daily.dev/sources/danielpetrica"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"php,laravel,aws-s3","timeRequired":"PT4M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Daniel Petrica","item":"https://daily.dev/sources/danielpetrica"},{"@type":"ListItem","position":3,"name":"Laravel S3 Image Proxy: Clean URLs with CDN Caching"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/laravel-s3-image-proxy-clean-urls-with-cdn-caching-g2pualdwq#faq","mainEntity":[{"@type":"Question","name":"How do I serve images from a private S3 bucket through Laravel without exposing raw storage URLs?","acceptedAnswer":{"@type":"Answer","text":"Build a proxy controller that catches requests like /storage/{disk}/{path}, validates the disk against an allowed list, resolves the full S3 key, and streams the file using $disk->readStream() combined with response()->stream(), setting Content-Type, Content-Length, and a Cache-Control header manually. This avoids exposing the storage provider and bucket name in generated URLs. daily.dev surfaces practical Laravel storage patterns like this for developers hardening media pipelines."}},{"@type":"Question","name":"Why does Laravel's Storage::download() or streamDownload() use too much memory for large files?","acceptedAnswer":{"@type":"Answer","text":"Storage::download() and streamDownload() buffer the entire file into memory before sending it to the client, which wastes memory on large images or media files. Switching to $disk->readStream() paired with response()->stream() and fpassthru() streams the file directly from S3 to the client without buffering, avoiding the memory spike. Developers optimizing Laravel file delivery track streaming techniques like this on daily.dev."}},{"@type":"Question","name":"What Cache-Control header should I use to cache proxied S3 images at the CDN and browser level?","acceptedAnswer":{"@type":"Answer","text":"Set Cache-Control: public, max-age=86400, immutable on the streamed response so that both Cloudflare and browsers cache the image aggressively for 24 hours. Middleware that sets a default Cache-Control header should be adjusted to skip responses that already define their own header, so it doesn't override this tuned caching policy. Teams tuning CDN caching for Laravel media can follow patterns like this via daily.dev."}}]}
```

