Craft 3 on Heroku: a start-to-finish guide

Deploy Craft 3 on Heroku complete with PHP 7.1, S3 uploads, CDN delivery, SCSS build process and inline CSS.

Introduction

This guide assumes the use of AWS, GitHub and Heroku. The guide will get you up and running both locally and on Heroku.

Project set up

Craft 3 is managed as a Composer package, which makes it easier to maintain and update than Craft 2.

Here's a summary of what's provided on Craft's install guide:

# Install composer if not already available
brew tap homebrew/dupes
brew tap homebrew/php
brew install composer
composer self-update

# Create Craft 3 project
composer create-project -s RC craftcms/craft PROJECT_DIR

Local Apache

Although macOS ships with Apache, I find it easier to use a Brew version. This is in part because the configuration for the OS version resets with every major OS update. Also, the macOS version is often a minor version or two behind.

Much of the following is based on the guide found on the Grav website. The short version is:

# Stop the default Apache
sudo apachectl stop
sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist 2> /dev/null

# Install Apache 2.4
brew install httpd24 --with-privileged-ports --with-http2
sudo brew services start httpd

# Install PHP 7.1 with Postgres and Apache support
brew install php71 --with-httpd24 --with-postgresql

You then need to open /usr/local/etc/httpd/httpd.conf and make the following modifications:

  • Change the port: 8080 => 80
  • Change root directory: /usr/local/var/www => /Users/USERNAME/Sites
  • Allow for .htaccess overrides: Override None => Override All
  • Allow for mod_rewrite by uncommenting:
    LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
    
  • Change the user to yourself
  • Change group to your own (typically staff)
  • Enable PHP on .php files by adding:
    <IfModule dir_module>
      DirectoryIndex index.php index.html
    </IfModule>
    <FilesMatch \.php$>
      SetHandler application/x-httpd-php
    </FilesMatch>
    
  • Uncomment the vhosts file. I like to tweak this line and move the configuration file to ~/Sites:
    Include /Users/USERNAME/Sites/httpd-vhosts.conf
    

To finish off the setup you need to create a httpd-vhosts.conf file. Add the following entry for your site, remembering to replace PROJECT_DIR, USERNAME and SITE_URL (e.g. torbensko.local) with your own:

<VirtualHost *:80>
  DocumentRoot "/Users/USERNAME/PROJECT_DIR/web"
  ServerName SITE_URL

  <Directory "/Users/USERNAME/PROJECT_DIR">
    Options Indexes MultiViews FollowSymLinks
    AllowOverride All
    Order allow,deny
    Allow from all
    Require all granted
  </Directory>
</VirtualHost>

To make your new site available at the specified URL, open /etc/hosts and add (replacing SITE_URL with same as above):

127.0.0.1 SITE_URL

Once done you'll need to restart Apache:

sudo apachectl -k restart

Database

As Heroku has strong support for Postgres, it's easier to use Craft with Postgres. To get up and running locally, download and install Postgres:

brew install postgres
brew services start postgresql

Create a database:

createuser DB_NAME
createdb -O DB_USER -Eutf8 DB_NAME

By default, Craft like to use separate environment variables for the database name, host, etc. By comparison Heroku uses a single variable: DATABASE_URL. It's easier to use this variable so we can take advantage of some of Heroku's database features, including credential cycling and database switching.

To allow Craft to accept the DATABASE_URL variable open config/db.php and replace with:

<?php

// e.g. postgres://USERNAME:[email protected]:PORT/DB_NAME
preg_match('|postgres://([a-z0-9]*):([a-z0-9]*)@([^:]*):([0-9]*)/(.*)|i', getenv('DATABASE_URL'), $matches);

$user = $matches[1];
$password = $matches[2];
$server = $matches[3];
$port = $matches[4];
$database = $matches[5];

return [
    'driver' => "pgsql",
    'server' => $server,
    'user' => $user,
    'password' => $password,
    'database' => $database,
    'schema' => getenv('DB_SCHEMA'),
    'tablePrefix' => getenv('DB_TABLE_PREFIX'),
    'port' => $port
];

