WordPress on Heroku with HHVM

How to

The instructions worked me, and I installed WP’s DB and set up my admin user.

Now it gets weird

I added the WordPress importer plugin so I could transfer my posts, and the Independent Publisher theme to composer.json like this:

"require": {
        "hhvm": "~3.2",
        "WordPress/WordPress": "*",
        "wpackagist-plugin/jetpack": "~3.1",
        "wpackagist-plugin/wpro": "~1.0",
        "wpackagist-plugin/sendgrid-email-delivery-simplified": "~1.3",
        "wpackagist-plugin/authy-two-factor-authentication": "~2.5",
        "wpackagist-plugin/wordpress-importer": "*",
        "wpackagist-theme/independent-publisher": "1.6"
 
    },

Since composer.lock is included in my repo, just re-deploying the site will not make composer install the new dependencies from composer.json. Why? Composer looks to composer.lock first because it has the exact versions of the packages to install. There are two workarounds:

  1. Remove composer.lock from Git repo
  2. Update composer.json AND composer.lock on some machine before committing to Git and deploying on Heroku

Remove composer.lock from Git repo?

This is easy, but not recommended because:

  1. Deploys will take longer and use more memory as the exact versions of all dependencies in composer.json will have to be resolved on every deploy

  2. Developers and production servers may end up with different composer.lock files, potentially resulting in everyone having out-of-sync dependencies

I wrote more about versioning composer.lock here.

Update both files before deploying?

Because "hhvm": "~3.2", is included in the required dependencies, composer update must be run using HHVM instead of PHP. That means I need an install of HHVM outside of Heroku just to update composer.lock

HHVM requires a 64 bit Unix-like operating system. It will not run on my Windows dev machine, nor a 32-bit Linux VM which is what my dev machine is capable of running. Time to upgrade?

Temporary Solution

For now, I have to follow this workflow to add a new dependency in composer.json:

1) Remove "hhvm": "~3.2", from composer.json
2) Run composer update on my dev box without HHVM installed
3) Add "hhvm": "~3.2", back to composer.json for Heroku’s sake
4) Commit composer.lock and composer.json to Git and deploy

HHVM will not appear in composer.lock, but that’s OK. Heroku still runs it.

Update after deploying?

This was tempting:

  1. Use heroku run bash to fire up another dyno with a CLI
  2. Update composer.lock in there
  3. Commit composer.lock to Git and push it
  4. Redeploy the site and get new dependencies when composer install runs

But I found the command hhvm ``which composer`` update throws an error:

Loading composer repositories with package information
Updating dependencies (including require-dev)
SlowTimer [5000ms] at curl: http://wpackagist.org/p/providers-old$77702c9f39565428994a020971d129f042db127809c1caa49589ce0862e93278.json
SlowTimer [5000ms] at curl: http://wpackagist.org/p/providers-old$77702c9f39565428994a020971d129f042db127809c1caa49589ce0862e93278.json
 
 
 
  [Composer\Downloader\TransportException]
  The "http://wpackagist.org/p/providers-old$77702c9f39565428994a020971d129f0
  42db127809c1caa49589ce0862e93278.json" file could not be downloaded: Failed
   to open http://wpackagist.org/p/providers-old$77702c9f39565428994a020971d1
  29f042db127809c1caa49589ce0862e93278.json (Operation timed out after 4949 m
  illiseconds with 2592872 out of 4680293 bytes received)

What about composer’s --ignore-platform-reqs ?

composer update --ignore-platform-reqs will get the update command to run locally for me, but the resulting composer.lock file is not suitable to run on Heroku. The app displays an error in browser, and running heroku logs shows that the deployment did not go smoothly:

2014-12-16T14:29:53.818251+00:00 app[web.1]: app_boot.sh: 23: app_boot.sh: vendor/bin/heroku-hhvm-nginx: not found

How did Heroku + HHVM perform?

Browsing through posts and pages was faster than on my current shared hosting (with neither install using caching). Performance in the admin area was about the same, which is where I wanted to see the most improvement.

Why to version your composer.lock file

Composer.json often lists dependencies with version numbers that can point to a bunch of potential packages. Lines like this:

"require": {
    "laravel/framework": "4.2.*",
    "ruflin/elastica": "1.3.0.0",
    "guzzlehttp/guzzle": "~4.0",
    "sunra/php-simple-html-dom-parser": "1.5.0"
},

