Building and Installing HHVM on CentOS 7

What’s HHVM?

HHVM is an open-source virtual machine designed for executing programs written in Hack and PHP. Instead of interpreting PHP directly, it uses a just-in-time (JIT) compiler to reach much higher throughput while staying compatible with existing PHP code. Facebook, which created HHVM, reported roughly a 9x gain in web request throughput and a 5x drop in memory use over the old PHP 5.2 + APC engine, and back in 2015 HHVM could run most of the popular PHP frameworks out of the box.

Note: This post is kept as a historical record from 2015. HHVM dropped PHP support in version 4.0 (2019) and now runs only Hack, and CentOS 7 itself reached end-of-life on June 30, 2024. The steps below no longer apply to a current system. To speed up PHP today, run a recent PHP 8.x with PHP-FPM and OPcache instead. The walkthrough is preserved as a snapshot of how HHVM was built and deployed at the time.

Prerequisites

Building HHVM from source is heavy. The final link step alone can eat well over a gigabyte of memory, so compile on a machine or VPS with at least 2 GB of RAM, ideally more. On a smaller instance the build either crawls or gets cut down by the OOM killer halfway through. Expect the whole compile to take anywhere from twenty minutes to over an hour depending on your CPU.

Prepare

We are not using sudo here because it makes the columns so long. Do it yourself or just be root here, for a short while.

HHVM pulls in a long list of libraries that CentOS’s base repositories don’t all carry, so start by enabling two extra ones: EPEL (Extra Packages for Enterprise Linux) and Remi, which ships the newer multimedia packages HHVM needs, including a compatible ImageMagick:

rpm -Uvh http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-5.noarch.rpm
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm # ImageMagick

With the repos in place, pull in the toolchain (GCC, CMake, Git) and the -devel header packages HHVM links against. The brace expansion is just shorthand: the shell expands {a,b}-devel into a-devel b-devel so the list stays readable instead of running off the screen:

# Use bash brace extension here or the list would be really frightening
yum install cpp gcc-c++ cmake git psmisc {binutils,boost,jemalloc}-devel \
{sqlite,tbb,bzip2,openldap,readline,elfutils-libelf,gmp,lz4,pcre}-devel \
lib{xslt,event,yaml,vpx,png,zip,icu,mcrypt,memcached,cap,dwarf}-devel \
{unixODBC,expat,mariadb}-devel lib{edit,curl,xml2,xslt}-devel \
glog-devel oniguruma-devel inotify-tools-devel ocaml

# HHVM won't build without ImageMagick from remi failing after hphp_runtime_static
yum remove ImageMagick # If it's already installed
yum install ImageMagick-last\* --enablerepo=remi # Newer one

One quirk: glog-devel installs its shared library under /usr/lib64, but HHVM’s build looks for it in /usr/lib. Bridge the two with a symlink, otherwise linking fails:

ln -s /usr/lib64/libglog.so /usr/lib/libglog.so

Get the Source

Clone the repository with --recursive. HHVM keeps several of its dependencies as Git submodules, and the build dies partway through if they aren’t all checked out:

cd /tmp
git clone https://github.com/facebook/hhvm -b master hhvm --recursive
cd hhvm

Build HHVM

HHVM builds with CMake; the bundled ./configure is just a thin wrapper around it, so call cmake directly. The three -D flags point the build at the ImageMagick libraries you installed from Remi; without them CMake either can’t find ImageMagick or links the wrong version, and the build fails late, after hphp_runtime_static, which is a frustrating place to discover the problem. The make -j line runs one job per core plus one. This is by far the most time-consuming step, and on a 2 GB box those parallel jobs are also the most likely thing to exhaust your RAM, so lower the job count if make gets killed:

# ./configure # That is a cmake wrapper, ignore it.
cmake \
-DLIBMAGICKWAND_INCLUDE_DIRS="/usr/include/ImageMagick-6" \
-DLIBMAGICKCORE_LIBRARIES="/usr/lib64/libMagickCore-6.Q16.so" \
-DLIBMAGICKWAND_LIBRARIES="/usr/lib64/libMagickWand-6.Q16.so" .
make -j$(($(nproc)+1)) # make with CORE_COUNT+1 threads, that would be fast. You may run out of RAM
# Test..
./hphp/hhvm/hhvm --version
# Install it..
sudo make install

