Full Stack Laravel

Fly runs apps close to users by taking Docker images and transmogrifying them into Firecracker micro-vms, running on our hardware around the world. If you want to ship a Laravel app, try it out on Fly.io. It takes just a couple of minutes.

We have docs on using fly launch to get you up and running quickly with Laravel, but let's talk about all the other stuff you probably want - Redis, MySQL, cron, and queues!

We're gonna spin up a new Laravel installation, add some user authentication (backed by MySQL), setup Laravel's scheduler, and then see how to use Redis for queues, session storage, and caching.

The Laravel Application

The first thing we need is a Laravel application. We can use fly launch to get one running really quickly.

cd ~/Fly

# 1. Create a shiny, new Laravel app
composer create-project laravel/laravel laravel-fly
cd laravel-fly


# 2. Launch on Fly!
fly launch
# Set app name, select region, deploy now


# 3. View the site in your browser:
fly open

Redis

Rather than trying to run everything in one VM on Fly, extra "services" are best run as separate VMs.

Following along in the Fly Redis docs, we can create a small Redis service for ourselves.

# A new, empty directory
mkdir ~/Fly/redis \
    && cd ~/Fly/redis


# Create a new redis app (don't deploy it yet!)
fly launch --image flyio/redis:6.2.6 --no-deploy --name lara-redis


# Add a persistent volume so Redis data sticks around
# Note: Volumes only live within the region they are created
fly volumes create redis_server --size 1


# Update fly.toml config to use the volume
cat >> fly.toml <<TOML
  [[mounts]]
    destination = "/data"
    source = "redis_server"
TOML


# Add a password (before launching!)
fly secrets set REDIS_PASSWORD=somepassword

A few things to note!

  1. We created a Volume. In conjunction with a tweak to fly.toml, using persistent storage makes sure data isn't lost when the app is re-deployed (however, volumes are specific to a single region).
  2. We gave Redis a password (by setting a secret), we didn't leave it open for just any old connection. This is required.
  3. As documented, don't forget to edit the generated fly.toml file to delete everything under [[services]] (but keep the [[services]] heading).

After that, we're ready to launch it.

fly launch

Private Networking

We'll want our application to talk to Redis. Luckily, private networks are a thing in Fly.

We named our Redis service lara-redis, which means other services (within our Fly organization) can reach the Redis service using hostname lara-redis.internal.

Laravel Config

We need to update the Laravel configuration to use Redis. Laravel needs to know the hostname and password of our Redis service.

Passwords are secrets, so we'll set it as such in our app service.

cd ~/Fly/laravel-fly

# Add in our redis password secret to this app as well
fly secrets set REDIS_PASSWORD=somepassword

Adding a secret triggers a quick re-deploy. Neat!

The secret is set, but we also need additional (not-so-secret) configuration to tell Laravel where and how use Redis. Update the fly.toml file:

[env]
  APP_ENV = "production"
  CACHE_DRIVER = "redis"
  SESSION_DRIVER = "redis"
  REDIS_HOST = "lara-redis.internal"

Let's test that this works! If we edit file routes/web.php, we can add a route that uses Redis:

Route::get('/test', function() {
    Cache::put('new-key', Str::random(16));

    return "The new cached string is " . ~Cache::get('new-key');
});

We updated some files, we need to deploy again to get that change in place:

fly deploy

You should be able to head to your-app.fly.dev/test to see the result. If you don't get any errors, then everything is working!

MySQL

Fly offers to set up an HA cluster of PostgreSQL for you.

There's no need to brag about it, but we know that MySQL is pretty great. For that, we can use PlanetScale in our Fly apps.

That link shows how to sign up and create a MySQL database on PlanetScale. Once created, we just need to add the proper configuration into our application.

You can just run MySQL as another Fly app as well - I'll be writing about that soon.

Edit fly.toml and update the environment variables:

[env]
  APP_ENV = "production"
  CACHE_DRIVER = "redis"
  SESSION_DRIVER = "redis"
  REDIS_HOST = "lara-redis.internal"

  # New stuff here
  DB_CONNECTION = "mysql"
  DB_HOST = "some_hostname.us-east-2.psdb.cloud"
  DB_DATABASE= "laravel-fly"
  MYSQL_ATTR_SSL_CA = "/etc/ssl/cert.pem"

