A few years ago I had written a blog post on setting up an Apache, MySQL and PHP web server on macOS. Many things have changed ever since and that tutorial became impossible to follow. Meanwhile my standard fallback local server, MAMP, has become too unstable and lagging behind the times to be practical. Other blog posts I read seem to be glossing over some details. So here you go, a tutorial on running a local Apache, MySQL, multiple PHP versions server on macOS Mojave using HomeBrew, updated for 2019. Bonus points: you can change the PHP version using the site’s .htaccess like you would on most live hosts.

Before proceeding, I’d like to explain why I got into all this trouble instead of using MAMP. In fact, I was using MAMP Pro. It was slow, idiosyncratic and randomly crashing in ways that required me to log out or, worse, restart my Mac. It is also using outdated versions, it’s too expensive for the glorified interface to free (and free of charge!) software that it is and its support was utterly useless. So I tried building my own local server following someone else’s tutorials written for macOS High Sierra but found out that Homebrew was now subtly broken in macOS Mojave. No problem, I am used to fixing broken stuff and writing step-by-step instructions so other people don’t have to go through the same pain. So here you are: a massive blog post detailing everything I’ve done to set up a stable, reliable local server with all the bells and whistles on two different Macs.

Our goal

We are going to install a local server with Apache, MySQL 5.7 and three versions of PHP (5.6, 7.2 and 7.3). We’ll also install Redis.

Our sites will be served from the Sites folder inside the user’s home directory.

The Apache server will be running as our user. PHP is also running as our user. This is great for local development.

We are going to use a self-signed SSL certificate, similar to what I described in my post on forging SSL certificates yourself, adapted for the Mac.

PHP will be working through PHP-FPM (FastCGI Process Manager). This is the most well performing, stable and recommended way to run PHP. It also allows us to swap PHP versions using .htaccess just like we’d do on most live hosts.

If you’ve used my previous tutorials for macOS, Windows or Linux you’ll probably notice that I used to have a different domain name for each PHP version. I no longer do that because of WordPress. WordPress hard-codes the URL to the site in several places, making it very painful to switch to a different PHP version using domain names. The .htaccess method is far easier. As a bonus, the plumbing that goes into supporting it makes PHP even faster than my multiple domains method!

Before you begin

This tutorial is meant to be followed by developers, be it front-end or backend. Some familiarity with the command line is appreciated but not required. At the very least you should not be afraid to use Terminal.

Now would also be a great time to take a backup of your Mac, e.g. using Time Machine, in case you break something.

Install XCode

We need a working compilation toolchain to continue. This is used by HomeBrew itself in some cases (more about HomeBrew later) and also when we need to compile some PHP extensions which are not bundled with the HomeBrew package. For this you need XCode. You can install XCode from the App Store. It’s free of charge. XCode is Apple’s all-in-one development package which includes system headers and the all important build toolchain (C compiler, automake, autoconf and so on and so forth).

Now open XCode and accept the license. It will take a while. After it’s done it will open the XCode app itself. We don’t need the actual application running. You can quit it.

Now, we need to install the command line tools and development headers for our version of macOS. This is something necessary from macOS Mojave onwards. They are no longer installed on the system when you install XCode. So let’s launch the package which does the installation:

open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg

Readers with other macOS versions: 10.14 is the version number for macOS Mojave. Substitute your macOS version number.

Install HomeBrew

As I’ve done in my previous tutorials for the Mac, I use Homebrew. If you’re new to macOS, Homebrew is the de facto package manager for macOS. It allows us to install and upgrade libraries and tools in a simple and sensible manner. Follow the instructions on the link above to install Homebrew.

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

Update HomeBrew

Open a Terminal and enter the following.

brew update
brew upgrade

This will tell HomeBrew to update itself and install updates to things already installed (in case you had already installed HomeBrew).

Disable your existing local server

If you are using MAMP, XAMPP or any other kind of local development server you need to shut it down now. If you already have sites installed on it remember to back them up. Once we’re done the old local server will no longer work.

If you were using MAMP and you had selected the “Make this version available on the command line” option in its interface you will need to edit the ~/.profile file to remove MAMP from the PATH.

If you are using a local server that’s based on virtualization (e.g. Local by Flywheel, Docker, vvv, Vagrant and so on) you need to shut these down too.

Quit Skype if you’re using it. Sometimes it binds to port 80 which is normally used by the web server. It’s annoying and will cause some false starts and a lot of frustration.

Installing Apache, MySQL and PHP

In this section we are going to do the initial installation and basic configuration of Apache, MySQL, three versions of PHP and Redis. At the end of this section everything will be working on its own but will not yet be set up to work together.

Install Apache

Installing Apache with HomeBrew is simple.

brew install httpd
brew link httpd
brew services start httpd

The first line installs Apache (traditionally called httpd, short for “http daemon” where “daemon” is how UNIX systems called background server processes). The second line creates symlinks so we can access Apache’s executables from the command line. The third line installs and starts a system service which will be launching Apache when we restart our Mac.

Necessary Apache fix

Since Apache runs as our own user, not root, we need to set up the directory where it stores its process identified file to be writeable by our own user. Otherwise Apache will start… but not really.

BTW, even if we didn’t change the ownership of the Apache process we’d need to create and set up the correct ownership of that folder anyway. It looks like Homebrew on Mojave makes some really weird choices which result in some servers not really working out of the box.

Here’s what you need to do

sudo mkdir /usr/local/var/run/httpd
sudo chown -R $(whoami):staff /usr/local/var/run/httpd

Change the Apache root to ~/Sites

As we discussed, we want Apache to look for our sites in the Sites folder under our user’s folder. Let’s create the folder first.

mkdir ~/Sites
cd ~/Sites
pwd

The last command prints out something like

/Users/nicholas/Sites

Note this down. This is the new Apache document root we need to configure in the next step. From the Terminal run the following.

open /usr/local/etc/httpd/httpd.conf -a TextEdit

It opens Apache’s configuration file in TextEdit.

Find the lines

DocumentRoot "/usr/local/var/www"
<Directory "/usr/local/var/www">

and change them to read

DocumentRoot "/Users/nicholas/Sites"
<Directory "/Users/nicholas/Sites">

Where /Users/nicholas/Sites is the new Apache document root you noted down above when you ran the pwd command.

