Deploying a Symfony app on Fly.io

So you want to deploy your Symfony app on Fly.io ? Great! But... fly launch will not be able to generate the correct configuration files for you, as there is no scanner & template yet for Symfony. However, there is a scanner for Laravel and with some changes it can be used for Symfony as well.

This is not a step-by-step guide. Trivial steps are omitted, but you can find all the modifications that I’ve made in the GitHub repository linked down below.

Generating the basic Fly configuration

As mentioned before, we will modify the Laravel configuration files to make them work for Symfony. First, we need to get those Laravel configuration files. There's two ways to get the basic Fly configuration into your project:

  1. You can copy the files from the Fly CLI repository.
  2. The faster approach is to use the Fly CLI to generate the files for you. You just have to trick the CLI into thinking your project is a Laravel project. You can do this by creating a file called artisan in the root directory of your project. This file can be empty, but it has to exist. After that you can run fly launch and it will generate the files for you. You can now remove the artisan file.
touch artisan
fly launch
rm artisan

Dealing with environment variables

There's a couple ways to specify runtime environment variables for your Symfony app in a Fly.io context:

  1. In fly.toml there is a section called [env] where you can define environment variables. These variables will be available to your application at runtime. They are suitable for environment variables that are not secret, such as your APP_DEBUG value.
  2. Fly secrets are a secure way to define environment variables. They are suitable for environment variables that are secret, such as your APP_KEY and DATABASE_URL.
  3. You can also use .env/.env.prod files for non-secret environment variables.

So, for non-secret environment variables there are two options: fly.toml or .env files. Personally, I prefer to use fly.toml for my non-secret environment variables. This means I don’t use .env or any of its variants (.env.prod etc.). To prevent them from getting into the image, I added this to the .dockerignore:

*.env*

Now, if you would try to deploy this, it would not work. This is because, by default, Symfony requires there to be a .env file. This can be disabled by specifying the disable_dotenv runtime option to be true in public/index.php:

$_SERVER['APP_RUNTIME_OPTIONS'] = [
    'disable_dotenv' => true,
];

Two problems:

  1. Now also locally you will not be able to use .env files anymore. Probably not desirable.
  2. You also need to specify this in bin/console. Meh.

To solve both problems, I decided to create a custom runtime that sets the disable_dotenv option to true:

<?php

namespace App;

use Symfony\Component\Runtime\SymfonyRuntime;

class FlySymfonyRuntime extends SymfonyRuntime
{
    public function __construct(array $options = [])
    {
        $options['disable_dotenv'] = true;

        parent::__construct($options);
    }
}

This file is then copied in my Dockerfile to the src/ directory: cp .fly/FlySymfonyRuntime.php /var/www/html/src/FlySymfonyRuntime.php and enabled by specifying APP_RUNTIME = '\App\FlySymfonyRuntime' in the fly.toml file.

Composer post-install-cmd event

Generally, Symfony projects have post-install-cmd events defined in the composer.json file that call commands such as assets:install and cache:clear. Running these in the Dockerfile can be problematic, especially when environment variables are required. Instead, I relocated these commands to the entrypoint. I did this by disabling scripts in the Dockerfile (composer install --no-scripts) and then creating a file called .fly/scripts/assets.sh with the following contents:

#!/usr/bin/env bash

/usr/bin/php /var/www/html/bin/console assets:install

As far as I understand, cache:clear only clears system caches that reside in the var/ directory, so it is not necessary to run this on every deploy: we have a brand-new VM after all.

cache:clear also warms up the cache. Because we are not calling cache:clear, we need to warm up the cache ourselves. I decided to do this by calling cache:warmup in the entrypoint. This can be done by creating a file called .fly/scripts/warmup.sh with the following contents:

#!/usr/bin/env bash

/usr/bin/php /var/www/html/bin/console cache:warmup

Dealing with the Fly.io proxy

TLS is terminated at the Fly.io proxy, so requests are coming in as HTTP requests. This can cause some annoying "bugs": URLs are generated as http:// URLs for example. To solve this, you need to declare the proxy as a trusted proxy, which can be done by specifying trusted_proxies: '127.0.0.1,REMOTE_ADDR' in the config/packages/framework.yaml file.

More information can be found here: How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy

Connecting to a database

This is the easy part! You can create a Postgres cluster on Fly using fly postgres create and then attach it using fly postgres attach <db-app-name> --app <app-name>. This will create a database and the DATABASE_URL secret for your app.

Migrations can be ran on deploy using a release command. This can be done by adding the following to the fly.toml file:

[deploy]
  command = "bin/console doctrine:migrations:migrate --no-interaction"

Queues

Queue workers could be defined as supervisor processes in your main web Fly VM, but I prefer to create a separate Fly VM for it. This is because I want to be able to scale the queue workers independently from the web VM. This can be done by adding the following to the fly.toml file:

[processes]
  app = ""
  worker = "php bin/console messenger:consume async"

Conclusion

In this post we've seen how to deploy a Symfony app on Fly.io. This is not particularly hard to do because the deployment process for Symfony is so similar to Laravel. The full code can be found here:

https://github.com/dejagersh/symfony-on-fly

I hope this was helpful! If you have any suggestions or questions, please reach out to me on Twitter: @dejagersh.