Add HHVM to Services

CentOS 7 ships with systemd, so register HHVM as a unit. Save the following to /usr/lib/systemd/system/hhvm.service:

[Unit]
Description=HHVM HipHop Virtual Machine (FCGI)

[Service]
ExecStart=/usr/local/bin/hhvm \
--config /etc/hhvm/server.ini \
--config /etc/hhvm/php.ini \
--config /etc/hhvm/config.hdf \
--user nginx \
--mode daemon \
-vServer.Type=fastcgi \
-vServer.Port=9001

[Install]
WantedBy=multi-user.target

The unit runs HHVM in daemon mode as the nginx user and has it speak FastCGI on port 9001, the port nginx will forward PHP requests to. The three --config files don’t exist yet; you’ll create them in the next section. Now enable it so it starts on boot:

systemctl enable hhvm
systemctl start hhvm
systemctl status hhvm

Configure HHVM

HHVM reads its settings from the three files the unit references. First create the directories it writes logs and runtime state into, owned by the same nginx user the service runs as:

mkdir /etc/hhvm
mkdir /var/run/hhvm
sudo chown nginx:nginx /var/run/hhvm
mkdir /var/log/hhvm
sudo chown nginx:nginx /var/log/hhvm

config.hdf holds runtime limits and logging in HHVM’s HDF format: resource caps, where the error and access logs go, and MySQL/mail defaults. Create it in /etc/hhvm:

ResourceLimit {
    CoreFileSize = 0 # in bytes
    MaxSocket = 10000 # must be not 0, otherwise HHVM will not start
    SocketDefaultTimeout = 5 # in seconds
    MaxRSS = 0
    MaxRSSPollingCycle = 0 # in seconds, how often to check max memory
    DropCacheCycle = 0 # in seconds, how often to drop disk cache
}

Log {
    Level = Info
    AlwaysLogUnhandledExceptions = true
    RuntimeErrorReportingLevel = 8191
    UseLogFile = true
    UseSyslog = false
    File = /var/log/hhvm/error.log
    Access {
        * {
            File = /var/log/hhvm/access.log
            Format = %h %l %u %t \"%r\" %>s %b
        }
    }
}

MySQL {
    ReadOnly = false
    ConnectTimeout = 1000 # in ms
    ReadTimeout = 1000 # in ms
    SlowQueryThreshold = 1000 # in ms, log slow queries as errors
    KillOnTimeout = false
}

Mail {
    SendmailPath = /usr/sbin/sendmail -t -i
    ForceExtraParameters =
}

server.ini is the server-facing config: it pins the FastCGI port (matching the unit file), the PID file, and where HHVM stores its compiled bytecode cache (hhvm.hhbc). Create it in /etc/hhvm:

; php options
pid = /var/run/hhvm/pid
; hhvm specific
hhvm.server.port = 9001
;hhvm.server.file_socket = /var/run/hhvm/sock
hhvm.server.type = fastcgi
hhvm.server.default_document = index.php
hhvm.log.use_log_file = true
hhvm.log.file = /var/log/hhvm/error.log
hhvm.repo.central.path = /var/run/hhvm/hhvm.hhbc

php.ini carries the familiar PHP knobs (memory limit, max post size) alongside a couple of HHVM-specific settings. Create it in /etc/hhvm:

hhvm.mysql.socket = /tmp/mysql.sock
hhvm.server.expose_hphp = true
memory_limit = 400M
post_max_size = 50M

Point Nginx at HHVM

HHVM is now listening on FastCGI port 9001, but nothing is sending it requests yet. The last step is to tell nginx to hand off PHP files to HHVM instead of serving them as plain text. Add a location block to your server config:

location ~ \.(hh|php)$ {
    fastcgi_pass   127.0.0.1:9001;
    fastcgi_index  index.php;
    include        fastcgi_params;
}

Reload nginx with systemctl reload nginx, then drop a phpinfo() page somewhere and load it. You should see HHVM identified as the engine, confirming requests are running through the JIT rather than stock PHP.

Reference