Public Laravel files on cdn.yourdomain, not on a bucket URL

Brewless now puts every bucket for files behind bunny.net on cdn.<your domain>, and sets AWS_URL once it answers, so Storage::url() stops naming the bucket.

In this post
  1. Why does Laravel link public files to the bucket?
  2. What the files address is
  3. Only public files, and the bucket stays private
  4. DNS and the certificate
  5. AWS_URL only once it answers
  6. Another name, or none
  7. Summary

Store a file as public on Laravel's s3 disk and ask for its URL, and you get the address of the bucket: https://shop-production-files.s3.nl-ams.scw.cloud/avatars/42.jpg. It works, but it names your storage provider and your bucket in every link, it skips the edge, and moving the files later means changing every link that was ever shared.

From today Brewless gives every bucket for files an address on your own domain: cdn.shop.example.eu, served by bunny.net. This post explains what that address is, what it serves and what it leaves alone.

Because the s3 disk has no other address to use. Storage::disk('s3')->url($path) uses the disk's url when there is one, which config/filesystems.php reads from AWS_URL, and otherwise builds one from the bucket and the endpoint. Until now Brewless set AWS_BUCKET and AWS_ENDPOINT but no AWS_URL, so every public file got a bucket URL.

We noticed it on our own sites: preview images made by spatie/laravel-og-image are stored on the s3 disk, and every share preview pointed at a ….s3.nl-ams.scw.cloud address.

What the files address is

With bunny.net connected, every deploy now gives the bucket for files an address of its own at the edge:

APP_URL Files address
https://shop.example.eu cdn.shop.example.eu
https://example.com cdn.example.com
The test address, or no domain of your own cdn-<label>.brewless.dev

The <label> is the same one your environment's test address has: organisation, application and environment, shortened with a hash when it would not fit a DNS label.

Behind the name is a pull zone of its own, next to the one in front of your app, named after it with -files. Its origin is the public endpoint of your bucket. Like the zone in front of your app, it routes through the EU only, takes TLS 1.2 and newer, has Origin Shield in front of the bucket and keeps its logs anonymised.

Note

A file is kept at the edge as long as the Cache-Control the bucket sends with it says, and for a day when it sends none. Give a file that changes a new path rather than overwriting it, as hashed file names do.

Only public files, and the bucket stays private

The pull zone asks the bucket without a key. That decides what it can serve:

Stored as At the bucket URL At the files address
public visibility Served Served, from the edge
Private (the default) 403 403

So the files address adds no new way in: what answers there is what already had a public link. Invoices, contracts and other private uploads stay behind temporary URLs or your own route, as described in Laravel uploads on Scaleway Object Storage. The bucket itself stays private, without a bucket policy that opens it.

DNS and the certificate

Brewless points the name where your DNS is:

  • Bunny DNS: a pull zone record for cdn.
  • Scaleway DNS: a CNAME to the pull zone.
  • No domain of your own: a CNAME in Brewless's own zone under brewless.dev.
  • DNS kept elsewhere: nothing is touched. The deploy names the record to make, such as cdn.shop.example.eu CNAME <pull zone>.b-cdn.net.

A record that already stands on the name is never changed. bunny.net issues the certificate once the name points at the pull zone. Waiting for DNS or a certificate never fails a deploy; the next deploy simply looks again.

AWS_URL only once it answers

A link to an address that does not answer yet would break every image on your site, so Brewless waits. Once the files address answers with its certificate, a deploy gives the containers:

AWS_URL=https://cdn.shop.example.eu

From then on Storage::url(), and every package that builds links through the disk, names the files address. The first deploy points the name, which takes a moment to spread, so it still links to the bucket; the next deploy asks for the certificate before it hands out the variables and usually switches. Until then the bucket URL keeps working, and existing links to it keep working afterwards too.

Tip

AWS_URL is a platform variable, given at deploy. An AWS_URL of your own always wins, and Brewless never writes it into your Secret Manager. See Variables.

Another name, or none

cdn under storage in brewless.yml chooses:

brewless.yml
environments:
  production:
    storage:
      name: shop-production-files
      cdn: files.example.eu   # or false for no files address

The short form storage: shop-production-files keeps the default. A name under brewless.dev is refused. With cdn: false the containers no longer get AWS_URL; a pull zone and a record made earlier stay as they are until you remove them, because Brewless removes nothing of yours by itself. See The files address.

Summary

  1. Every environment with a bucket for files and bunny.net in front gets cdn.<your domain> on its next deploy.
  2. Only files stored as public are served there; the bucket stays private.
  3. Brewless writes the DNS record at Bunny DNS or Scaleway DNS, and names it when your DNS is elsewhere.
  4. AWS_URL follows once the address answers, and yours always wins.
  5. storage.cdn picks another hostname, or false for none.

Frequently asked questions

Why does Storage::url() return a bucket URL?

Laravel's s3 disk builds the URL from the bucket and the endpoint unless the disk has a url, which config/filesystems.php reads from AWS_URL. Without AWS_URL a public file gets the address of the bucket at your storage provider.

What is the files address in Brewless?

It is cdn. plus the host of your APP_URL, such as cdn.shop.example.eu, served by a bunny.net pull zone of its own in front of the environment's bucket for files. Every deploy sets it up, and once it answers with a certificate the containers get AWS_URL pointing at it.

Does the files address make private uploads public?

No. The pull zone asks the bucket without a key, so a file stored as private still answers 403 there. Only files your app stores with public visibility are served, which are the files that already had a public bucket URL.

Do I have to change my DNS?

Not when your domain is at Bunny DNS or Scaleway DNS, because Brewless writes the record there. When your DNS is kept elsewhere, the deploy names the CNAME to make and changes nothing itself.

Can I keep my own AWS_URL or switch the files address off?

Yes. An AWS_URL of your own always wins and Brewless never writes AWS_URL into your Secret Manager. storage.cdn set to false makes no files address, and storage.cdn set to a hostname uses that name instead of cdn. plus your domain.

All posts Read this post as markdown

Try it on your own app

Brewless deploys your Laravel app to Scaleway and bunny.net, in your own accounts. Start free with one application, production included.