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 below

2. 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 is promoted 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-onRun 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

Solutions Architect. Technical Lead. Full-stack Developer. http://craigmorris.io