We’re going to save you money today by running a queue worker in a Fly.io machine. Read on to see how exactly this saves money and how I set it up. With Fly.io, you can get your Laravel app running globally in minutes!
Fly.io machines have been announced and have been made the default way new apps will be run on Fly.io’s platform. They have many advantages, and one of them is their super-fast boot times.
In this article, I’ll show you how to use machines to run both the web app and a queue worker in the same app. I’ll also save you money in the process, so let’s dive in!
Setting up the web app and worker
First things first: I will be talking about apps and machines here.
By apps I mean applications on Fly.io, they are created by running fly launch
and they have a fly.toml
file that holds their configuration. An app contains one or more machines.
Machines are VMs that belong to a specific app, and they have some special Fly.io sauce on top: The machines API. This is a REST api that allows you to manipulate these machines in all kinds of ways: update, scale, destroy, restart, clone,… You name it!
The cool thing about machines in an app is that they can run different commands. So I can create one app that contains two machines, one for the web app and one for the queue worker. This is set up in the fly.toml
using process groups. The app
process group is the default, and will be set up already if you ran fly launch
. I added a worker
process group by adding it to the processes
tag:
[processes]
app = ""
worker = "php artisan queue:work --stop-when-empty"
The app
will run whatever is in the Docker entrypoint, and the worker will run the php artisan queue:work --stop-when-empty
command. This will keep the worker running as long as it’s fed with new jobs to process. When it’s idle, the machine will stop gracefully and stop slowly draining your wallet. Good stuff!
How do we get the machine running again though? That’s where the machines API comes in: If we know the ID of the worker machine, we can just run a CURL command to start the machine after we added in a job. That’s nice, but it could be even more convenient! I created an artisan command to start a machine, and I’ll run it whenever I dispatch a job.
If you need an example of a job that would be great fit to test our shiny new machines, check out my article on invoice PDF generation with Spatie’s Browsershot package!
Starting a machine with an artisan command
Here’s the machines API endpoint I’ll use to start a machine:
curl -i -X POST \
-H "Authorization: Bearer ${FLY_API_TOKEN}" -H "Content-Type: application/json" \
"http://${FLY_API_HOSTNAME}/v1/apps/${FLY_APP_NAME}/machines/${FLY_MACHINE_ID}/start"
There’s 4 variables that need to be filled in:
- FLY_API_TOKEN: the api token to authenticate on fly.io. Run
fly auth token
to get it. - FLY_API_HOSTNAME: the hostname of Fly.io’s machines API.
_api.internal:4280
when connected to Fly.io’s internal network (in apps running on Fly.io for example) or127.0.0.1:4280
if you’re usingflyctl proxy
. - FLY_APP_NAME: The name of your app on Fly.io.
- FLY_MACHINE_ID: The ID of the
worker
machine.
I’d suggest putting the last three in the [env]
section of fly.toml
, since they will never change much. The FLY_API_TOKEN won’t change as well, but that’s quite sensitive so I’d use secrets for that.
I wanted my artisan command to be generic, so I set the machine ID as input on the command. It’ll be pulled from the env variables anyway, but this leaves room for multiple workers that handle different queues and/or connections. Here’s how it looks:
protected $signature = 'machine:start {id : the ID of the machine to be started.}';
protected $description = "This command starts a Fly.io machine. It needs the machine's ID as input.";
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$flyApiHostname = "http://_api.internal:4280";
$flyAuthToken = env("FLY_AUTH_TOKEN");
$flyAppName = env("FLY_APP_NAME");
$machineId = $this->argument('id');
$response = \Http::withHeaders([
'Authorization' => "Bearer $flyAuthToken",
'Content-Type' => 'application/json'
])->post("$flyApiHostname/v1/apps/$flyAppName/machines/$machineId/start");
if ($response->failed())
{
$this->error($response);
return Command::FAILURE;
}
$this->info($response);
return Command::SUCCESS;
}
Running the artisan command whenever a job is dispatched
If I can get that machine:start <ID>
command to run every time this job is dispatched, then the worker machine will boot up every time a job is added to the queue. Great stuff! I did this by extending the dispatch()
method on the Dispatchable
trait that my job uses.
// in the Job class
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+ use Dispatchable {dispatch as traitDispatch;}
+ use InteractsWithQueue, Queueable, SerializesModels;
// other stuff here...
+ public static function dispatch(...$arguments)
+ {
+ Artisan::call('machine:start', ['id' => env('FLY_WORKER_ID')]);
+ self::traitDispatch(...$arguments);
+ }
Notice that I’m not overriding the dispatch()
method, I’m extending it: I do what I need to do and then I call self::traitDispatch(...$arguments)
which runs the original dispatch()
method. This way, the Artisan call:
command is executed every time right before the job is actually dispatched. Nice!
That’s a wrap!
By now, I’ve talked about Fly.io’s machines a lot: What they are, what makes them special and how to use the machines API. Since I want to make things as easy as possible, I’ve also laid out how to start a machine whenever needed using an Artisan command.
This all comes together to set up a machine that only runs when it needs to. This saves on computing resources which is good for the environment but more importantly: it saves you money. Now go pitch using Fly.io machines to your boss and get that raise you know you deserve!