Open .env and add:

DATABASE_URL="postgres://DB_USER:@localhost:5432/DB_NAME"

To inspect and modify your database, I recommend using Postico. You can install this using Homebrew Cask:

brew cask install postico

Setting up Craft

Update your .env and set the environment to local. Open general.php and add a new setting, remembering to replace SITE_URL:

    // Local environment settings
    'local' => [
        // Base site URL
        'siteUrl' => 'http://SITE_URL',

        // Dev Mode (see https://craftcms.com/support/dev-mode)
        'devMode' => true,
    ],

Open your browser and navigate to http://SITE_URL/admin and follow the instructions.

Grunt and SASS

I currently use Grunt to build my site style sheet. To set up do the following:

npm init
npm i autoprefixer dotenv grunt grunt-cli grunt-concurrent grunt-contrib-clean grunt-contrib-copy grunt-contrib-watch grunt-postcss grunt-sass jit-grunt --save

Create a Gruntfile.js in your project folder and add:

'use strict';

require('dotenv').config();

module.exports = function(grunt) {
  
  var pkgConfig = grunt.file.readJSON('package.json');

  require('jit-grunt')(grunt, {});

  // Project configuration.
  grunt.initConfig({
    pkg: pkgConfig,

    watch: {
      sass: {
        files: ['scss/{,*/}*.{scss,sass}'],
        tasks: ['build:css']
      },
    },

    sass: {
      options: {
        outputStyle: process.env.OPTIMISE_CSS ? 'compressed' : 'nested',
        includePaths: ['node_modules']
      },
      dist: {
        files: {
          'web/styles/main.css': 'scss/main.scss'
        }
      }
    },

    // Add vendor prefixed styles
    postcss: {
      options: {
        processors: [require('autoprefixer')({ browsers: ['last 1 version', 'ie 9'] })]
      },
      dist: {
        files: [
          {
            expand: true,
            cwd: 'web/styles/',
            src: '{,*/}*.css',
            dest: 'web/styles/'
          }
        ]
      }
    },
  });

  grunt.registerTask('build:css', [
      'sass', 
      'postcss'
    ]);

  grunt.registerTask('build', [
      'build:css'
    ]);
};

Create a PROJECT_DIR/scss folder and add a main.scss. Then run grunt build to do a one time build or run grunt watch to build automatically on changes.

If your CSS is small enough (say < 50kb), I would recommend embedding it into your HTML. Although this bloats your HTML it avoids an extra server request and generally improves your site performance. To do so install the Inlin plugin:

composer require aelvan/inlin

Add to your page layout:

<style>
  {{ craft.inlin.er('styles/main.css', true) | raw }}
  </style>

Setting up S3 for assets

Install the S3 plugin either via the web interface or via composer:

composer require craftcms/aws-s3

Open Craft and enable the plugin via the control panel.

