Taking Laravel Global

Image by Annie Ruygt

Fly.io empowers you to run apps globally, close to your users. If you want to ship a Laravel app, try it out on Fly.io. It takes just a couple of minutes.

Fly.io puts your application close to your users by making global deployments easy. It’s literally just a few commands.

Here’s an example of showing an app, currently in DFW (Dallas), scaled out to also include Frankfurt and Singapore:

# Add 2 additional regions
fly regions add fra sin

# Scale VM count to fill your regions
fly scale count 3

Two commands! User requests will be routed to the nearest application instance.

That’s the easy part. The hard part is making sure your application behaves in it’s new, globally available setup. Let’s explore what you need to know.

Where’s the data?

I’m not burying the lede here. This TL;DR of this article is asking yourself “Where’s my data?” and coming up with an answer.

This is similar to good-old-fashion load balancing, but with a twist. With a load balancer, you need to figure out how every application instance sees the same data. In a global environment, we also need to figure out how to do that quickly.

These are solved problems, but the solutions involve trade-offs! Grab a cup of coffee while we explore the problems and solutions to global infrastructure.

Deploy Globally on Fly

Fly.io makes running full-stack applications easier than ever before. Give it a whirl!

Deploy your Laravel app!

Problem: File Storage

We’ll start with the lowly file. If your application accepts user-uploaded files, you have to decide where to put them.

By default, Laravel saves files to the local disk. If your application is served from multiple application instances, this is problematic.

If a user uploads a file into Dallas, that file file won’t exist in Singapore.

Fly.io provides volumes for persistent storage, however volumes are pinned to a region. They don’t replicate around the world. Maybe someday!

The answer here is to upload files to a central storage location - typically S3 or some compatible storage service.

The least-annoying way to accomplish this is using Laravel’s File Storage, which allows you to set a driver to some non-local place like S3, DO Spaces, CloudFlare R2, or what-have-you.

Problem: Session Storage

Sessions are usually stored as files on the local disk. Sounds familiar, right? We just talked about file storage!

Similar to storing files, we need to put our session data were every application instance can see it.

Redis is a very popular place to store sessions.

Unfortunately, replicating Redis across multiple, global regions is slow enough to be noticeable. Sessions are stored and retrieved on just about every web request, so replication lag could be killer.

The simplest solution here is to use Cookie-based session storage. This is just a configuration change in Laravel - setting SESSION_DRIVER=cookie in your .env file.

Cookie-based sessions store data in a cookie. Cookies are stored in web browsers, instead of somewhere in your infrastructure. The user’s browser will always send the cookie alongside web requests, and thus the session data is accessible by your Laravel application.

It’s elegant in its simplicity - you pushed the storage problem off to your users, and they don’t have to know nor care about that implementation detail. That’s the best kind of solution!

For security, I recommend the following settings:

# Enable cookie-based sessions

# Only let them work when https:// is used

Additionally, you may want to update config/sessions.php in order to set encrypt => true, ensuring the cookie session data is encrypted. There’s a few more settings related to security available in config/session.php. Be sure to review them.

Here’s the main trade-off with cookie-based sessions. Cookies can only store a limited amount of data. That limit is roughly 4096 bytes (browser-dependent). If you use session storage to store a bunch of data - maybe don’t! Or investigate other means of saving data between requests.

Session storage in Laravel is typically used for things like flash messages, and validation error messages.

Maybe Use File-Based Sessions Anyway?

Fly.io’s global network uses BGP to send user requests to the nearest “edge” location. Fly.io knows what application instance is closest to that edge location, and routes requests to it.

Assuming your users aren’t zooming around the world, it’s a safe bet that any given user will likely be sending requests to the same region every time. With that assumption, we could actually use file-based sessions!

The trade-off is the odd case where a user get routed to another region for some reason. They’d no longer be logged in! This is possible if an application instance in a region becomes unhealthy or if there are some global BGP-related issues (it is DNS, after all).

Multiple App Instances in One Region

If you decide that users will always be served from one region anyway, you may still end up with a problem.

What if you have multiple application instances in one region? Fly.io supports that use case - it will load-balance requests between all application instances in a region.

This means each application instance within a given region would need access to a central session store.

In that case, you might want to setup a single Redis instance per region to store sessions. You’d have to do a bit of extra work in your Laravel configuration to ensure each application instance connected to the correct Redis host .

Problem: Cache Storage

Caching is an issue similar to session storage. You may first reach for file-based caching, but then realize that you end up with a cache per application instance. Each application instance’s cache can get out of sync easily.

Like with Sessions, you might get away with an application-specific cache.

However you need to be careful about data “correctness”. If your cache has different data in each region, is that an issue? I can’t tell you - that depends on your application!

One thing I can say is that caching is one of those things where “eventual consistency” usually is okay. That means using a distributed Redis setup might work!

In fact, you may be able to have your cake and eat it too. We wrote up a Redis-backed caching strategy that could let you have both global and regional cache storage using one Redis cluster!

Don’t sleep on that article.

Problem: Proxy-awareness

Applications in Fly.io are served from behind a proxy. This proxy routes requests to the nearest application instance from your users.

Because a proxy is involved, your app needs to be aware of the X-Forwarded-* headers.

If you use the fly launch command to initialize your Laravel application, Fly.io takes care of that.

Within Laravel, the X-Forwarded-* headers effect your application’s ability to:

  1. See correct client information (e.g. getting the correct IP address)
  2. Generate URL’s correctly (e.g. ensuring https:// is used)

Your app needs to be told to trust Fly.io’s proxy, which can be done in the TrustProxies middleware. You can update your app/Http/Middleware/TrustProxies.php file and set protected $proxies = '*';.

Pro-tip: You can see what headers the Fly-Proxy is sending your application using https://debug.fly.dev/

Problem: The Database

The phrase “mission critical” is often found just a few words away from “database”. We need our data, and we like our ACID!

Flying in the face of data consistency (and speedy queries) is running an application globally. Having the same data (with any consistency) in multiple regions around the world is a tough nut to crack.

We don’t want an app server in Singapore having to talk to a database server in Dallas. The speed of light and the number of network hops required make that agonizingly slow for the typical full-stack application.

So, here’s a solution that threads the needle between “easy to implement” and “reasonably fast”:

  1. Have a primary database. It’s the only place that accepts write queries
  2. Place read-replicas in multiple global regions - preferably each one is close to (if not within) each region hosting your application
  3. Setup Laravel’s built-in feature to route write and read queries to the proper database instances
  4. Use the Fly-Replay header to route traffic that result in write queries to the primary region

How are you expected to do all of that!? Here’s how to run MySQL globally on Fly.io with PlanetScale.

That article covers the complete setup, and goes into details about how to handle sending write queries across the globe in an efficient manner.

Oh, is that all?

That’s a lot of stuff to care about! Full-stack frameworks need to store data for a bunch of use cases - your applications data, session storage, caching (if you use it), and more.

This all presents some new challenges in a global environment. We not only need every app instance to see the same data, but access it quickly as well. Fly.io makes this uniquely easy, but we still need to know how this affects our application configuration.