Source: unsplash.com

Getting a web development environment with Apache/Nginx, MySQL and PHP in your brand-new macOS Sierra 10.12

Clean install is an opportunity only if you don’t get caught while catching it.

With the release of the brand new macOS Sierra I caught the opportunity to do a fresh install; despite repeating “nevermore” a good amount of times, I had to restore my web development environment. Being an early Linux fan, I don’t like MAMP or other “packaged” solutions, neither I want some greedy Vagrant boxes feast on my poor MacBook Air, so I choosed the do-it-yourself way, rejecting Apple’s built-in Apache and unleashing the power of Homebrew.

I found this good walkthrough by Alan Ivey for Yosemite which needed some slight updates for Sierra, so this post follows the same structure; Alan did some work to make every change copy&paste-able, I won’t be so nice to avoid typos and to make you more aware of what you’re doing into your system. All the

code-like

sections thereafter are either terminal commands or file content that you have to edit or create, after reading them.
Remember: each time you brainlessly copy&paste, you make a little unicorn cry.

Note (19/12/2017): I switched from mod_fastcgi to the better mod_proxy_fcgi as pointed out by Rob Record; if you followed the former walkthrough you only need to update your http-php.conf, load the required modules and change the php-fpm listen parameter.

Note (14/12/2017): due to the recent changes in Chrome to force HTTPS via HSTS for .dev domains, I changed all .dev references to .test, so if you followed my setup you may want to apply the changes accordingly (thanks to Henrijs for reporting). — I also updated PHP to 7.1.

TL;DR

If you already know the details and just want a checklist, here you are:

  1. Install Homebrew
  2. Install MySQL and optimize a couple of settings
  3. Install Apache 2.4, mod_fastcgi, enable needed mods on httpd.conf, setup FastCGI for PHP-FPM and configure automatic virtual hosting
  4. Install PHP 7.1 and (optionally) opcache
  5. Install DNSMasq for name aliasing
  6. Celebrate
Source: pixabay.com

Get this party started

Foreword: this is not by any means the only way to get a web development environment on your system, nor is the best one. You can rely on Vagrant for example, which is for sure a better solution if you have the chance to automate your deployments with Capistrano or Ansible. It’s just the best for my needs, so try it and choose by yourself.

As with its predecessors, macOS Sierra comes with bare-metal Apache+PHP; nevertheless I like to stay on the edge of the updates and my Linux heritage make me want the more control I can get over my system, so let’s put that apple-ish stuff away and install a new AMP system.

What you’re going to get

At the end you’ll have all your sites hosted in your ~/Sites directory, automatically deployed and served to http://sitename.test where sitename will be the name of the directory (i.e. ~/Sites/sitename) without touching any configuration file, and your local software will be optimized for local development.

Homebrew

If you still doesn’t have Homebrew on your Mac (but you really should), installing it is a matter of going to http://brew.sh and copy&paste the install command:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Homebrew is the APT-like package manager for macOS and you’ll find yourself more and more; to install a package you’ll need a simple brew install; take a look to the man page for other commands.


Source: stock.tookapic.com

MySQL

Installing MySQL via Homebrew is a matter of launching the following commands:

brew install mysql
cp -v $(brew --prefix mysql)/support-files/my-default.cnf $(brew --prefix)/etc/my.cnf

The former will install MySQL, while the latter will copy the sample configuration file into the local etc/ directory

Homebrew software keeps its configuration files in this directory so it does not pollute your system; if you installed Homebrew with the default prefix, the directory will be /usr/local/etc/.

Now you’ll have to configure MySQL to allow for the maximum packet size (do not do this in production!), keep InnoDB tables in separate files — this will keep the size of data files low and make file-based backups, like Time Machine, easier to manage; you can also uncomment the sample option for innodb_buffer_pool_size to improve performance. So, edit the conf file with your preferred editor (which I really hope for you is ViM):