When composer install is run without a composer.lock file available, it has to translate the ~, *, =>, and other bits of flexible version numbering into an real version number to download for you. It also has to grab the dependencies of each one of the dependencies you specify. This takes time, memory and bandwidth. When composer finishes, it saves a list of the packages it has downloaded to composer.lock with real version numbers and all the required packages included.

Example Scenario

You start a new project by yourself. You set up composer.json with some packages, run composer install and commit all the right stuff to get, minus composer.lock

72 hours later

Another developer on your team receives your code, including the composer.json. They run composer install and receive a different set of packages than you did 72 hours earlier because one of the dependencies pushed an update. Will that be a problem? Maybe…

72 hours later

The project is done! Time to deploy. Somewhere in the process composer install runs. All the dependencies will be resolved again wasting bandwidth, memory, time, and potentially installing a different set up of packages then on either development machines. This won’t introduce bugs if you are lucky, but it isn’t optimal.

What’s inside composer.lock?

You’ll notice composer.lock is a much bigger file than composer.json (72kb vs 1kb for my example). Here’s what it looks like:

{
    "_readme": [
        "This file locks the dependencies of your project to a known state",
        "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
        "This file is @generated automatically"
    ],
    "hash": "675d6a1de44ac07f57f730785f25142b",
    "packages": [
        {
            "name": "classpreloader/classpreloader",
            "version": "1.0.2",
            "source": {
                "type": "git",
                "url": "https://github.com/mtdowling/ClassPreloader.git",
                "reference": "2c9f3bcbab329570c57339895bd11b5dd3b00877"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/mtdowling/ClassPreloader/zipball/2c9f3bcbab329570c57339895bd11b5dd3b00877",
                "reference": "2c9f3bcbab329570c57339895bd11b5dd3b00877",
                "shasum": ""
            },
            "require": {
                "nikic/php-parser": "~0.9",
                "php": ">=5.3.3",
                "symfony/console": "~2.1",
                "symfony/filesystem": "~2.1",
                "symfony/finder": "~2.1"
            },
            "bin": [
                "classpreloader.php"
            ],
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0-dev"
                }
            },
            "autoload": {
                "psr-0": {
                    "ClassPreloader": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "description": "Helps class loading performance by generating a single PHP file containing all of the autoloaded files for a specific use case",
            "keywords": [
                "autoload",
                "class",
                "preload"
            ],
            "time": "2014-03-12 00:05:31"
        },
        {
            "name": "d11wtq/boris",
            "version": "v1.0.8",
            "source": {
                "type": "git",
                "url": "https://github.com/d11wtq/boris.git",
                "reference": "125dd4e5752639af7678a22ea597115646d89c6e"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/d11wtq/boris/zipball/125dd4e5752639af7678a22ea597115646d89c6e",
                "reference": "125dd4e5752639af7678a22ea597115646d89c6e",
                "shasum": ""
            },
            "require": {
                "php": ">=5.3.0"
            },
            "suggest": {
                "ext-pcntl": "*",
                "ext-posix": "*",
                "ext-readline": "*"
            },
            "bin": [
                "bin/boris"
            ],
            "type": "library",
            "autoload": {
                "psr-0": {
                    "Boris": "lib"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "time": "2014-01-17 12:21:18"
        },
 
......[more packages listed here]....
 
    ],
    "aliases": [
 
    ],
    "minimum-stability": "stable",
    "stability-flags": {
        "mockery/mockery": 20
    },
    "prefer-stable": false,
    "platform": [
 
    ],
    "platform-dev": [
 
    ]
}

You’ll find all the packages in composer.json and the resolved dependencies listed, each with their version number. Why is this better? When composer install uses information from composer.lock, it simply downloads the listed packages, and doesn’t have to traverse through the dependency tree searching for packages, or figure out which version of a package to download. This saves time when a developer or server runs composer install, and ensures the same versions of every package are requested every time. Keep composer.lock in version control, and the team and production server will benefit from this when composer install is run.

More info

Read this conversation on GitHub:

KingCrunch commented on Feb 11

@paparts Sounds like you don’t versionize the composer.lock? As a rule of thumb: For applications versionize it, for libraries, don’t. You shouldn’t run update on a live system, because it is quite likely, that sooner or later a package comes in, that breaks your application, without that you’ve tested it locally. The composer.lock and composer.phar install ensures, that exactly that packages in that versions are installed, that you’ve development your application against.

paparts commented on Feb 12

I didn’t notice that the framework I was using has listed the composer.lock on the ignore list. Thanks for pointing that out.

Fatal error: Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory' in phar:///home/ubuntu/somefolder/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php:985

Here’s an explanation of composer install and composer update and how they are different: http://adamcod.es/2013/03/07/composer-install-vs-composer-update.html.

Here’s the official composer documentation explaining what the lock file does: https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file

How to run composer.phar on Dreamhost

Dreamhost’s shared hosting offers up some challenges to get Composer’s composer.phar running.

  1. Even if your websites are setup to use PHP 5.3 or 5.4, the CLI version of php might be PHP 5.2, which is too old and doesn’t include phar. You can check this by logging into your account with a terminal via SSH and the command
    php --version
  2. Since this is a shared hosting environment, you can’t change things the way you would in own Vagrant Ubuntu box or local dev environment.

You’ll have to get through these to run Laravel 4 and other applications that rely on Composer.

GitHub user Braunson has the best walkthrough on setting up composer.phar on DreamHost that I have found. These instructions worked on my account. I did have to do the extra step of putting phprc into a 5.4 folder. After logging out of SSH and logging back in, the phar extension worked, and I could use composer.

Another way to check if phar is available is to run

php -i | grep phar

, which will pull out phar-related config info. A server with phar enabled will look like this:

Configure Command =>  './configure'  '--prefix=/usr/local/php54' '--datadir=/usr/local/php54/share' '--localstatedir=/usr/local/php54/var' '--without-pear' '--with-config-file-path=/etc/php54' '--with-config-file-scan-dir=/etc/php54/conf.d' '--with-zend-vm=GOTO' '--enable-cgi' '--enable-fpm' '--enable-libxml=shared' '--enable-bcmath=shared' '--enable-calendar=shared' '--enable-ctype=shared' '--enable-dom=shared' '--enable-exif=shared' '--enable-fileinfo=shared' '--enable-filter=shared' '--enable-ftp=shared' '--enable-hash=shared' '--enable-intl=shared' '--enable-json=shared' '--enable-mbstring' '--enable-mbregex' '--enable-mbregex-backtrack' '--enable-pcntl=shared' '--enable-pdo' '--enable-phar=shared' '--enable-posix=shared' '--enable-session=shared' '--enable-shmop=shared' '--enable-simplexml=shared' '--enable-soap=shared' '--enable-sockets=shared' '--enable-sysvmsg=shared' '--enable-sysvsem=shared' '--enable-sysvshm=shared' '--enable-tokenizer
=shared' '--enable-wddx=shared' '--enable-xml=shared' '--enable-xmlreader=shared' '--enable-xmlwriter=shared' '--enable-zip=shared' '--with-openssl=shared' '--with-kerberos' '--with-pcre-regex' '--with-sqlite3' '--with-zlib=shared' '--with-bz2=shared' '--with-curl=shared' '--with-gd=shared' '--with-jpeg-dir=/usr' '--with-png-dir=/usr' '--with-zlib-dir=/usr' '--with-xpm-dir=/usr' '--with-freetype-dir=/usr' '--with-gettext=shared' '--with-gmp=shared' '--with-mhash=shared' '--with-iconv=shared' '--with-imap=shared' '--with-imap-ssl' '--with-ldap=shared' '--with-mcrypt=shared' '--with-msql=shared' '--with-mysql=shared,mysqlnd' '--with-mysql-sock=/No-MySQL-hostname-was-specified' '--with-mysqli=shared,mysqlnd' '--with-pdo-mysql=shared,mysqlnd' '--with-pdo-pgsql=shared' '--with-pdo-sqlite' '--with-pgsql=shared' '--with-pspell=shared' '--with-readline=shared' '--with-tidy=shared' '--with-xmlrpc=shared' '--with-xsl=shared'
Registered PHP Streams => compress.bzip2, php, file, glob, data, http, ftp, http
s, ftps, zip, compress.zlib, phar
Phar-based phar archives => enabled
Tar-based phar archives => enabled
ZIP-based phar archives => enabled
phar.cache_list => no value => no value
phar.readonly => On => On
phar.require_hash => On => On

Notice all the lines towards the bottom that show phar is available.