Assuming you know how to use AWS, log into your AWS account and create a new bucket. Then create a new programatic IAM user for the CMS. You'll need to give the user permissions to access the bucket. To avoid giving blanket access, I use the following permission:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ListS3",
            "Effect": "Allow",
            "Action": [
                "s3:ListAllMyBuckets",
                "s3:ListBucket",
                "s3:HeadBucket",
                "s3:ListObjects"
            ],
            "Resource": "*"
        },
        {
            "Sid": "CmsBucketAccess",
            "Effect": "Allow",
            "Action": [
                "s3:PutAnalyticsConfiguration",
                "s3:GetObjectVersionTagging",
                "s3:CreateBucket",
                "s3:ReplicateObject",
                "s3:GetObjectAcl",
                "s3:DeleteBucketWebsite",
                "s3:PutLifecycleConfiguration",
                "s3:GetObjectVersionAcl",
                "s3:PutObjectTagging",
                "s3:DeleteObject",
                "s3:GetIpConfiguration",
                "s3:DeleteObjectTagging",
                "s3:GetBucketWebsite",
                "s3:PutReplicationConfiguration",
                "s3:DeleteObjectVersionTagging",
                "s3:GetBucketNotification",
                "s3:PutBucketCORS",
                "s3:GetReplicationConfiguration",
                "s3:ListMultipartUploadParts",
                "s3:PutObject",
                "s3:GetObject",
                "s3:PutBucketNotification",
                "s3:PutBucketLogging",
                "s3:GetAnalyticsConfiguration",
                "s3:GetObjectVersionForReplication",
                "s3:GetLifecycleConfiguration",
                "s3:ListBucketByTags",
                "s3:GetInventoryConfiguration",
                "s3:GetBucketTagging",
                "s3:PutAccelerateConfiguration",
                "s3:DeleteObjectVersion",
                "s3:GetBucketLogging",
                "s3:ListBucketVersions",
                "s3:ReplicateTags",
                "s3:RestoreObject",
                "s3:GetAccelerateConfiguration",
                "s3:GetBucketPolicy",
                "s3:GetObjectVersionTorrent",
                "s3:AbortMultipartUpload",
                "s3:PutBucketTagging",
                "s3:GetBucketRequestPayment",
                "s3:GetObjectTagging",
                "s3:GetMetricsConfiguration",
                "s3:DeleteBucket",
                "s3:PutBucketVersioning",
                "s3:ListBucketMultipartUploads",
                "s3:PutMetricsConfiguration",
                "s3:PutObjectVersionTagging",
                "s3:GetBucketVersioning",
                "s3:GetBucketAcl",
                "s3:PutInventoryConfiguration",
                "s3:PutIpConfiguration",
                "s3:GetObjectTorrent",
                "s3:PutBucketWebsite",
                "s3:PutBucketRequestPayment",
                "s3:GetBucketCORS",
                "s3:GetBucketLocation",
                "s3:ReplicateDelete",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}

Please note: I don't provide any guarantees on the quality or reliability of this permission - use at your own risk. If there are any AWS experts out there, please let me know if you have any recommendations for refining the permission.

Within the Craft control panel, create a new asset volume. Once you copy the IAM credentials in you should be able to select your bucket.

To further increase your performance, you can also use AWS's CloudFront. Just setup up the distribution for you bucket and add the details to your asset volume via the Craft control panel.

Setting up Heroku

Assuming you know the basics of Heroku, log in and do the following:

  • Create a new app
  • Provision the Heroku Postgres add-on. The free tier should initially serve you well.
  • (Optionally) attach the project to your GitHub repo and enable automatic deploys.

Locally, install the Heroku CLI:

brew install heroku/brew/heroku

Log into the tool (heroku auth:login) and add the NodeJS buildpack to your app so the Grunt process will work.

heroku buildpacks:add --index 2 heroku/nodejs --app HEROKU_APP_NAME

Add to your project a Procfile with the following:

web: vendor/bin/heroku-php-apache2 web

Publishing

If you have setup automatic deploys you can deploy the code using: git push. To push a copy of your database to the remote server run the following:

# If the DB exists you'll need to reset it. It is worth taking a back up using the Heroku web interface. Replace HEROKU_APP_NAME
heroku pg:reset DATABASE_URL --app HEROKU_APP_NAME

# Replace DB_NAME with your local database name and HEROKU_APP_NAME.
heroku pg:push DB_NAME DATABASE_URL --app HEROKU_APP_NAME

If you wish to take a copy of your remote database, you can use the following command:

# Replace DB_NAME and HEROKU_APP_NAME
heroku pg:pull DATABASE_URL DB_NAME-$(date +%F) --app HEROKU_APP_NAME

If you want to keep multiple versions of the database locally, you can instead use:

# You'll need to update DATABASE_URL in your `.env` file
heroku pg:pull DATABASE_URL DB_NAME-$(date +%F) --app HEROKU_APP_NAME

Conclusion

This should have you up and running with a Heroku/Craft 3 site. If you have any further questions about the process please leave me a comment below.

Read more

Craft 3 on Heroku: a start-to-finish guide

Deploy Craft 3 on Heroku complete with PHP 7.1, S3 uploads, CDN delivery, SCSS build process and inline CSS.

Read more...