vim $(brew --prefix)/etc/my.cnf
innodb_buffer_pool_size = 128M # uncommented option
max_allowed_packet = 1073741824
innodb_file_per_table = 1

Since some time, Homebrew introduced a good shortcut form managing its services without tapping directly into launchctl, so now you can use the brew services command.

Remember that the brew services commands persist over reboots, so the start really means “start now and at each reboot”. Also, brew services launched as normal user will install services in your ~/Library, while if launched as root will install services in the system’s /Library — this is worth to know if you’re managing a multiuser system.

To have MySQL start now and at reboot, you can issue:

brew services start mysql

By default, MySQL’s root user has an empty password from any connection, you’ll want to fix this launching mysql_secure_installation:

$(brew --prefix mysql)/bin/mysql_secure_installation

Source: unsplash.com

Apache

To get Apache up&running you’ll need to “tap” another repository into Homebrew because the httpd24 package relies on the package homebrew-dupes/zlib.

brew tap homebrew/dupes

The usual way to get PHP over Apache until some time ago would be to install mod_php and forget it; this approach has some downsides like requiring configuration file update on PHP update and sticking to prefork MPM, so I went the PHP-FPM route. PHP-FPM is a standalone PHP process that communicates with Apache over socket or TCP/IP (default port 9000). The advantages you’re getting are:

  1. you don’t have to update Apache configuration file at each PHP update; you don’t even have to reload Apache. As a bonus, you can switch PHP versions on-the-fly without touching Apache.
  2. you can use event or worker Apache MPM, which have better performance over prefork.
  3. you can even switch between Apache and Nginx or other CGI-enabled web servers without keeping a separate PHP configuration for each one.

So, let’s install Apache 2.4 with the event MPM, using Homebrew’s OpenSSL library since it’s more up-to-date than macOS one, and mod_fastcgi to communicate with PHP-FPM:

brew install homebrew/apache/httpd24 --with-brewed-openssl --with-mpm-event

Now you have to enable a couple of modules you’ll need later which are not enabled by default: these are ssl_module, vhost_alias_module, actions_module, and alias_module. Fire up your ViM and uncomment the relevant lines on httpd.conf, and while you’re in, append the lines for loading the file with all the PHP-FPM stuff to the bottom of the file and to load virtual hosts

WARNING: it’s mandatory that you change my home directory with your home directory — if things don’t work, please re-read the foreword about brainless copy&paste.
vim $(brew --prefix)/etc/apache2/2.4/httpd.conf
# Uncomment the lines below
LoadModule ssl_module libexec/mod_ssl.so
LoadModule vhost_alias_module libexec/mod_vhost_alias.so
LoadModule actions_module libexec/mod_actions.so
LoadModule alias_module libexec/mod_alias.so
LoadModule proxy_module libexec/mod_proxy.so
LoadModule proxy_fcgi_module libexec/mod_proxy_fcgi.so
# Append the following at the bottom of the file
# 1. change /usr/local with your `brew --prefix` if you
# didn't install it in the default location
# 2. change /Users/ciromattia to your $HOME
# Include PHP conf
Include /usr/local/etc/apache2/2.4/extra/httpd-php.conf
# Include our VirtualHosts
Include /Users/ciromattia/Sites/httpd-vhosts.conf

To make Apache send all PHP to PHP-FPM you’ll use mod_proxy_fcgi and you’ll configure everything into a separate file to have more separation of concerns. Again, you’ll have to replace every /Users/ciromattia instance with your home directory (i.e. `echo $HOME`)

