A minimal Laravel application for Fly.io.
You will need PHP 8+. You can check the version using php --version
. And composer.
- Clone this repo
- Duplicate
.env.example
naming it.env
- Run
composer install
to install its dependencies - Run
php artisan key:generate
to generate a new secret key - Run
php artisan serve
to run a local development server
You should be able to visit http://localhost:8000
and see the home page.
- Edit the provided
fly.toml
so it has your choice of app name and URL instead of these:
app = "fly-hello-laravel"
APP_URL = "https://fly-hello-laravel.fly.dev"
- Run
fly launch
. When it gets to the point asking if you want to deploy now, say No. Why? Because in production you need a secret APP_KEY. Without it the app will return an error like:
No application encryption key has been specified. "exception":"[object] (Illuminate\Encryption\MissingAppKeyException"
- Set that APP_KEY by running
fly secrets set APP_KEY=the-value-from-your-env-file
- Run
fly deploy
You should be able to visit https://your-app-name.fly.dev
and see the Laravel demo home page.
In this guide we'll learn how we packaged this Laravel application into an image ready to deploy to Fly's global application platform.
This is slightly more complicated than it is for other runtimes since PHP does not include a web server. We need to add one. Here we use nginx. And so we need to keep both it and PHP running. We do that using supervisor.
If you already have a Laravel application you would like to deploy, skip this step.
There are different approaches to creating a brand new Laravel application depending on your OS. Here we've used the laravel-installer approach. It assumes you have composer and PHP already installed:
composer global require laravel/installer
laravel new example-app
cd example-app
php artisan serve
You should be able to visit http://localhost:8000
and see the default Laravel home page.
Now that you have a Laravel application working locally, you need to add some files in order to run it on Fly. This method uses supervisor to keep nginx and PHP running. There are other ways to do this though. See: https://fly.io/docs/app-guides/multiple-processes/.
To make this approach work you need to add four things:
- a
Dockerfile
- a
.dockerignore
- a
/docker
folder which contains configuration files for PHP, nginx, and supervisor - a
fly.toml
that tells Fly what type of application you have (its port, protocol, and so on). Fly can generate this for you, however we can provide our own containing the environment variables we know that we'll need
You can just copy the files we've provided (adjusting each file depending upon your application's requirements). And then skip ahead to Add a fly.toml below.
But if you'd like to know why we made those changes, please continue:
The Dockerfile
tells Fly the dependencies that need to be installed in order for the application to run. If you take a look at the one we added, you can see each step is commented. You can adjust it as desired. For example you may not be using Laravel's mix for static assets. In which case you could remove the references to node and npm.
This example uses a basic base image in order to install only what we need and make clear what is going on at each step. To keep the size small it uses Alpine Linux rather than a much larger base OS, such as Ubuntu.
As mentioned above, some runtimes contain a web server. PHP does not. So we need nginx to proxy requests to it. But now we have multiple processes to keep running (php-fpm
and nginx
). To do that we use supervisor
. So we install that too.
If we simply added a Dockerfile
with no .dockerignore
file, it would include all the files in our application's folder. That would include ones that we certainly don't want being deployed (like .env
) and ones we don't need deployed (like the node_modules
folder). It makes clear the files and folders we do want included.
It is a personal preference to use a deny-unless-admitted approach for safety. If we add a file or folder in future, we can choose whether we want it deployed by adding it to the .dockerignore
.
This contains the various configuration files for PHP, nginx and supervisor to tell them what to do.
During the build they get moved to where the various processes expect them to be. For example in the Dockerfile
you will see we move the custom nginx configuration file to where nginx expects it to be by using this command RUN mv docker/nginx.conf /etc/nginx/nginx.conf
.
These configuration files will likely vary depending on the functionality your application needs and the size of Fly vm you choose. For example you could adjust the request timeouts or the amount of memory allocated to PHP.
What are the files in this folder?
The docker/supervisor.conf
file governs what processes supervisor
starts and/or keeps running. In this case we have a group of programs. At a minimum we need nginx
and php-fpm
to be running. You will see they are set to autorestart. That's important as we need them both to be running for the application to serve requests.
Their logs could be sent to a local file. However we don't really want to have to manually SSH in to every vm to see the log files. Far better they output to /dev/stdout
or /dev/stderr
as then we can access them by simply using the fly logs
command.
Since our application does not use queues, notifications or scheduling, those sections are commented out in our supervisor.conf
file. They are just there to give an indication of how they may work.
The docker/nginx.conf
provides a complete nginx configuration. This is based on the Alpine Linux nginx docs. You might want a simpler one, such as Laravel's example nginx conf. Note again how we change its default behaviour of logging to a local file to log to stdout/stderr:
#access_log /var/log/nginx/access.log main;
#error_log /var/log/nginx/error.log;
access_log /dev/stdout main;
error_log /dev/stdout;
The docker/php.ini
, docker/php-fpm.conf
and docker/app.conf
determine how PHP runs.
In docker/php.ini
we only included the options we actually want to override. For example we applied some common options recommended for security, such as expose_php = Off
.
The docker/php-fpm.conf
and docker/app.conf
are complete files taken from the original installation of PHP-FPM. Using a complete file lets us to see exactly what default values it uses. That's important because there are some that need to be changed to run a PHP application in a container.
Of those two files, the docker/php-fpm.conf
does not need to be changed much. Again, we simply change where its errors are output:
;error_log = log/php8/error.log
error_log = /dev/stderr
The docker/app.conf
includes the most changes. The default PHP-FPM installation calls this file www.conf
using a [www]
pool. We changed it to [app]
. So this replaces the www.conf
file you would otherwise see.
If you look through the file you can see the commented-out "OLD" values and their replacements. We've changed the user. We've changed the port to a socket. We also change the value of two important variables which are not obvious: catch_workers_output
and clear_env
:
catch_workers_output = yes
clear_env = no
What does this do? Again, it's for logging. If have used Laravel's logger helper logger()->debug('Test');
or facade Log::debug('Test');
when you run your app locally, you may log to a file (laravel.log
) or you may output to your terminal. Those log lines can be extrememly useful for debugging. If you left the default value of catch_workers_output = no
in this conf file, that output would not be caught. So you would never see it. Catching the output allows it to be piped to the master process, and on to Fly.
What does this do? This lets PHP-FPM access the environment variables provided by the system. If you leave this as its default yes you will find Laravel is not happy. It can't access all the environment variables you've either set within the fly.toml
file (we'll come on to that in a moment) or by using fly secrets
. And those environment variables are very important, controlling things such as the application environment and secret key.
If you have deployed any application to Fly before, you will recognise this. The fly.toml
file tells Fly about your application, such as what ports it should use, the protocol and health-checks. One is generated for you by the Fly CLI however you may prefer to have one already if you know the settings you need.
Our fly.toml
includes some standard settings, such as exposing port 443
and 80
to the outside world, and have our app listening on port 8080
. It has a http healthcheck on /
which (if all is well) will return a 200
status code. Notice also the [env]
section which contains environment variables. This contains any variable whose value is not secret.
If using our fly.toml
you will need to update the application name from fly-hello-laravel to your own app-name-here in two places:
app = "fly-hello-laravel"
APP_URL = "https://fly-hello-laravel.fly.dev"
Note: You should set APP_DEBUG
as false when running Laravel in production. But we don't set that here within the fly.toml
. Why? Currently it seems that that value is parsed into a string. This can be confirmed using gettype(env('APP_DEBUG'))
within Laravel. It returns string. And the string "false" is truthy. And so debgging remains on. In Laravel's config/app.php
the default value of APP_DEBUG should be false and so it is left as false unless we set it as true.
We also haven't made use of the [[statics]]
option to offload serving static assets to Fly. That is because we are using nginx which is already a very efficient way to serve static files. However you could add that if you prefer.
Due to the way Laravel caches files, we don't use the option to cache configuration. We want to still have access to environment variables set at runtime.
You should also make sure you do not deploy with cached routes. Why? If a different APP_KEY was used (as it will likely be) you will get a Laravel error complaining that:
Your serialized closure might have been modified or it's unsafe to be unserialized
... and your application won't run. We delete the cache in the Dockerfile
as part of the build.
The sample app does not use Laravel's mix (to minify static assets) however if you want to use that in your app, you will need to install its dependencies with npm install
.
You can then run mix locally using npx mix
or npx mix --production
.
If you haven't already done so, install the Fly CLI and then log in to Fly.
To launch the app, run fly launch
from the application's directory.
The CLI will spot the existing fly.toml
:
An existing fly.toml file was found for app app-name-here
? Would you like to copy its configuration to the new app? (y/N)
Type y (yes).
The CLI will spot the Dockerfile
:
Scanning source code
Detected a Dockerfile app
You'll be asked to give the app a name. Type in your own app-name-here.
You'll be prompted to choose an organization. They are used to share resources between Fly users. Since every Fly user has a personal organization, let's pick that.
You'll be asked for the region to deploy the application in. Pick one closest to you for the best performance. That should already be selected.
It will ask if you want a database. In this case type N (no). The sample app does not need one
It will then ask if you want to deploy now. Say No. Why? In production your application needs to have a secret key set. If you were to deploy now you would see errors in the logs along the lines of:
No application encryption key has been specified. "exception":"[object] (Illuminate\Encryption\MissingAppKeyException"
You can get that secret value for APP_KEY
from your .env
file (or you can generate a new one using php artisan key:generate
).
Run fly secrets set APP_KEY=the-value-of-the-secret-key
. That will stage that secret in Fly, ready to deploy it:
Secrets are staged for the first deployment
Now you can go ahead and run fly deploy
and the build should proceed:
...
--> Building image done
==> Pushing image to fly
...
You should see the build progress, the healthchecks pass, and a message to confirm the application was successfully deployed.
You have successfully built and deployed your Laravel application on Fly.
Use fly open
as a shortcut to open the app's URL in your browser. If you are using http, Fly will upgrade it to https.
Use fly logs
to see the log files.
Use fly status
to see its details:
App
Name = your-app-name
Owner =
Version = 1
Status = running
Hostname = your-app-name.fly.dev
Deployment Status
ID = a3c2f40e-bed9-4ce1-923a-9d8ad3183a1c
Version = v1
Status = successful
Description = Deployment completed successfully
Instances = 1 desired, 1 placed, 1 healthy, 0 unhealthy
Instances
ID PROCESS VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
abcdefgh app 1 lhr run running 2 total, 2 passing 0 0h10m ago
- The
Dockerfile
deliberately does not use the--no-cache
flag when installing packages as that caused random errors. For example clearly there is annginx
package, however installingnginx
would occasionally fail:
=> ERROR [ 7/31] RUN apk add --no-cache nginx
> [ 7/31] RUN apk add --no-cache nginx
#10 0.413 fetch https://dl-cdn.alpinelinux.org/alpine/edge/main/x86_64/APKINDEX.tar.gz
#10 5.419 WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/edge/main: temporary error (try again later)
#10 5.420 fetch https://dl-cdn.alpinelinux.org/alpine/edge/community/x86_64/APKINDEX.tar.gz
#10 5.841 ERROR: unable to select packages:
#10 5.881 nginx (no such package):
#10 5.881
- The
Dockerfile
runsmix
withnpx
so that we can use a local copy of it.