Skip to content

Host a Laravel app on fly.io

Posted on:2 octobre 2023 at 02:00

We will see in this article how to host a real Laravel application inexpensively at fly.io.
By “real application”, I mean, a one with a database, jobs / CRON and a way to store uploaded files in volumes. All of that for very cheap or even free in some cases. Stay tuned!

First of all, what is fly.io?

I quote their site Fly.io transforms containers into micro-VMs. The description seems very explicit. You can scale 0 to unlimited, you don’t have to manage a server and the underlying operating system. Just do your app and ship it! In my opinion, it also have the advantage to be very competitive in term of pricing versus similar alternatives like Google App Engine for example. It is not a general rule, but I have my laravel website creaboutique.dezeiraud.com currently on Fly. And in one month, it cost me $0.00 with the use of the shared-cpu-1x machine. Because of a sort of “free tier” of 3 free shared-cpu-1x VM. Not a lot of power, but sufficient for a small website. Another advantage, the machine can scale to 0 if no traffic inbound after a certain time. So you can save cost & be more ecological. The downside is that the user who wakes up the VM will have a slightly longer loading time than normal, around 300ms.

Cool but, where is my database?

You may have noticed, but there is no database at Fly. We need to look at some managed alternatives (or not, but I don’t cover unmanaged and self-hosted database on a VPS). And good news, we have a plenty out there. Even with good free tier.

Of course, you can choose what provider you want. Azure, AWS, DigitalOcean. It doesn’t matter, you are not constrained by fly. Only endpoint accessible via https from the internet is necessary. To begin with, I will only tell you about 2 that I tested, one without success and the other that I am currently using in free tier for creaboutique.

We will see in a later chapter how to configure the Supabase database and connect to it.

And where I save my files uploaded by my Laravel app?

Good question! If you want of course, you can upload them to AWS S3 or similar / compatible services. It is up to you.
But what if you don’t want to upload same to an external service? Because Fly.io serve your app in a docker container, we can’t just upload files in local storage. Because after an update, data well be losed. And anyway, it is readonly. But, like any Docker image, you can create Volumes. And this is exactly what we are going to do in our setup. Of course, you need to pay for the volume, depending the number of storage you need.

Let’s go

First step, create your account on fly.io. Depending your Operating System, follow this step to install the Fly CLI. Then, login yourself in the CLI following this second step. It is very easy so I don’t waste words in this article to guide you.

Now go to the root folder of your laravel app and run:

fly launch

When asked if you want to deploy now, say No. Fly should have already detected that your application uses Laravel and configured accordingly.
If you have other environment variables to set, you can edit the fly.toml file and add them. Also replace APP_URL with the URL your app will be served on (by default, “https://.fly.dev”).

For example:

# fly.toml app configuration file generated for creaboutique on 2023-09-20T17:27:40+02:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = "MyLaravelApp"
primary_region = "cdg"
swap_size_mb = 512
console_command = "php /var/www/html/artisan tinker"

[build]
  [build.args]
    NODE_VERSION = "18"
    PHP_VERSION = "8.1"

[deploy]
  release_command = "php /var/www/html/artisan migrate --force"

[env]
  APP_ENV = "production"
  APP_NAME = "MyLaravelApp"
  APP_URL = "MyLaravelApp.dezeiraud.com"
  DB_CONNECTION = "pgsql"
  DB_HOST = "db.xxxxxxxxxxxxxx.supabase.co"
  DB_DATABASE = "postgres"
  DB_PORT = "5432"
  LOG_CHANNEL = "stderr"
  LOG_LEVEL = "info"
  MAIL_ENCRYPTION = "tls"
  MAIL_FROM_ADDRESS = "[email protected]"
  MAIL_FROM_NAME = "MyLaravelApp"
  MAIL_HOST = "smtp.gmail.com"
  MAIL_MAILER = "smtp"
  MAIL_PORT = "587"
  MAIL_USERNAME = "[email protected]"
  SESSION_DRIVER = "cookie"
  SESSION_SECURE_COOKIE = "true"

[processes]
  app = ""

[[mounts]]
  source = "storage_dir"
  destination = "/var/www/html/storage"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ["app"]

You can copy this configuration, obviously changing with your values. Also, this configuration automatically run the Laravel migration after each deploy. You can also see in [[mount]] the configuration of a volume.

Like you can see, we don’t set directly secrets in the [env] section. For that, we have a fly command fly secrets set. For example:

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

Database

The next step is to setup your Supabase postgres database in the environement. For that, register and login to your Supabase dashboard, create a new project. Then, Project settings > Database. You can find there Connection info needed for your Laravel app. Copy / Past this values in the appropriate env config and DB_USERNAME, DB_PASSWORD with fly command fly secrets set. Scroll down in the supabase database settings and check Enforce SSL on incoming connections. It is very important for security. And voila! It is okay for the database.

Now, we need to setup workers to run Laravel queue. This step is optionnal, depending if your Laravel app used queue or not. From your project root folder, you can see a .fly folder. Open it and create a new file worker.conf next to entrypoint.sh with this content:

[program:worker]
priority=5
autostart=true
autorestart=true
stdout_events_enabled=true
stderr_events_enabled=true
command=php /var/www/html/artisan queue:listen
user=www-data
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Storage

For the storage, it is a bit more complicated. First, in .fly/scripts/caches.sh add storage:link line at the bottom:

#!/usr/bin/env bash

/usr/bin/php /var/www/html/artisan config:cache --no-ansi -q
/usr/bin/php /var/www/html/artisan route:cache --no-ansi -q
/usr/bin/php /var/www/html/artisan view:cache --no-ansi -q

/usr/bin/php /var/www/html/artisan storage:link --no-ansi -q

Then, next to .fly/scripts/caches.sh, create a new file called 00-volumes.sh with this content:

if [ ! -d "/var/www/html/storage/app" ]; then
    if [ ! -d "/var/www/html/storage_" ]; then
        echo "could not find storage_ dir to copy to volume"
        exit 1
    else
        cp -r /var/www/html/storage_/. /var/www/html/storage
        rm -rf /var/www/html/storage_
    fi
fi

This script is important for the first deployment. It allow us to paste the storage folders in the volume. Like you can see in it, you need to create a copy of your storage folder at the root of your laravel project called storage_. Next to the .env folder for example. It is necessary because if not, the storage folder will be override by the virtual storage of the docker container and your app crash.

You also need to create the volume, for that run fly volumes create storage_dir --region ams --size 1 (remember to change the region if needed and the size in Go).

fly deploy

Now it’s time to run `fly deploy“. This command may take some time because it creates the Docker image for your project. Some informations can be prompted the first time during this step. Be warned. And after some times, the first deployement is the longest, your app is accessible on internet in a subdomain of fly! You can monitore everything from your fly.io dashboard.

By default, the shared-cpu-1x is not used, but a more expensive one, you can check that with fly scale show. To change that, very simple, run fly scale vm shared-cpu-1x --memory 256 and deploy again.

Next step, but it doesn’t concern the deployment of an Laravel app only on fly.io, is the setup of a custom domain, you have multiple way to do it, check this documentation.