vim $(brew --prefix)/etc/apache2/2.4/extra/httpd-php.conf
<IfModule proxy_fcgi_module>
## Define FilesMatch in each vhost
<IfModule mod_mime.c>
AddHandler application/x-httpd-php .php .php5 .phtml
AddHandler application/x-httpd-php-source .phps
</IfModule>
DirectoryIndex index.php index.phtml
# Defining a worker will improve performance
# And in this case, re-use the worker (dependent on support from the fcgi application)
# If you have enough idle workers, this would only improve the performance marginally
<Proxy "fcgi://localhost:9500/" enablereuse=on max=10>
</Proxy>
# Add the following if you're using Apache 2.4.26+
ProxyFCGIBackendType GENERIC
<FilesMatch "\.php$">
# Pick one of the following approaches
# Use the standard TCP socket
#SetHandler "proxy:fcgi://localhost/:9500"
# If your version of httpd is 2.4.9 or newer (or has the back-ported feature), you can use the unix domain socket
SetHandler "proxy:unix:/usr/local/var/run/php-fpm.www.sock|fcgi://localhost:9500"
</FilesMatch>
</IfModule>

The VirtualHosts configuration will be, as mentioned, in ~/Sites/httpd-vhosts.conf, but Apple doesn’t ship ~/Sites anymore so you’ll have to create it by yourself, along with directories for logs and SSL:

mkdir -p ~/Sites/{logs,ssl}

Let’s go on populating ~/Sites/httpd-vhosts.conf. The point here is making the webserver listen on 8080/8443 ports (the latter is for SSL); if you’re thinking of replacing with well-known ports (80/443) please stop and follow along: I’ll use port forwarding to automatically forward (i.e. “translate”) the well-known ports to the unprivileged ones.
If you’re wandering why add this layer of complexity, here’s your answer: this allows you to run Apache as unprivileged user, which in the short term makes you able to start and stop it without sudo and in the long run is compliant with the Principle of Least Privilege.
In the configurationg you’ll find also:

  • a basic SSL configuration (with self-signed certificate)
  • an example of declared VirtualHost (should the automatic one fail)

Once again, replace /Users/ciromattia with your $HOME.

touch ~/Sites/httpd-vhosts.conf
vim ~/Sites/httpd-vhosts.conf
#
# Listening ports.
#
#Listen 8080 # defined in main httpd.conf
Listen 8443
#
# Set up permissions for VirtualHosts in ~/Sites
#
<Directory "/Users/ciromattia/Sites">
Options Indexes FollowSymLinks MultiViews
AllowOverride All
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Allow from all
</IfModule>
</Directory>
# For http://localhost in the users' Sites folder
<VirtualHost _default_:8080>
ServerName localhost
DocumentRoot "/Users/ciromattia/Sites"
</VirtualHost>
<VirtualHost _default_:8443>
ServerName localhost
Include "/Users/ciromattia/Sites/ssl/ssl-shared-cert.inc"
DocumentRoot "/Users/ciromattia/Sites"
</VirtualHost>
#
# VirtualHosts
#
## Manual VirtualHost template for HTTP and HTTPS
#<VirtualHost *:8080>
# ServerName project.test
# CustomLog "/Users/ciromattia/Sites/logs/project.test-access_log" combined
# ErrorLog "/Users/ciromattia/Sites/logs/project.test-error_log"
# DocumentRoot "/Users/ciromattia/Sites/project.test"
#</VirtualHost>
#<VirtualHost *:8443>
# ServerName project.test
# Include "/Users/ciromattia/Sites/ssl/ssl-shared-cert.inc"
# CustomLog "/Users/ciromattia/Sites/logs/project.test-access_log" combined
# ErrorLog "/Users/ciromattia/Sites/logs/project.test-error_log"
# DocumentRoot "/Users/ciromattia/Sites/project.test"
#</VirtualHost>
#
# Automatic VirtualHosts
#
# A directory at /Users/ciromattia/Sites/sitename
# can be accessed at http://sitename.test
# This log format will display the per-virtual-host as the first field followed by a typical log line
LogFormat "%V %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combinedmassvhost
# Auto-VirtualHosts with .test
<VirtualHost *:8080>
ServerName test
ServerAlias *.test
CustomLog "/Users/ciromattia/Sites/logs/test-access_log" combinedmassvhost
ErrorLog "/Users/ciromattia/Sites/logs/test-error_log"
VirtualDocumentRoot /Users/ciromattia/Sites/%-2+
</VirtualHost>
<VirtualHost *:8443>
ServerName test
ServerAlias *.test
Include "/Users/ciromattia/Sites/ssl/ssl-shared-cert.inc"
CustomLog "/Users/ciromattia/Sites/logs/test-access_log" combinedmassvhost
ErrorLog "/Users/ciromattia/Sites/logs/test-error_log"
VirtualDocumentRoot /Users/ciromattia/Sites/%-2+
</VirtualHost>

