Laravel on Heroku: The Ultimate Guide
Heroku provides a good base level guide to get a Laravel running on its platform, however, it really stops at the bare minimum.
We’ve got 10+ (micro-ish) services running in production on Heroku and over the years we’ve fine tuned our setup to ensure that these apps are performant and our DevOps is as seamless as possible.
There’s a bit of scope, so we’ll break it down into key areas. Follow along in the app’s lifecycle:
- When an app is provisioned
- When an app is built
- When an app is released
- When an app is released for the FIRST time
- When an app is running
- When an app is running tests
When an app is provisioned
Here we will tell Heroku what services we require for an app. Assuming that most Laravel apps will store data in a database and have Redis for managing things like queues.
Let’s create an app.json
in the root directory so that Heroku will automatically provision these on our behalf.
When Heroku detects this file it will provision the Postgres and Redis server and set the DATABASE_URL
and REDIS_URL
environment variables, ready for us to connect. We also add scheduler
to run Artisan commands on a periodic basis (aka cronjob).
When an app is built
This is where we can do any build related processes.
PHP
Heroku supports running build scripts via composer.json > scripts > compile
.
For Laravel, we’ll add the following to composer.json
1. If your routes, events, or views change per environment, move these commands to the
release
section covered below2. If you have Route Closures, they will need to be moved to Controllers to cache routes
Vue / React / Webpack Build
Heroku supports running build scripts via package.json > scripts > build
If we have any JS or CSS to build, we should add Node to our buildpack in app.json
:
Then add a build
script in your package.json. If you’re using this with Laravel Mix, then simply refer to the production script.
Running this will compile our CSS and JS and create a manifest file. Since these are build artifacts, we should ignore those compiled files. Add the following to your .gitignore
public/mix-manifest.json
public/css/app.css
public/js/app.js
When an app is released
On every release we want to run our database migrations, invalidate any cache (or warm the cache) and upload new assets to the CDN.
Heroku let’s us do this via a special release
process that we specify in our Procfile
.
Add the following to your Procfile in our root directory:
release: php artisan migrate --force && php artisan cache:clear && php artisan config:cache
We run
php artisan config:cache
here instead of during build because if a slug ispromoted
in a pipeline, the build artefacts are directly copied over, which would include the staging environment variables.
When an app is released (the first time)
If this is a brand new app, we might want to load the database with fixture data or setup oAuth clients.
Heroku lets us hook into the first time an app is deployed via the app.json > scripts > postdeploy
setting
For example if we had some fixture data and wanted to setup an oAuth Client in Laravel Passport.
When app is running
Now it’s time to get the app actually running! There are number of different things we need to setup so let’s break them down :)
Web Server (Nginx / Apache)
Our Laravel app is executed by Apache or Nginx. Nginx is generally faster so we need to tell Heroku to use Nginx and serve from the public directory.
Let’s add to our Procfile
in the root directory and add the following
web: vendor/bin/heroku-php-nginx -C nginx.conf public/
Lastly, we need to configure Nginx to rewrite URLs to Laravel. Create nginx.conf
in the root directory and add the following.
This also ensures that the app is accessed via HTTPS.
Env Variables
The env
variables in the app.json
are set on new apps created. Let’s specify what the defaults should be for new apps created from our repo.
The APP_KEY
generated by Heroku is a 64 character string however Laravel expects a 32 character string. We can modify our config/app.php
to include support for Heroku’s random value.
In addition we can also dynamically set our APP_URL
if the HEROKU_APP_NAME
variable is set (which happens for review apps or if dyno metadata is enabled).
Lastly, since Heroku provides DATABASE_URL
let’s remove the defaults in config/database.php
, otherwise it overrides what is specified in the URL.
composer.json
Let’s add ext-redis
to our composer.json
to make our connection with Redis much more efficient.
composer require ext-redis
Trust Proxies
Traffic is routed through Heroku’s Load Balancer which terminate SSL connections. We need to configure Laravel to trust the headers that Heroku’s load balancer sets.
In app/Http/Middleware/TrustProxies.php
Queue Worker
Add the following to the Procfile
worker: php artisan queue:work
Scheduler / Cron
If you have cron jobs to run, you have to configure them manually in the Heroku Scheduler interface.
I would recommend AGAINST using Laravel’s Task Scheduling. Since the scheduler is “best effort”, there’s no guarantee that the job will be run at the exact minute specified, so it won’t match the every10Minutes()
you configure using Laravel.
If you are fine with paying for a permanent Dyno, you can use Laravel Task Scheduling by following the guide from AutoIdle — A Heroku Add-on — Run Laravel Scheduled Jobs
When an app is running tests
At this point our app should be up and running on Heroku, but we want to run our automated tests using Heroku CI.
First, let’s use the TAP standard output so that Heroku can understand the results of our tests.
composer require gh640/phpunit-tap --dev
Then tell Heroku how to run our test script to use the tap printer. In app.json
The test-setup
script should usually replicate whatever you’re doing in your release
and postdeploy
scripts.
That’s it!
Your Heroku app, continuous integration and review apps should be running like a dream.
For a starter repo — check out https://github.com/morrislaptop/laravel-heroku