Save & close the file. Then restart the Apache server.

brew services restart httpd

Change Apache port and user, allow .htaccess files

By default Apache is set up to use port 8080 and a user and user group called _www. Also, it’s set up to not allow .htaccess files to override any settings. None of that helps if you want to do anything useful on your local server. We’re going to set up Apache to use port 80, your own user and allow .htaccess files.

From the Terminal run the following.

whoami
open /usr/local/etc/httpd/httpd.conf -a TextEdit

The first line prints out your username, e.g. nicholas. Note it down.

The second line opens Apache’s configuration file in TextEdit.

Find the line

Listen 8080

and change it to

Listen 80

Find the lines

User _www
Group _www

and change them to

User nicholas
Group staff

Remember that nicholas in the example above should be replaced with your username, the one you found by running whoami above.

Now find the lines

#
# AllowOverride controls what directives may be placed in .htaccess files.
# It can be "All", "None", or any combination of the keywords:
# AllowOverride FileInfo AuthConfig Limit
#
AllowOverride None

and change them to

#
# AllowOverride controls what directives may be placed in .htaccess files.
# It can be "All", "None", or any combination of the keywords:
# AllowOverride FileInfo AuthConfig Limit
#
AllowOverride All

Watch out! There are two instances of the AllowOverride None line but only one preceded by the comment block (the lines starting with #) above. You must only change the line preceded by the comment block, not the other one.

Save & close the file. Then restart the Apache server.

brew services restart httpd

At this point if you go to http://localhost you should see an error telling you the page is not found. If you any other error (like, the server is not found or not responding) you did something wrong. Go back and trace your steps. You probably forgot to create the /usr/local/var/run/httpd folder or give it the correct permissions.

Install MySQL

Likewise for MySQL 5.7.

brew install mysql@5.7
brew link mysql@5.7 --force
brew services start mysql@5.7

The –force part of the second line is mandatory. It has to do with how HomeBrew works. If you omit it, it will complain that it can not link MySQL.

At this point your MySQL database only has one user called root with an empty password. You can use these credentials to connect to your database with a database administration tool such as Sequel Pro (free of charge). It is also recommended that you use Sequel Pro’s “Users” tool to change the password of the root user. Trust me, this will save you from an embarrassing moment down the line.

Install PHP

Next up, we’re going to install all three PHP versions we are interested in:

brew install php@5.6
brew install php@7.2
brew install php@7.3

The way macOS Mojave ships, some of the directories HomeBrew needs to write files to are not writeable. We can fix that with the following commands. macOS will ask you to enter your password before running them; this is normal.

sudo chown -R $(whoami):staff /usr/local/include/php/
sudo chown -R $(whoami):staff /usr/local/lib/php/
sudo chown -R $(whoami):staff /usr/local/etc/php/

Now we can set up PHP 7.3 as the default PHP version on our Mac:

# Set PHP 7.3 as the default PHP version
brew unlink php
brew link --overwrite --force php@7.3

PHP necessary fix #1: configuration files

Here’s a small problem. For some strange reason HomeBrew “forgets” to copy the configuration files to a place where each PHP version can find them. We have to fix that ourselves.

# Install configuration files for PHP 5.6
cd $(brew --prefix php@5.6)
cp -R .bottle/* /usr/local/
# Install configuration files for PHP 7.2
cd $(brew --prefix php@7.2)
cp -R .bottle/* /usr/local/
# Install configuration files for PHP 7.3
cd $(brew --prefix php@7.3)
cp -R .bottle/* /usr/local/

PHP necessary fix #2: PHP-FPM listening ports

All PHP versions are set up to start a PHP-FPM (PHP FastCGI Process Manager) server listening to the same port, 9000. This can not work, of course. Moreover, we really need port 9000 available for XDebug. Thus, we are going to have each PHP-FPM version listen to a different port: 9056 for PHP 5.6, 9072 for PHP 7.2 and 9073 for PHP 7.3. Yes, the convention is 90 followed by the PHP version without the dot. It’s easier to remember that way.

Moreover, while we’re at it, we’re going to have PHP run under our user and the built-in user group staff.

For PHP 5.6 run

open /usr/local/etc/php/5.6/php-fpm.conf -a TextEdit

This opens a file in TextEdit. Find the lines:

user = _www
group = _www

and change them to

user = nicholas
group = staff

where, of course, nicholas is your username. If you are not sure what is your username run whoami in the Terminal.

A little further down you’ll find

listen = 127.0.0.1:9000

Change this to

listen = 127.0.0.1:9056

Save and close the file.

For PHP 7.2 run

open /usr/local/etc/php/7.2/php-fpm.d/www.conf -a TextEdit

Make the same changes as for PHP 5.6 with one difference. The listen line should read:

listen = 127.0.0.1:9072

For PHP 7.3 run

open /usr/local/etc/php/7.3/php-fpm.d/www.conf -a TextEdit

Make the same changes as for PHP 7.2 with one difference. The listen line should read:

listen = 127.0.0.1:9073

Start the PHP-FPM services

Now we can install and start the PHP-FPM system services for all three PHP versions we have installed.

brew services start php@5.6
brew services start php@7.2
brew services start php@7.3

Install Redis

Finally, we can install Redis.

brew install redis
brew link redis
brew services start redis

Getting PHP to work with Apache

Right now we have PHP-FPM running, we have Apache running but they don’t talk to each other. As a result, you can’t run PHP scripts from Apache. We need to fix this. As we’ve done before run the following on a Terminal to open Apache’s configuration file.

open /usr/local/etc/httpd/httpd.conf -a TextEdit

You need to find the following lines (they are non-contiguous) and remove the # in front of them:

#LoadModule deflate_module lib/httpd/modules/mod_deflate.so
#LoadModule mime_magic_module lib/httpd/modules/mod_mime_magic.so
#LoadModule expires_module lib/httpd/modules/mod_expires.so
#LoadModule proxy_module lib/httpd/modules/mod_proxy.so
#LoadModule proxy_http_module lib/httpd/modules/mod_proxy_http.so
#LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so
#LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so

The proxy_* lines are necessary to run PHP at all. The other lines enable features commonly used by PHP CMS, e-commerce, forum etc scripts. They are typically enabled on live hosts so we need to enable on our local host to have a similar environment.

Now find the line

<Directory "/Users/nicholas/Sites">

where “nicholas” is your username. You may remember that we found this earlier by running whoami on a Terminal. Add the following after it

    <FilesMatch "\.php$">
        SetHandler "proxy:fcgi://localhost:9073/"
    </FilesMatch>

This may look like black magic so let’s explain this a bit. We are telling Apache that any file inside the directory /Users/nicholas/Sites which matches the regular expression \.php$ (its name ends in .php) is to be handled by the FastCGI proxy and forwarded to port 9073 on our local machine. Remember when we set up the PHP versions we configured a different port for each PHP-FPM version; 9073 is the one for PHP 7.3’s PHP-FPM. So we have effectively told Apache to parse all PHP files with PHP 7.3 by default.

Save and close the file, then restart the Apache server.

brew services restart httpd

We are going to create a new test file to make sure PHP is working:

cat << EOL > ~/Sites/test.php
<?php phpinfo();
EOL

Now access http://localhost/test.php on your browser. You should see PHP’s information page saying that you are running PHP 7.3.0 (or whatever PHP version got installed).

But I promised you different PHP versions, didn’t I? Let switch to PHP 5.6. We will create a .htaccess file inside the ~/Sites folder which does exactly that.

cat << EOL > ~/Sites/.htaccess
<FilesMatch "\.php$">
  SetHandler "proxy:fcgi://localhost:9056/"
</FilesMatch>
EOL

Reload the page. Now the PHP version is 5.6.39! These three magic lines told Apache to substitute the default proxying of .php files through PHP 7.3 with proxying through PHP 5.6.

Likewise for PHP 7.2:

cat << EOL > ~/Sites/.htaccess
<FilesMatch "\.php$">
SetHandler "proxy:fcgi://localhost:9072/"
</FilesMatch>
EOL

Reload the page, now you get PHP 7.2. If you were to remove the .htaccess

rm ~/Sites/.htaccess

and reload the page you are back to PHP 7.2. Isn’t that cool? Much easier than MAMP’s user-hostile set-up-the-host-again-and-wait-forever-to-restart-all-servers approach. We’re cooking with gas, folks!

If you’re happy accessing sites as subdirectories of http://localhost you are done. If you want to add that special extra sauce to make things smoother for your development experience keep reading!

One subdomain per site

For the longest time I’ve used the convention of the domain name foobar.local.web being used to access a site sitting in the foobar folder on my local server. This preserves my sanity since I can easily correlate the site I’m working on with the folder I’m looking at. Ever since I found out about Apache’s mod_vhost_alias I am using it to easily create this kind of subdomain hosting without having to reconfigure Apache for each site I add to the server.

Setting up the subdomain resolution

First of all, we need to tell macOS about our sites’ subdomains. Since there is no public DNS server matching them with IP addresses we need to edit macOS’ /etc/hosts file. The easiest tool to do that is GasMask.

After installing GasMask click on its icon which, unsurprisingly, looks like a gas mask and click on Show Editor to display the main wondow. From the top toolbar choose Create, Local and name the new file My Sites. Click on it and then click on Activate on the toolbar to activate this file.

On the right hand editor you can set up the IP to hostname assignment. The IP is the first thing you enter. Then you can enter one or more hostnames separated by spaces. For example:

127.0.0.1  www.local.web
127.0.0.1  wordpress.local.web joomla.local.web
127.0.0.1  client1.local.web client2.local.web client3.local.web

You get the idea. Save and close that file but do not quit GasMask.

Tip: With the GasMask editor open go to GasMask, Preferences from the top menu, click on General and activate Open at Login. Otherwise you need to run GasMask every time you want to use your local web server.

Set up Apache with virtual host aliasing

As we’ve done before run the following on a Terminal to open Apache’s configuration file.

open /usr/local/etc/httpd/httpd.conf -a TextEdit

You need to find the following line

#LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so

and remove the # in front so it now reads

LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so

Find these lines

# Virtual hosts
#Include /usr/local/etc/httpd/extra/httpd-vhosts.conf

and append one more line so they now read

# Virtual hosts
#Include /usr/local/etc/httpd/extra/httpd-vhosts.conf
Include /usr/local/etc/httpd/extra/local.web.conf

This tells Apache to load one more file which contains the configuration for our local.web sites. Save the file and close TextEdit.

Back in the Terminal enter the following

touch /usr/local/etc/httpd/extra/local.web.conf
open /usr/local/etc/httpd/extra/local.web.conf -a TextEdit

This opens a new, empty file. Paste the following:

<VirtualHost *:80>
    ServerAdmin webmaster@local.web
    DocumentRoot "/Users/nicholas/Sites"
    ServerName local.web
    ServerAlias www.local.web
    #ErrorLog "/usr/local/var/log/httpd/local.web.error_log"
    #CustomLog "/usr/local/var/log/httpd/local.web.access_log" common
    
    <Directory "/Users/nicholas/Sites">
        AllowOverride All
        DirectoryIndex index.html index.php
        Require all granted
        
        #See https://wiki.apache.org/httpd/PHP-FPM
        <FilesMatch "\.php$">
            SetHandler "proxy:fcgi://127.0.0.1:9073/"
        </FilesMatch>
    </Directory>
</VirtualHost>

<VirtualHost *:80>
    ServerAdmin webmaster@local.web
    ServerAlias *.local.web
    UseCanonicalName Off
    VirtualDocumentRoot "/Users/nicholas/Sites/%1"
    
    #ErrorLog "/usr/local/var/log/httpd/local.web.error_log"
    #CustomLog "/usr/local/var/log/httpd/local.web.access_log" common
    
    <Directory "/Users/nicholas/Sites">
        AllowOverride All
        DirectoryIndex index.html index.php
        Require all granted
        
        #See https://wiki.apache.org/httpd/PHP-FPM
        <FilesMatch "\.php$">
            SetHandler "proxy:fcgi://127.0.0.1:9073/"
        </FilesMatch>
    </Directory>
</VirtualHost>

There are only two things you need to be careful about:

  • /Users/nicholas/Sites is the path to the new Apache document root, as you have set it up when installing Apache earlier.
  • 9073 is the port that PHP-FPM listens to. Remember, 9073 is for PHP 7.3. If you want a different default PHP version change that.

Afterwards, restart Apache

brew services restart httpd

Both http://localhost and http://www.local.web display the same pages now. If you were to create a folder called client1 in the Sites folder under your user directory you’d be able to access its contents as any of the following:

  • http://client1.local.web (preferred)
  • http://localhost/client1 (traditional)
  • http://www.local.web/client1 (not recommended)

Enable XDebug

No PHP development environment is complete without XDebug, the most popular PHP debugger. Unfortunately, XDebug does not ship with HomeBrew’s PHP so we need to install it with PECL. To complicate the matters more, we need to do this for all installed PHP versions. For each version this requires a small dance of unlinking PHP, relinking it, installing XDebug and then again unlinking PHP and relinking the default version. Are you dizzy yet?

After we’re done we’ll have installed XDebug on all three versions of PHP. XDebug remote debugging will be enabled but not started by default. You will need to use the IDE key PHPSTORM to do that. The easiest way is through bookmarklets created with PhpStorm’s bookmarklet generator.

XDebug for PHP 5.6

brew unlink php
brew link --force --overwrite php@5.6
pecl install xdebug-2.5.5
brew unlink php@5.6
brew link --force --overwrite php@7.3

If you got any errors during the pecl install stage please refer to my blog post on compiling PHP on macOS and follow the “Prerequisites” section only, then retry the commands above.

When you ran the pecl install command it printed a lot of stuff ending with something like this

You should add "zend_extension=/usr/local/Cellar/php@5.6/5.6.39/pecl/20131226/xdebug.so" to php.ini

Note down the part in bold letters. This will be different for each PHP version. You need to paste it below.

Now we need to edit PHP 5.6’s php.ini and tell it to load XDebug:

open /usr/local/etc/php/5.6/php.ini -a TextEdit

At the bottom of the file, right above the “; Local Variables:” line add the following:

[xdebug]
zend_extension=/usr/local/Cellar/php@5.6/5.6.39/pecl/20131226/xdebug.so
xdebug.remote_enable=1
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.remote_autostart=0
xdebug.idekey=PHPSTORM

Again, the line in bold corresponds to the bold part of the message I told you to copy.

Finally, restart PHP-FPM for PHP 5.6

brew services restart php@5.6

XDebug for PHP 7.2

brew unlink php
brew link --force --overwrite php@7.2
pecl install xdebug
brew unlink php@7.2
brew link --force --overwrite php@7.3

When you ran the pecl install command it printed a lot of stuff ending with something like this

You should add "zend_extension=/usr/local/Cellar/php@7.2/7.2.13/pecl/20170718/xdebug.so" to php.ini

Note down the part in bold letters. This will be different for each PHP version. You need to paste it below.

As before, we need to edit PHP 7.2’s php.ini and tell it to load XDebug:

open /usr/local/etc/php/7.2/php.ini -a TextEdit

At the bottom of the file, right above the “; Local Variables:” line add the following:

[xdebug]
zend_extension=/usr/local/Cellar/php@7.2/7.2.13/pecl/20170718/xdebug.so
xdebug.remote_enable=1
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.remote_autostart=0
xdebug.idekey=PHPSTORM

Finally, restart PHP-FPM for PHP 7.2

brew services restart php@7.2

XDebug for PHP 7.3

Since PHP 7.3 is the default version we don’t need to go through the unlink/relink dance. However, at the time of this writing there is no stable version of XDebug supporting PHP 7.3. Therefore we have to explicitly tell it to use the latest beta version compatible with PHP 7.3.

pecl install xdebug-2.7.0beta1

When you run this command it prints a lot of stuff ending with something like this

You should add "zend_extension=/usr/local/Cellar/php/7.3.0/pecl/20180731/xdebug.so" to php.ini

Note down the part in bold letters. This will be different for each PHP version. You need to paste it below.

As before, we need to edit PHP 7.3’s php.ini and tell it to load XDebug:

open /usr/local/etc/php/7.3/php.ini -a TextEdit

At the bottom of the file, right above the “; Local Variables:” line add the following:

[xdebug]
zend_extension=/usr/local/Cellar/php/7.3.0/pecl/20180731/xdebug.so
xdebug.remote_enable=1
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.remote_autostart=0
xdebug.idekey=PHPSTORM

Finally, restart PHP-FPM for PHP 7.3

brew services restart php@7.3

Install MailHog

MailHog is a small application which intercepts email sent out of your sites and keeps it locally. You can use a web interface to review the mail. This comes in handy when testing the email features of the sites you are building without risking any email accidentally escaping to the wild.

We can install it through HomeBrew

brew install mailhog
brew services start mailhog

Your Mac will ask you if you want to allow incoming connections from MailHog. Allow them.

Every time your Mac starts so will MailHog. It is set up by default with

  • the SMTP server on port 1025
  • the HTTP server on port 8025 (this means you can see the intercepted e-mails aby pointing your browser to http://localhost:8025/)
  • in-memory message storage
  • a sendmail-compatible script at /usr/local/bin/mhsendmail

You can use it from PHP in two ways. For scripts which allow you to set up your own mail settings (e.g. Joomla!, Akeeba Solo etc) just tell them to use SMTP on localhost, port 1025 without authentication. For scripts which go through PHP’s mail() function (e.g. WordPress) you need to change your php.ini file’s sendmail_path line to

sendmail_path = /usr/local/bin/mhsendmail

I explain where php.ini files are and how to change them below.

Alternatively, create a file named .user.ini (note the dot in front!) in your site’s root directory and put that line there. Note that it may take up to 5′ for the new settings to activate.

PHP configuration

The default PHP configuration is not very good if you’re planning or running most well known PHP CMS, e-commerce etc scripts on your local host. You need to modify them.

First, you should know their location:

  • /usr/local/etc/php/5.6/php.ini for PHP 5.6
  • /usr/local/etc/php/7.2/php.ini for PHP 7.2
  • /usr/local/etc/php/7.3/php.ini for PHP 7.3

You can open them for editing with a command like

open /usr/local/etc/php/5.6/php.ini -a TextEdit

in Terminal.

Here are the lines I tend to change on my local server (they are not consecutive in the php.ini file)

; Reload the .user.ini file every 60 seconds instead of every 5'
user_ini.cache_ttl = 60
; Log errors
error_log = /usr/share/var/log/php-error.log
; Maximum POST size 20M. Must be at least 1.5x larger than upload_max_filesize
post_max_size = 20M
; Maximum upload file size 10M. Don't set it lower than 5M.
upload_max_filesize = 10M
; Default timezone settings. I use settings for Nicosia, Cyprus.
date.timezone = Asia/Nicosia
date.default_latitude = 35.185566
date.default_longitude = 33.382275
; Write to PHAR files. I need this because I build PHAR files.
phar.readonly = Off
; I use MailHog for catching e-mail
smtp_port = 1025
sendmail_path = /usr/local/bin/mhsendmail
; Enable OPcache with conservative, developer-friendly settings
opcache.enable=1
opcache.enable_cli=1
opcache.max_accelerated_files=10000
opcache.use_cwd=1
opcache.validate_timestamps=1
opcache.revalidate_freq=5
opcache.save_comments=1
opcache.load_comments=1
opcache.enable_file_override=1
opcache.dups_fix=1
opcache.error_log=/usr/local/var/log/php-opcache-error.log
; XDebug settings, as we explained earlier
xdebug.remote_enable=1
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.remote_autostart=0
xdebug.idekey=PHPSTORM

After changing the PHP configuration remember to restart the PHP-FPM service for that PHP version, e.g.

brew services restart php@5.6

Creating and installing an SSL certificate

Most people think of HTTPS as something you need only on live servers, to protect the privacy of the data exchanged between your server and your visitors’ browsers. However, there are some newer web APIs which only work on HTTPS sites for security reasons, for example FIDO U2F (two step verification with hardware security keys). Or you may want to test if your site works when you use HTTPS Everywhere, a browser plugin by the Electronic Frontier Foundation which enforces the use of HTTPS no matter what the site says. Generally, HTTPS is a very useful thing to have on your local development server.

Creating a custom certificate

We will create a working directory where we will create a certification authority and a star certificate. Open a Terminal and let’s get to work.

mkdir -p /usr/local/opt/ca
cd /usr/local/opt/ca
mkdir -p certs crl newcerts private
mkdir -p intermediate/certs intermediate/crl intermediate/newcerts intermediate/private intermediate/csr

Next up, we will create some files required by OpenSSL to create certificates

touch index.txt
echo 1000 > serial
touch intermediate/index.txt
echo 1000 > intermediate/serial

Now we’ll create a custom OpenSSL configuration suitable for creating a Certification Authority (root SSL certificate). You can change the stuff in bold letters to match your company.

cat << 'EOL' > openssl.cnf
# OpenSSL root CA configuration file.
# Copy to `/usr/local/opt/ca/openssl.cnf`.
#
# This file is based on the instructions found in https://jamielinux.com/docs/openssl-certificate-authority/index.html

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /usr/local/opt/ca
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# The root key and root certificate.
private_key       = $dir/private/ca.key.pem
certificate       = $dir/certs/ca.cert.pem

# For certificate revocation lists.
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 30

# SHA-1 is deprecated, so use SHA-2 instead.
default_md        = sha256

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_strict

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
# Options for the `req` tool (`man req`).
default_bits        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only

# SHA-1 is deprecated, so use SHA-2 instead.
default_md          = sha256

# Extension to add when the -x509 option is used.
x509_extensions     = v3_ca

[ req_distinguished_name ]
# See .
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = CY
stateOrProvinceName_default     = Nicosia
localityName_default            = Egkomi
0.organizationName_default      = Akeeba Ltd
organizationalUnitName_default  = Production Department
emailAddress_default            = no-reply@akeebabackup.com

[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://www.local.web/intermediate.crl.pem

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

[ san_env ]
subjectAltName=DNS:local.web,DNS:*.local.web
EOL

Now we will create a Certification Authority. Please note that all of our certificates will be encrypted with a password. It is set by the first command below. Change it to something other than akeeba.

export CERTIFICATE_PASSWORD=akeeba
brew install openssl
export PATH=$(brew --prefix openssl):$PATH
cd /usr/local/opt/ca
openssl genrsa -aes256 \
  -passout pass:$CERTIFICATE_PASSWORD \
  -out private/ca.key.pem 4096
openssl req -config openssl.cnf \
  -key private/ca.key.pem -passin pass:$CERTIFICATE_PASSWORD \
  -new -x509 -days 7300 -sha256 -extensions v3_ca \
  -subj "/CN=macOS Dev Box Root CA/O=Akeeba Ltd./OU=Production Department/C=CY/ST=Nicosia/L=Egkomi" \
  -out certs/ca.cert.pem

Now we’ll create an intermediate certification authority using a similar set of commands.

cat << 'EOL' > intermediate/openssl.cnf
# OpenSSL root CA configuration file.
#
# This file is based on the instructions found in https://jamielinux.com/docs/openssl-certificate-authority/index.html

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir = /usr/local/opt/ca/intermediate
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand

# The root key and root certificate.
private_key = $dir/private/intermediate.key.pem
certificate = $dir/certs/intermediate.cert.pem

# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/intermediate.crl.pem
crl_extensions = crl_ext
default_crl_days = 30

# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256

name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only

# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256

# Extension to add when the -x509 option is used.
x509_extensions = v3_ca

[ req_distinguished_name ]
# See .
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address

# Optionally, specify some defaults.
countryName_default = CY
stateOrProvinceName_default = Nicosia
localityName_default = Egkomi
0.organizationName_default = Akeeba Ltd
organizationalUnitName_default = Production Department
emailAddress_default = no-reply@akeebabackup.com

[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

[ san_env ]
subjectAltName=DNS:local.web,DNS:*.local.web
EOL

cd /usr/local/opt/ca
openssl genrsa -aes256 \
-passout pass:$CERTIFICATE_PASSWORD \
-out intermediate/private/intermediate.key.pem 4096

openssl req -config intermediate/openssl.cnf -new -sha256 \
-key intermediate/private/intermediate.key.pem \
-passin pass:$CERTIFICATE_PASSWORD \
-subj "/CN=macOS Dev Box Intermediate CA/O=Akeeba Ltd./OU=Production Department/C=CY/ST=Nicosia/L=Egkomi" \
-out intermediate/csr/intermediate.csr.pem

openssl ca -config openssl.cnf -extensions v3_intermediate_ca -batch \
-passin pass:$CERTIFICATE_PASSWORD \
-days 3650 -notext -md sha256 \
-in intermediate/csr/intermediate.csr.pem \
-out intermediate/certs/intermediate.cert.pem

cat intermediate/certs/intermediate.cert.pem \
certs/ca.cert.pem > intermediate/certs/ca-chain.cert.pem

Finally, we can create our SSL certificate for our development server and copy everything we need to /usr/local/etc/httpd/ssl.

cd /usr/local/opt/ca

openssl genrsa -aes256 \
  -passout pass:$CERTIFICATE_PASSWORD \
  -out intermediate/private/local.web.key.pem 2048

openssl req -config /usr/local/opt/ca/intermediate/openssl.cnf \
  -key intermediate/private/local.web.key.pem \
  -extensions san_env \
  -passin pass:$CERTIFICATE_PASSWORD \
  -subj "/CN=*.local.web/O=Akeeba Ltd./OU=Production Department/C=CY/ST=Nicosia/L=Egkomi" \
  -new -sha256 -out intermediate/csr/local.web.csr.pem

openssl ca -config intermediate/openssl.cnf -batch \
  -extensions server_cert -extensions san_env \
  -days 1835 -notext -md sha256 \
  -in intermediate/csr/local.web.csr.pem \
  -passin pass:$CERTIFICATE_PASSWORD \
  -out intermediate/certs/local.web.cert.pem

mkdir /usr/local/etc/httpd/ssl
cp intermediate/certs/ca-chain.cert.pem /usr/local/etc/httpd/ssl

openssl rsa -in intermediate/private/local.web.key.pem \
  -out /usr/local/etc/httpd/ssl/local.web.key \
  -passin pass:$CERTIFICATE_PASSWORD

cp intermediate/certs/local.web.cert.pem /usr/local/etc/httpd/ssl/local.web.crt

Enabling HTTPS on Apache

Having an SSL certificate is no good unless we can use it on the web server. Let’s edit Apache’s configuration to enable SSL support.

open /usr/local/etc/httpd/httpd.conf -a TextEdit

Find the line

Listen 80

Append one more line so now it reads

Listen 80
Listen 443

This tells Apache to listen to port 443, the default HTTPS port.

Next up, find the line

#LoadModule ssl_module lib/httpd/modules/mod_ssl.so

Change it to

LoadModule ssl_module lib/httpd/modules/mod_ssl.so

This enables HTTPS support in Apache (but does not configure it – we’ll do that in our virtual host configuration). Now find the following line

#LoadModule http2_module lib/httpd/modules/mod_http2.so

Change it to

LoadModule http2_module lib/httpd/modules/mod_http2.so

This enables HTTP/2 support in Apache. We’ll use that when configuring our virtual hosts. Speaking of which, let’s edit our virtual host configuration.

open /usr/local/etc/httpd/extra/local.web.conf -a TextEdit

Append the following lines

<VirtualHost *:443>
    Protocols h2 http/1.1
    ServerAdmin webmaster@local.web
    DocumentRoot "/Users/nicholas/Sites"
    ServerName local.web
    ServerAlias www.local.web
    #ErrorLog "/usr/local/var/log/httpd/local.web.error_log"
    #CustomLog "/usr/local/var/log/httpd/local.web.access_log" common
    #CustomLog "/usr/local/var/log/httpd/local.web.ssl_log" "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"

    SSLEngine on
    SSLCertificateFile "/usr/local/etc/httpd/ssl/local.web.crt"
    SSLCertificateKeyFile "/usr/local/etc/httpd/ssl/local.web.key"
    SSLCertificateChainFile "/usr/local/etc/httpd/ssl/ca-chain.cert.pem"

    <Directory "/Users/nicholas/Sites">
        AllowOverride All
        DirectoryIndex index.html index.php
        Require all granted

        <FilesMatch "\.php$"><filesmatch "\.php$"="">
            SSLOptions +StdEnvVars
            SetHandler "proxy:fcgi://localhost:9073/"
        </FilesMatch>
    </Directory>
</VirtualHost>

<VirtualHost *:443>
    Protocols h2 http/1.1
    ServerAdmin webmaster@local.web
    ServerAlias *.local.web
    UseCanonicalName Off
    VirtualDocumentRoot "/Users/nicholas/Sites/%1"
    
    #ErrorLog "/usr/local/var/log/httpd/local.web.error_log"
    #CustomLog "/usr/local/var/log/httpd/local.web.access_log" common
    #CustomLog "/usr/local/var/log/httpd/local.web.ssl_log" "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"

    SSLEngine on
    SSLCertificateFile "/usr/local/etc/httpd/ssl/local.web.crt"
    SSLCertificateKeyFile "/usr/local/etc/httpd/ssl/local.web.key"
    SSLCertificateChainFile "/usr/local/etc/httpd/ssl/ca-chain.cert.pem"
    <Directory "/Users/nicholas/Sites">
         AllowOverride All
         DirectoryIndex index.html index.php
         Require all granted
         <FilesMatch "\.php$">
             SSLOptions +StdEnvVars
             SetHandler "proxy:fcgi://localhost:9073/"
         </FilesMatch>
    </Directory>
</VirtualHost>      

Apart from the obvious SSL* directives which enable HTTPS support the other notable additions are Protocols and SSLOptions. The former enables HTTP/2 support in our TLS-enabled (HTTPS) virtual host. The latter fixes a problem with PHP not seeing some environment variables when using HTTPS.

Finally, let’s restart Apache

brew services restart httpd

PHP configuration

By default, PHP won’t trust our self-signed certificate. This means that any well-written script on our local machine which tries to access a local site through PHP’s stream wrappers (e.g. with fopen() or file_get_contents()) or cURL will fail to connect with a certificate error message.

The solution to that is to create a custom Certificate Authority cache file (cacert.pem) which includes our custom root CA certificate. Then we need to tell PHP to use it by default.

First, let’s create the custom cacert.pem

cd /usr/local/opt/ca
curl --remote-name --time-cond cacert.pem https://curl.haxx.se/ca/cacert.pem
cat certs/ca.cert.pem cacert.pem > new_cacert.pem
rm cacert.pem 
mv new_cacert.pem cacert.pem

Using it with PHP involves editing the php.ini file as described earlier. You need to modify the following lines (they are not contiguous in the php.in file):

openssl.cafile = /usr/local/opt/ca/cacert.pem
curl.cainfo = /usr/local/opt/ca/cacert.pem

Remember to restart the PHP service afterwards. Also keep in mind that you need to do that in all three versions of PHP.

Important! Even though we create our own default cacert.pem file it doesn’t mean that PHP scripts are going to necessarily use it. For example, Joomla! comes with its own cacert.pem file. My software such as FOF, Akeeba Backup and Akeeba Solo also ship with their own cacert.pem file. You will need to overwrite these cacert.pem files with the one you generated for these scripts to work when connecting back to your local server through HTTPS.

Telling your browsers to trust the custom certificate

Your browsers are, by default, not aware of your custom root CA certificate and won’t trust it for HTTPS. Trying to visit your local sites over HTTPS will result in a warning until you let your browsers know to trust that certificate.

This is described in my Forge your own SSL certificates for local development article, towards the end. The certificate file you need is located in /usr/local/opt/ca/certs/ca.cert.pem.

Creating databases for new sites

I prefer to use a simple convention for naming the databases of my sites. If the site’s folder is foobar (therefore I can access it as foobar.local.web) the database name is foobar, the database username is foobar and its password is foobar. This can be succintly expressed in the following two-liner SQL script for creating new databases:

CREATE DATABASE `foobar` DEFAULT COLLATE utf8mb4_unicode_520_ci;
GRANT ALL PRIVILEGES ON `foobar`.* TO 'foobar'@'localhost' IDENTIFIED BY 'foobar';

That’s four things you need to remember to replace. Or you can use the following longer SQL script with prepared statements and just ONE (1) thing to replace at the top of the script.

SET @db='foobar';
SET @create = CONCAT("CREATE DATABASE ", @db, " DEFAULT COLLATE utf8mb4_unicode_520_ci");
PREPARE stmt FROM @create;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @grant = CONCAT("GRANT ALL PRIVILEGES ON ", @db, ".* TO '", @db, "'@'localhost' IDENTIFIED BY '", @db, "'");
PREPARE stmt2 FROM @grant;
EXECUTE stmt2;
DEALLOCATE PREPARE stmt2;

Either way, substitute foobar with your database name.

INCREDIBLY IMPORTANT! Your database name must only consist of all lowercase letters a-z, digits 0-9 and underscores. Do not use uppercase letters. Do not use accented letters, special letters or letters with diacritics such as any of éêêåßçñøü. Do not use non-latin characters such as Greek, Cyrillic, Chinese, Japanese, Korean etc. Keep everything under 20 characters. Ignore these suggestions at your own peril. You have been warned.

Install the SSH2 extension for PHP

The SSH2 extension is the only available method with decent performance and memory usage to connect to remove SFTP and SSH servers from PHP running on macOS. The other option, cURL, will not work because it does not come with SFTP support compiled in.

I mention SSH2 explicitly because, like XDebug, it has its quirks depending the PHP version you are installing it in.

Before you start, you need to install the required libssh2 library.

brew install libssh2

SSH2 for PHP 5.6

brew unlink php
brew link --force --overwrite php@5.6
pecl install ssh2
brew unlink php@5.6
brew link --force --overwrite php@7.3

Now we need to edit PHP 5.6’s php.ini and tell it to load SSH2:

open /usr/local/etc/php/5.6/php.ini -a TextEdit

Find all the lines starting with ;extension=

Below them add

extension=ssh2.so

Finally, restart PHP-FPM for PHP 5.6

brew services restart php@5.6

SSH2 for PHP 7.2

brew unlink php
brew link --force --overwrite php@7.2
pecl install ssh2-1.1.2
brew unlink php@7.2
brew link --force --overwrite php@7.3

Now we need to edit PHP 7.2’s php.ini and tell it to load SSH2:

open /usr/local/etc/php/7.2/php.ini -a TextEdit

Find all the lines starting with ;extension=

Below them add

extension=ssh2.so

Finally, restart PHP-FPM for PHP 7.2

brew services restart php@7.2

SSH2 for PHP 7.3

At the time of this writing there is no PECL package for SSH2 compatible with PHP 7.3. Therefore, we have to do this the Hard Way (and hope it works).

sudo mkdir -p /usr/local/src
sudo chown $(whoami):staff /usr/local/src
cd /usr/local/src
git clone https://github.com/php/pecl-networking-ssh2.git
cd pecl-networking-ssh2/
phpize
./configure
make
make install

As before, we need to edit PHP 7.3’s php.ini and tell it to load XDebug:

open /usr/local/etc/php/7.3/php.ini -a TextEdit

Find all the lines starting with ;extension=

Below them add

extension=ssh2.so

Finally, restart PHP-FPM for PHP 7.3

brew services restart php@7.3

Install any PECL extension

PHP 5.6

Use the following command template

brew unlink php
brew link --force --overwrite php@5.6
pecl install yourPackage
brew unlink php@5.6
brew link --force --overwrite php@7.3

Where yourPackage is the name of the PECL extension you want to install.

PHP 7.2

Use the following command template

brew unlink php
brew link --force --overwrite php@7.2
pecl install yourPackage
brew unlink php@7.2
brew link --force --overwrite php@7.3

Where yourPackage is the name of the PECL extension you want to install.

PHP 7.3

This is much simpler since it’s the default PHP version:

pecl install yourPackage

Where yourPackage is the name of the PECL extension you want to install.

For all PHP versions

If your PECL package has external library dependencies you need to install them before trying to install the PECL package. Usually you can install these dependencies with HomeBrew. I won’t help you figure out which dependencies you need for each PECL package and PHP version so please don’t ask me 🙂

After compiling the PECL package you need to edit the php.ini file of the relevant PHP version and add the required extension=yourPackage.so line (or whatever else the PECL compilation told you to do; see the XDebug compilation as an example). Remember to restart PHP-FPM for the PHP version whose php.ini file you modified.

Is this really better than MAMP?

In my humble opinion: ABSO-F@CK!NG-LUTELY.

Yes, it’s a pain in the rear to set up the first time. But you get to learn how servers work which makes you a better developer. Not to mention that you can use your new knowledge to anticipate and work around server configuration issues.

Yes, after running brew upgrade I may have to go back and check if my PECL extensions still work – or recompile them as necessary. This is good, because it makes me update my PECL extensions. They are software too and like all software they do have bugs fixed and features added in subsequent releases.

No, you do not have a GUI to set up hosts. You don’t need one. If you need some fancy, custom domain name use GasMask to set up the domain resolution and copy the local.web.conf file to create your new virtual host. Modify httpd.conf to include the new virtual host file and restart Apache. Bam! Done!

No more waiting forever after every small change for servers to restart (you can restart only the affected service). No more pulling your hair with stuck servers which require restarting the Mac. No more MySQL server crashes when you try to restore a large amount of data. No more paying an extortionist yearly fee to get PHP versions which are months out of date.

If you’re willing to trade convenience for freedom and absolute control then, yes, this setup is better than MAMP. It comes down to a The Matrix kind of dilemma. Take the red pill, build your own server and be free. Take the blue pill, you’re back to your pretty MAMP jail where someone else dictates what you can or cannot do. I chose the red pill.

Whatever you do, have fun!

Published by Nicholas Dionysopoulos

PHP developer, author of Akeeba Backup and Admin Tools. Father, husband, cat herder and geek. Proudly uses all major Operating Systems on desktop and mobile.

17 replies on “Custom Apache and PHP server on macOS, the definitive 2019 edition”

  1. Please note that php@5.6 is no longer maintained by homebrew :

    ———
    php@5.6 was deleted from homebrew/core in commit 37b075c205:
    php@5.6 removal due to EOL
    ———

    1. Neither is 7.0. HomeBrew only includes the versions of PHP officially supported by the PHP project. Having to develop for PHP 5.6 (since a third of my clients uses it) I have ended up using Linux instead, either as my main OS or in a Vagrant box when using macOS. The downside is that it needs a heck of a lot more memory which rendered my older MacBook Pro with 8GB RAM unsuitable for the job, unless I want to put up with endless swapping.

      PS: Even with Linux, PHP 5.6 is going to be removed really soon. I don’t know what I’m supposed to do in this case. Maybe drop PHP 5.6 support because it’s becoming a major PITA to test against an obsolete version of PHP?

  2. Thank you for this tut !
    I lost it at item SSL. I got confused when to cd and the cat thing..A more beginner style type of step by step would be great. As an extra a guide to enable xdebug in VScode would be great too…
    Finale Igor brew apache & php incl.xdebug up….
    Thank you again…
    joe

    1. That is the easy, step-by-step way to do it. You are only supposed to change one line, the password, and then just blindly paste the stuff I tell you to paste to the command line. There is no GUI or fewer steps to generate SSL certificates.

  3. Great Tutorial, thank you. I’m having problems on php installation though, even following all the steps. http://localhost/index.php or even an html file in the path gives me a 404. What I’m missing? Is it because of the built in Apache server on Mojave?
    Please note: I’m a good Linux user, but new to Mac
    Thank you so much!

    1. If you had activated the built-in Apache server in macOS that could be it. If you are using Skype it also binds to port 80 by default.

      That said, macOS is just BSD under the hood so let’s open a Terminal and let it spill its secrets. Try running lsof -nP -i4TCP:80 | grep LISTEN to see what is listening to port 80. The second column is the PID. You can find it in the output of ps -A to understand where that process came from.

  4. If you have to install brew php@5.6 formula since it is mark as deprecated you can do it by adding tap brew exolnet deprecated:

    brew tap exolnet/homebrew-deprecated; brew install php@5.6

  5. If you have to install brew php@5.6 formula since it is mark as deprecated you can do it by adding tap brew exolnet deprecated:

    brew tap exolnet/homebrew-deprecated; brew install php@5.6

  6. Hei
    i executed this command on Mac
    php -S localhost:4000
    after this my mamp server dosent work on my Mac.
    how can i fix this. i also intalled brew and installed php7.3 . but it still doesnt work. please help me.

    1. You’ve mentioned three mutually exclusive things.

      php -S is used to run a temporary, very limited development server built into PHP. If you need it you know how it works or where to find documentation about it so it’s pretty clear that you don’t want.

      MAMP is a complete Apache, MySQL and PHP package for macOS. It’s mutually exclusive with running the temporary built-in PHP server and with creating your own Apache, MySQL and PHP environment — the premise of this article.

      The instructions from HomeBrew are about creating your own Apache, MySQL and PHP environment. You cannot use that to upgrade MAMP’s PHP. If you had actually read the article you’d have already known that MAMP’s policy of not supporting new PHP versions and making it unnecessarily complicated to install your own is exactly why I went that route. So why exactly did you think it even remotely reasonable to ask me to help you with what I explicitly declared impractical in the second paragraph of the article, setting the article’s raison d’etre, shall forever remain a mystery…

  7. Would it be easy to install the latest MariaDB instead of MySQL? I’ve read that MariaDB should be compatible with MySQL, but I’m not sure it can be installed without any changes.

    Also, can I start the server manually without installing the system startup service?

  8. This was an epic effort. Well done and thanks so much. MAMP has been causing me more problems than it’s been solving lately, so it was time to pull the plug.
    It looks like Xdebug supports 7.3 now. So the ‘-beta’ rider isn’t needed on install.

  9. Hi Nicholas,
    Your tutorial is excellent. Worked out of the box.
    1. In part “Enabling HTTPS on Apache” > addition in local.web.conf your code says at some point “”. This thrown an error. I changed to ” ” and it worked
    2. I get some warnings.. (anything to worry??)
    -AH01909: vavoum.local:443:0 server certificate does NOT include an ID which matches the server name
    -AH01873: Init: Session Cache is not configured [hint: SSLSessionCache]
    -AH00094: Command line: ‘/usr/local/opt/httpd/bin/httpd -D FOREGROUND’

    Thank you again!!

    1. I am not sure what you changed. It probably contains lower / greater than signs which causes the comment system to kill that bit of your comment (thanks, WordPress…).

      Regarding the warnings, Google them 🙂 None is a problem but by reading up on them you’ll understand better how your server works.

Comments are closed.