To get Apache enable SSL (or simply not whining about missing files) you have to create the SSL configuration file and the certificate; again, you have to replace… yeah, you got it, right?

vim ~/Sites/ssl/ssl-shared-cert.inc
SSLEngine On
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW
SSLCertificateFile "/Users/ciromattia/Sites/ssl/selfsigned.crt"
SSLCertificateKeyFile "/Users/ciromattia/Sites/ssl/private.key"

Generate your certificate:

openssl req \
-new \
-newkey rsa:2048 \
-days 3650 \
-nodes \
-x509 \
-subj "/C=US/ST=State/L=City/O=Organization/OU=$(whoami)/CN=*.test" \
-keyout ~/Sites/ssl/private.key \
-out ~/Sites/ssl/selfsigned.crt

Great, the though part is over, start your Apache with Homebrew services:

brew services restart httpd24

Port Forwarding

Now, let’s go back to that port forwarding topic we discussed earlier. At the moment your server is answering to something like http://mygreatsite.test:8080 or https://mygreatsite.test:8443 which is less than ideal, so you can create a little .plist script that will be executed at root and will forward all request on port 80 to 8080 and on port 443 to 8443, so you’ll be able to user addresses like http://mygreatsite.test and still get what you payed for.
You’ll have to create the file /Library/LaunchDaemons/it.winged.httpdfwd.plist as root, since it needs elevated privileges:

vim /Library/LaunchDaemons/it.winged.httpdfwd.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>it.winged.httpdfwd</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>echo "rdr pass proto tcp from any to any port {80,8080} -> 127.0.0.1 port 8080" | pfctl -a "com.apple/260.HttpFwdFirewall" -Ef - &amp;&amp; echo "rdr pass proto tcp from any to any port {443,8443} -> 127.0.0.1 port 8443" | pfctl -a "com.apple/261.HttpFwdFirewall" -Ef - &amp;&amp; sysctl -w net.inet.ip.forwarding=1</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>root</string>
</dict>
</plist>

To load without logging out and back in or rebooting, run launchctl:

sudo launchctl load -Fw /Library/LaunchDaemons/it.winged.httpdfwd.plist

Photo by Suvan Chowdhury

PHP

I use PHP 7.0 so the following is based on it, but you can easily swap version numbers to get 5.3, 5.4 or 5.6. At the moment of writing there’s also PHP 7.1 on Homebrew repositories but it doesn’t play nice with FPM stuff, so you’ll better stick with 7.0 (or reply with instructions on how to make it work).

Homebrew allows you also to keep multiple PHP versions and switching on the fly is a no-brainer with FPM configuration (sidenote: you don’t have to use brew-php-switcher or similar stuff because they assume you embraced the mod_php way).

So, install PHP along with Opcache extension, which will speed up your development environment, Xdebug extension, which I use along with PhpStorm, and MCrypt.

brew install homebrew/php/php71
brew install php71-xdebug php71-mcrypt

Here you are the settings I usually change, feel free to edit yours as you like, keeping in mind that you need to define the date.timezone and the error_log, which are not set by default, and to enable opcache.