Note the addition of MYSQL_ATTR_SSL_CA=/etc/ssl/cert.pem, which is required for connecting to databases on PlanetScale (Using TLS for remote database connections is extremely important)!

The value /etc/ssl/cert.pem is correct for the container Fly setup for you via fly launch. See here for other possible values.

Laravel needs to know the username/password for the database as well. Those seem pretty secret, so let's use secrets!

I opted to keep the hostname an environment variable, but you might want to set it as a secret.

fly secrets set DB_USERNAME=your-username DB_PASSWORD=your-password

Authentication With Laravel Breeze

Many Laravel apps will have user authentication. Let's use Laravel Breeze to quickly scaffold auth into our app.

cd ~/Fly/laravel-fly

composer require laravel/breeze --dev

php artisan breeze:install

# If you're running your app locally
# you'll want to get static assets/run migrations
npm install
npm run dev

php artisan migrate

Check that it works locally if you'd like, then deploy it!

fly deploy

Did you know you can ssh into your apps? We'll use that to run our migrations:

fly ssh console -C "php /var/www/html/artisan migrate --force"
Connecting to top1.nearest.of.black-rain-2336.internal... complete
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (216.52ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (199.27ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (197.86ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (310.91ms)

Migrate on Deploy

If you're wondering how to automate the running of your migrations, you're in luck! That's configurable in the deploy section of the fly.toml file.

The following will run your migrations just before making the latest version of the application available.

# File: fly.toml
[deploy]
  release_command = "php /var/www/html/artisan migrate --force"

You should now be able to register and log in.

It works, we've got a full(ish) Laravel app running!

Cron and Queues

The fly launch command gave us some boilerplate to easily use Laravel's scheduler (cron) and queue workers.

To enable those, we can update the generated docker/supervisor.conf file. In order to run the scheduler (artisan schedule:run) and a queue worker (artisan queue:work), uncomment and tweak the following:

[program:laravel-schedule]
numprocs=1
autostart=true
autorestart=true
redirect_stderr=true
process_name=%(program_name)s_%(process_num)02d
command=/usr/sbin/crond -f -l 8
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:laravel-queue]
user=app
numprocs=1
autostart=true
autorestart=true
redirect_stderr=true
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --backoff=3 --max-time=3600
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Note that I adjusted the queue:work command to use the default connection (Redis, in our case). This necessitates an update to our fly.toml file so redis becomes the QUEUE_CONNECTION we want:

[env]
  APP_ENV = "production"
  CACHE_DRIVER = "redis"
  SESSION_DRIVER = "redis"
  REDIS_HOST = "lara-redis.internal"
  DB_CONNECTION = "mysql"
  DB_HOST = "17ip2dquu9ud.us-east-2.psdb.cloud"
  DB_DATABASE= "laravel-fly"
  MYSQL_ATTR_SSL_CA = "/etc/ssl/cert.pem"

  # New thing here:
  QUEUE_CONNECTION = "redis"

Don't forget to run fly deploy after making changes.

Poke the Bear

Let's see what's going on in our VM. We can use the logs command to see what's going on behind the scenes, or the ssh command to poke around!

cd ~/Fly/laravel-fly

# See some logs!
fly logs

# SSH and interact with the VM!
fly ssh console

# We can see that our php-fpm, queue, and cron daemons are running:
> ps aux | grep [f]pm
> ps aux | grep [q]ueue
> ps aux | grep [c]ron

What We Did

We have a Laravel application running with all the usual things:

  1. MySQL
  2. Redis
  3. cron (for the scheduler)
  4. Queues

That covers the most popular use cases for Laravel, but there's lots more you can do as well! My favorite part of this is how easy it is to setup extra services in Fly (Redis in our case). The private networking makes it super easy.

Fly is working on making database integrations easier as well. PlanetScale is super cool, and even supports multi-region read replicas (which works well with scaling Fly apps)!