vim $(brew --prefix)/etc/php/7.1/php.ini
date.timezone = "Europe/Rome"
memory_limit = 512M
post_max_size = 80M
upload_max_filesize = 64M
default_socket_timeout = 600
max_execution_time = 300
max_input_time = 600
error_log = /Users/ciromattia/Sites/logs/php-error_log

To make PHP talk with Apache via socket you have to change also the listen parameter of php-fpm configuration:

vim $(brew --prefix)/etc/php/7.1/php-fpm.d/www.conf
listen = /usr/local/var/run/php-fpm.www.sock

Note: if you’re using php5.6 the listen option is in $(brew — prefix)/etc/php/5.6/php-fpm.conf file.

For XDebug you’ll need to edit a separate configuration file — as usual, edit it accordingly to your personal needs.

vim $(brew --prefix)/etc/php/7.1/conf.d/ext-xdebug.ini
[xdebug]
zend_extension="/usr/local/opt/php71-xdebug/xdebug.so"
xdebug.remote_enable=1
xdebug.remote_port="9005"
xdebug.profiler_enable=1
xdebug.profiler_output_dir="/tmp"
xdebug.idekey=PHPSTORM

Finally, start PHP-FPM:

brew services start php71

Photo by Thomas Vanhaect

DNSMasq

DNSMasq is the last puzzle piece that will route every request to a .test domain to localhost (127.0.0.1), so go on installing it and creating and populating it’s configuration file:

brew install dnsmasq
vim $(brew --prefix)/etc/dnsmasq.conf
address=/.test/127.0.0.1
listen-address=127.0.0.1
port=35353

Use once again brew services to start DNSMasq:

brew services start dnsmasq

macOS can be configured to use multiple DNS based on domain name (you can type “man 5 resolver” to get much more details), so let’s configure it to use localhost (i.e. DNSMasq) for DNS queries ending in .test:

sudo mkdir /etc/resolver
sudo vim /etc/resolver/test
nameserver 127.0.0.1
port 35353

It should work out-of-the-box; to test it use the command ping -c 3 mygreatsite.test and it should return results from 127.0.0.1. If it doesn’t work, try turning WiFi off and on or reboot your system.


Source: pixabay.com

Nginx anyone?

I know, I mentioned Nginx in the title, and I did because if it’s so simple to switch PHP versions it should be so simple to swap web servers too, you may wonder.
The answer is yes, and it involves installing nginx: you can choose the plain one bundled in Homebrew or you can tap homebrew-nginx if you need some custom modules.

brew install nginx

The default configuration file on macOS Sierra defines a directory to add supplementary configuration files, so leave the main conf file alone and create and populate /usr/local/etc/nginx/servers/test.conf.

vim $(brew --prefix)/etc/nginx/servers/test.conf
server {
listen 8080;
listen 8443 ssl;
server_name ~^(?<sname>.+?).test$;
root /Users/ciromattia/Sites/$sname;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
access_log /Users/ciromattia/Sites/logs/$sname-access.log;
error_log /Users/ciromattia/Sites/logs/combined-error.log debug;
ssl_certificate /Users/ciromattia/Sites/ssl/selfsigned.crt;
ssl_certificate_key /Users/ciromattia/Sites/ssl/private.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 10m;
sendfile off;
location ~ \.php$ {
try_files $uri = 404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}

Now it’s only a matter of (optionally) stopping Apache and starting Nginx with our beloved brew services:

brew services stop httpd24
brew services start nginx

Celebrate!

Yay! You got a local development environment that allows you to simply create a directory into ~/Sites/ and have it automatically deployed to http://directory.test.
If you get stuck or find something missing feel free to drop me a line!

Logs

Oh, one last advice: you can find the logs (and you’ll need them!) in the following locations:

  • Apache/Nginx: /usr/local/var/log/apache2/error_log, /usr/local/var/log/nginx/error.log and ~/Sites/logs/
  • PHP-FPM: /usr/local/var/log/php-fpm.log
  • MySQL: /usr/local/var/mysql/$(hostname).err