Configure Apache and PHP-FPM on macOS

Apple Computer.

Overview

Over the last few months, I have experimented with different hosting arrangements, WordPress, and Hugo CMS configurations. Now it's time to start creating content to see if I can create a viable site. To that end, I want to start by writing about my efforts to create a local website development environment on my Mac.

As a foundation, I have chosen Homebrew to install most of the software that I am using. Please go to their site for installation instructions. It's not the only game in town. Apple does provide a web development environment, but their software may be (slightly) out-of-date or hard to configure. MacPorts is another good option. After trying out both, I had a hard time deciding, but finally chose Homebrew for it's popularity and ease of use.

This post is just about Apache and PHP; I wrote about MySQL in a separate post. I originally wrote this post for macOS Catalina, but I just updated it for macOS Big Sur. In this post, I also describe some tools that I wrote to manage Apache and PHP processes.

Note

I recently moved my site from WordPress to Hugo. Hugo is a static site generator and does not require PHP or MySQL.

Using the Apache Event Module

There are multiple sites that describe how to install Apache and PHP on a Mac using Homebrew. But most of them describe using Apache with the pre-fork Multi-Processing Module (MPM). That's understandable since Homebrew installs Apache with the pre-fork module active. But in the Linux world, the current, default module is the event module. This allows you to run PHP as a separate process, which is called on as needed. With the pre-fork module, PHP is built in (with mod_php) and is executed all the time even if PHP is not required to fulfill the request.

Running Apache with the pre-fork module is probably just fine while working with a local development environment. But I wanted to learn how to use it with the event module, running PHP as a separate process. By the way, you should check out "macOS 12.0 Monterey Apache Setup: Multiple PHP Versions." This is one of the best posts that I have seen about setting up a local website development environment. It has inspired much of what I have done to setup my local development environment. It also uses Homebrew to install most of the software.

Install Apache with the brew command

Let's get started; the first step is to install Apache, using Homebrew. Please install Homebrew and verify your installation if you haven't done so.

 1$ brew doctor
 2Your system is ready to brew.
 3
 4$ brew install httpd
 5
 6$ which -a httpd
 7/usr/local/bin/httpd
 8/usr/sbin/httpd
 9
10$ httpd -v
11Server version: Apache/2.4.47 (Unix)
12Server built:   May 12 2021 11:12:57
13
14$ httpd -V | grep MPM
15Server MPM:     prefork

First, we verified that Homebrew is ready to go. Next, we installed Apache (and a number of dependencies). Finally, we ran a couple of commands to check the installation. Note, use the which command to verify that the Homebrew version of Apache is first in your PATH (you will have to restart your terminal session). The last command shows that Apache was installed with the prefork module enabled.

Great. Now, let's start it up and test it with Safari:

 1$ brew services start httpd
 2==> Tapping homebrew/services
 3Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-services'...
 4remote: Enumerating objects: 1188, done.
 5remote: Counting objects: 100% (67/67), done.
 6remote: Compressing objects: 100% (56/56), done.
 7remote: Total 1188 (delta 22), reused 18 (delta 10), pack-reused 1121
 8Receiving objects: 100% (1188/1188), 352.17 KiB | 3.67 MiB/s, done.
 9Resolving deltas: 100% (498/498), done.
10Tapped 1 command (43 files, 449KB).
11==> Successfully started `httpd` (label: homebrew.mxcl.httpd)

(The first time you run the brew services command, it will add the Homebrew-services tap.) Click on: http://localhost:8080 to test that Apache is running. You should see It Works! in your Safari browser. Note, that you had to specify the non-standard 8080 port number as part of the URL. That's because Homebrew does not like using sudo, which is required if you want to start up Apache with the standard port 80 configuration. That's called a privileged port and requires root privileges.

If you don't see "It Works!", then you need to do some troubleshooting. Make sure that httpd is actually running. I have seen cases where the brew services command said that it started, but it either didn't start or died right away (later on I will describe my a2ctl tool for starting and stopping httpd; it does additional error checking). Let's verify that the httpd processes are running:

1$ ps -x | egrep httpd | sed '/grep/d' 
2 6791 ??         0:00.05 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
3 6802 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
4 6803 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
5 6804 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
6 6805 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
7 6806 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
8 6967 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
9 6975 ??         0:00.00 /usr/local/opt/httpd/bin/httpd -D FOREGROUND

If you do not see a list of httpd processes, or you are still having issues, please check the Apache error log file.

1$ tail -f /usr/local/var/log/httpd/error_log

It will usually show what went wrong.

As you may have guessed, this is how we stop Apache:

1$ brew services stop httpd 
2Stopping `httpd`... (might take a while)
3==> Successfully stopped `httpd` (label: homebrew.mxcl.httpd)

Switching Apache to the event module and port 80

Using port 80

Next, I want to change my Apache configuration to use the event module and port 80. To start and stop Apache when using privilege ports, sudo has to be used. Homebrew is designed to not use sudo. You will see examples of it being used, but in my experience, the brew services ... command does not play nicely with sudo. Therefore, I wrote my own shell script to start and stop Apache.

Make sure that you have stopped Apache with the above brew services command. I wrote a shell script called a2ctl to start, stop, or restart Apache:

  1#!/bin/zsh
  2
  3# Usage: a2ctl start|stop|restart|status
  4
  5error_exit()
  6{
  7  echo -e "$1" 1>&2
  8  exit 1
  9}
 10
 11# Usage
 12if [[ ! $# -eq 1 || ! ($1 == "start" || $1 == "stop" || $1 == "restart" || $1 == "status") ]]
 13then
 14  error_exit "Usage: a2ctl start|stop|restart|status"
 15else
 16  action="$1"
 17fi
 18
 19pwait()
 20{ 
 21  process=$1
 22  action=$2
 23  count=0
 24
 25  if [[ $action == "stop" ]]
 26  then
 27    until ! pgrep -q $process || [[ $count -gt 5 ]]
 28    do
 29      echo "waiting for $process to stop $count ..."
 30      sleep 1
 31      ((count++))
 32    done 
 33    if ! pgrep -q $process; then
 34      echo "$process stopped OK ..."
 35    else
 36      error_exit "$process failed to stop"
 37    fi
 38  else
 39    # action is start 
 40    sleep 1 # give process time to die for configuration file errors ...
 41    until pgrep -q $process || [[ $count -gt 5 ]]
 42    do
 43      echo "waiting for $process to start $count ..."
 44      sleep 1
 45      ((count++))
 46    done
 47    if pgrep -q $process; then
 48      echo "$process started OK ..."
 49    else
 50      error_exit "$process failed to start"
 51    fi
 52  fi
 53}
 54
 55a2start()
 56{
 57  if pgrep -q httpd; then
 58    echo "Apache is already started ..."
 59    exit 0
 60  fi
 61
 62  if [ ! -f "/usr/local/opt/httpd/homebrew.mxcl.httpd.plist" ]; then
 63    error_exit "Cannot start Apache -- homebrew.mxcl.httpd.plist is missing ..."
 64  fi
 65
 66  sudo cp /usr/local/opt/httpd/homebrew.mxcl.httpd.plist /Library/LaunchDaemons/
 67  if [ "$?" != "0" ]; then
 68    error_exit "homebrew.mxcl.httpd.plist copy failed ..."
 69  fi
 70
 71  sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.httpd.plist > /dev/null 2>&1
 72  pwait httpd start
 73}
 74
 75a2stop()
 76{
 77#  if ! pgrep -q httpd; then
 78#    echo "Apache is already stopped ..."
 79#    exit 0
 80#  fi
 81
 82  sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.httpd.plist > /dev/null 2>&1
 83  pwait httpd stop
 84
 85  # Don't start automatically
 86  sudo rm -f /Library/LaunchDaemons/homebrew.mxcl.httpd.plist
 87}
 88
 89a2restart()
 90{
 91  # First, stop Apache ...
 92#  if ! pgrep -q httpd; then
 93#    echo "Apache is already stopped, so just start it up ..."
 94#    a2start
 95#    exit 0
 96#  fi
 97
 98#  sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.httpd.plist > /dev/null 2>&1
 99#  pwait httpd stop
100
101  # Then, start Apache ...
102#  sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.httpd.plist > /dev/null 2>&1
103#  pwait httpd start
104  a2stop
105  a2start
106}
107
108a2status()
109{
110  # Get status about running httpd processes
111  ps -axo user,pid,start,etime,time,nice,vsz,rss,command |
112    egrep '^USER|httpd' | sed '/grep/d'
113}
114
115case $action in
116  "start")
117    a2start
118    ;;
119  "stop")
120    a2stop
121    ;;
122  "restart")
123    a2restart
124    ;;
125  "status")
126    a2status
127    ;;
128  *)
129    # Should never happen ...
130    error_exit "Invalid action ..."
131    ;;
132esac
133
134exit 0

As noted by the Usage comment, it is very easy to use. Once started, Apache will start automatically when the macOS system is restarted. If stopped, Apache will not start when the system restarts. If stopped or running, restart will (re)start Apache. Now that we have our new Apache start/stop script, lets update our Apache configuration to use port 80:

 1$ cd /usr/local/etc/httpd
 2
 3$ mkdir -p ~/orig/usr/local/etc/httpd
 4
 5$ cp httpd.conf ~/orig/usr/local/etc/httpd
 6
 7# Use your favorite text editor ...
 8$ vi httpd.conf
 9
10$ diff ~/orig/usr/local/etc/httpd/httpd.conf .
1152c52
12< Listen 8080
13---
14> Listen 80
15
16223c223
17< #ServerName www.example.com:8080
18---
19> ServerName localhost
20
21$ a2ctl start
22Password:
23httpd started OK ...

Now, go to http://localhost (dropping the 8080) and you should see It Works! again.

It seems that on macOS Big Sur, the brew services command will still run httpd ok after changing to port 80. I am not sure if that's always the case. I am sticking with using sudo in a2ctl to stay consistent with my Linux experience. Feel free to experiment for yourself. BTW, you can use a2ctl to check the status of your running httpd processes:

1$ a2ctl status
2USER               PID STARTED     ELAPSED      TIME NI      VSZ    RSS COMMAND
3root              7939 12:20PM       00:06   0:00.02  0  4431348   4404 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
4_www              7941 12:20PM       00:06   0:00.00  0  4310480   1076 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
5_www              7942 12:20PM       00:06   0:00.00  0  4310480   1088 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
6_www              7943 12:20PM       00:06   0:00.00  0  4302288   1076 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
7_www              7944 12:20PM       00:06   0:00.00  0  4302288   1092 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
8_www              7945 12:20PM       00:06   0:00.00  0  4326864   1100 /usr/local/opt/httpd/bin/httpd -D FOREGROUND

Switching to the event module

Make the following changes to switch Apache to the event module:

 1$ cd /usr/local/etc/httpd
 2
 3$ vi httpd.conf
 4
 5$ diff ~/orig/usr/local/etc/httpd/httpd.conf .
 6 o o o
 766,67c66,67
 8< #LoadModule mpm_event_module lib/httpd/modules/mod_mpm_event.so
 9< LoadModule mpm_prefork_module lib/httpd/modules/mod_mpm_prefork.so
10---
11> LoadModule mpm_event_module lib/httpd/modules/mod_mpm_event.so
12> #LoadModule mpm_prefork_module lib/httpd/modules/mod_mpm_prefork.so
13
14488c488
15< #Include /usr/local/etc/httpd/extra/httpd-mpm.conf
16---
17> Include /usr/local/etc/httpd/extra/httpd-mpm.conf
18
19$ a2ctl restart
20Password:
21httpd stopped OK ...
22httpd started OK ...
23
24$ httpd -V | grep MPM
25Server MPM:     event

We commented out the prefork module and uncommitted the event module. Uncommenting the Include httpd-mpm.conf file is optional, but it allows updating the number of httpd event servers, etc. This is the default event MPM configuration defined in this file:

 1$ cd /usr/local/etc/httpd/extra
 2
 3$ cat httpd-mpm.conf
 4 o o o
 5# event MPM
 6# StartServers: initial number of server processes to start
 7# MinSpareThreads: minimum number of worker threads which are kept spare
 8# MaxSpareThreads: maximum number of worker threads which are kept spare
 9# ThreadsPerChild: constant number of worker threads in each server process
10# MaxRequestWorkers: maximum number of worker threads
11# MaxConnectionsPerChild: maximum number of connections a server process serves
12#                         before terminating
13<IfModule mpm_event_module>
14    StartServers             3
15    MinSpareThreads         75
16    MaxSpareThreads        250
17    ThreadsPerChild         25
18    MaxRequestWorkers      400
19    MaxConnectionsPerChild   0
20</IfModule>
21 o o o

Change Document Root to the Local User

Because we are setting up a Development environment (don't do this on a production system), we will move our Document Root to our local user Sites directory. Make the following changes to httpd.conf (changing user to your local user):

 1# Set local user ...
 2
 3193,194c193,194
 4< User _www
 5< Group _www
 6---
 7> User user
 8> Group staff
 9
10# Update Document Root to point to the local user
11
12248,249c248,249
13< DocumentRoot "/usr/local/var/www"
14< <Directory "/usr/local/var/www">
15---
16> DocumentRoot /Users/user/Sites
17> <Directory /Users/user/Sites>
18269c269
19<     AllowOverride None
20---
21>     AllowOverride All
22
23# Restart apache ...
24
25$ a2ctl restart
26Password:
27httpd stopped OK ...
28httpd started OK ...

By the way, if something doesn't go right, be sure to check the Apache error log at /usr/local/var/log/httpd/error_log. For the "started OK" case you should see a line that says "resuming normal operations." Also, take a look at the a2ctl status output:

1$ a2ctl status 
2USER               PID STARTED     ELAPSED      TIME NI      VSZ    RSS COMMAND
3root            8859 12:46PM       00:08   0:00.02  0  4440840   4720 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
4user            8861 12:46PM       00:08   0:00.00  0  4332580   1468 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
5user            8862 12:46PM       00:08   0:00.00  0  4341796   1476 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
6user            8863 12:46PM       00:08   0:00.00  0  4333604   1496 /usr/local/opt/httpd/bin/httpd -D FOREGROUND

Apache starts up with root ownership and then it spawns the user processes.

Let's test our new local user root directory. Create a index.html in your ~/Sites directory and test it in your Safari browser:

1$ mkdir -p ~/Sites
2
3$ cd ~/Sites
4
5$ vi index.html
6
7$ cat index.html
8<h1>Our Local User Web Root</h1>

Go to http://localhost. You should see Our Local User Web Root in your browser (you may need to refresh your browser). If all is good, we can move on to installing and configuring PHP-FPM.

Install and Configure PHP-FPM

I have configured Apache to work with the event module and our local Sites document root. Now let's make the Apache configuration changes to talk to PHP-FPM. We will enable the _mod_proxy_fcgi_ and _mod_proxy_ modules along with httpd.conf changes to enable FastCGI support. You can read more about how this works at the Apache Module mod_proxy_fcgi website. Finally, I will install and configure PHP-FPM.

Configure Apache to work with PHP-FPM

Edit httpd.conf with your favorite text editor. Make the following changes:

 1131c131
 2< #LoadModule proxy_module lib/httpd/modules/mod_proxy.so
 3---
 4> LoadModule proxy_module lib/httpd/modules/mod_proxy.so
 5135c135
 6< #LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so
 7---
 8> LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so
 9
10282c282
11<     DirectoryIndex index.html
12---
13>     DirectoryIndex index.php index.html
14
15# and add this block right after the dir_module block ...
16
17# Run php-fpm via proxy_fcgi
18<IfModule proxy_fcgi_module>
19    <FilesMatch ".php$">
20        SetHandler "proxy:unix:/usr/local/var/run/php-fpm.sock|fcgi://localhost"
21    </FilesMatch>
22</IfModule>

Restart Apache with the above changes:

1$ a2ctl restart
2Password:
3httpd stopped OK ...
4httpd started OK ...

Check the Apache error log if it doesn't start up OK. At this point Apache is ready to run PHP. We don't have to touch it again. We can install PHP, change PHP versions, etc., and Apache will keep talking to the currently running PHP version. So let's move on to our PHP install.

Setting up PHP-FPM to work with Apache

As of this writing, the newest version of PHP is version 8.0. However, my Pair Networks shared host is using version 7.4. The following instructions will describe how to install and configure PHP version 7.4 as the default version of PHP. I will also install version 8.0. We could use the brew services command to start and stop the PHP-FPM service. But I opted to write a shell script to start and stop PHP-FPM. I can also easily switch PHP versions with my shell script. I will also update the PHP-FPM configuration file to use a Unix socket to talk to Apache (which is ready to go on the Apache side). The PHP brew installation defaults to using a TCP/IP socket, but the Unix socket approach has less overhead. You can read about what's the difference between a Unix socket and a TCP/IP socket.

Let's frist install PHP versions 7.4 and 8.0:

 1$ brew install php@7.4
 2
 3$ brew install php
 4
 5# Set the active version of PHP to version 7.4
 6
 7$ brew unlink php && brew link --overwrite --force php@7.4
 8
 9# Restart your terminal session ...
10
11$ php -v
12PHP 7.4.19 (cli) (built: May 13 2021 06:28:47) ( NTS )
13Copyright (c) The PHP Group
14Zend Engine v3.4.0, Copyright (c) Zend Technologies
15    with Zend OPcache v7.4.19, Copyright (c), by Zend Technologies
16
17$ brew services start php@7.4
18==> Successfully started `php@7.4` (label: homebrew.mxcl.php@7.3)

The first brew command installed PHP version 7.4 and a bunch of dependencies. The second install command installed the latest production version of PHP, which is currently version 8.0. Finally, the third brew command switched the default version of PHP to version 7.4. Note: Be sure to restart your terminal session after completing the brew link command. Then you can verify your PHP version, and I also showed how to start the PHP-FPM services with the brew services command. For now, stop PHP-FPM with the _brew services stop php@7._4 command. We need to update the PHP-FPM configuration file, so that it will be able to talk to Apache.

Starting and Stopping PHP-FPM

You can continue using the brew services command to start and stop PHP-FPM. I prefer having a little more control for starting and stopping, including verifying that PHP-FPM actually did start. I have seen cases where the brew services command will report a successful start, but PHP-FPM actually did not start (or died immediately). I also wanted to implement a convenient way to switch PHP versions. Therefore, I wrote a shell script called phpctl to start, stop, and restart PHP-FPM. I also included an status action to get information about the running PHP-FPM processes. But before we get into the details about phpctl, let's update the PHP-FPM configuration for Apache (and some additional tweaks).

First, change directory to /usr/local/etc/php/PHP_VERSION/php-fpm.d. The default PHP-FPM configuration is in www.conf. You will want to save a copy of that file in a safe place because it has a lot of useful comments in it, but we are going to filter out all the commented lines and make our Unix proxy changes and point to our local user (which you need to update):

 1$ pwd
 2/usr/local/etc/php/7.4/php-fpm.d
 3
 4$ mkdir -p ~/orig/usr/local/etc/php/7.4/php-fpm.d
 5
 6$ cp www.conf ~/orig/usr/local/etc/php/7.4/php-fpm.d
 7
 8# Strip out all the comments ...
 9
10$ cat www.conf | egrep -v "^;" | egrep -v "^$" > tmpfile
11
12$ mv tmpfile www.conf
13
14$ cat www.conf
15[www]
16user = _www
17group = _www
18listen = 127.0.0.1:9000
19pm = dynamic
20pm.max_children = 5
21pm.start_servers = 2
22pm.min_spare_servers = 1
23pm.max_spare_servers = 3
24
25# Update www.conf to use a Unix socket and our local user ...
26
27$ cat www.conf
28[www]
29user = user
30group = staff
31listen = /usr/local/var/run/php-fpm.sock
32pm = dynamic
33pm.max_children = 5
34pm.start_servers = 2
35pm.min_spare_servers = 1
36pm.max_spare_servers = 3

Now we are ready to test that PHP-FPM is working with Apache. You will need to first stop (if not stopped already) and then start PHP-FPM with the brew services command. Next, we will create a simple info.php PHP script to test that everything is working:

 1$ brew services stop php@7.4 
 2Stopping `php@7.4`... (might take a while)
 3==> Successfully stopped `php@7.4` (label: homebrew.mxcl.php@7.4)
 4
 5$ brew services start php@7.4
 6==> Successfully started `php@7.4` (label: homebrew.mxcl.php@7.4)
 7
 8$ cd ~/Sites
 9
10$ vi info.php
11
12$ cat info.php
13<?php
14  phpinfo();
15?>

Go to http://localhost/info.php. You should see the PHP info screen with lots and lots of information about your running PHP installation. Note that the Server API shows FPM/FastCGI. If all is good, then we are about done with the exception of optionally using my p7ctl PHP control script. Also, don't forget to make the same www.conf changes for PHP-FPM 8.0.

 1$ brew services stop php@7.4
 2Stopping `php@7.4`... (might take a while)
 3==> Successfully stopped `php@7.4` (label: homebrew.mxcl.php@7.4)
 4
 5$ cd ../../8.0/php-fpm.d
 6
 7$ php -v
 8PHP 7.4.19 (cli) (built: May 13 2021 06:28:47) ( NTS )
 9Copyright (c) The PHP Group
10Zend Engine v3.4.0, Copyright (c) Zend Technologies
11    with Zend OPcache v7.4.19, Copyright (c), by Zend Technologies
12
13$ brew unlink php && brew link --force --overwrite php@8.0
14Unlinking /usr/local/Cellar/php@7.4/7.4.19_1... 25 symlinks removed.
15Linking /usr/local/Cellar/php/8.0.6_1... 24 symlinks created.
16
17$ php -v
18PHP 8.0.6 (cli) (built: May 13 2021 05:36:01) ( NTS )
19Copyright (c) The PHP Group
20Zend Engine v4.0.6, Copyright (c) Zend Technologies
21    with Zend OPcache v8.0.6, Copyright (c), by Zend Technologies
22
23# Update www.conf like we did for version 7.4:
24
25$ cat www.conf
26[www]
27user = user
28group = staff
29listen = /usr/local/var/run/php-fpm.sock
30pm = dynamic
31pm.max_children = 5
32pm.start_servers = 2
33pm.min_spare_servers = 1
34pm.max_spare_servers = 3
35
36$ brew services start php@8.0
37==> Successfully started `php` (label: homebrew.mxcl.php)
38
39# Again, run the info.php script in Safari:
40# http://localhost/info.php
41
42$ brew services stop php@8.0 
43Stopping `php`... (might take a while)
44==> Successfully stopped `php` (label: homebrew.mxcl.php)

Go to http://localhost/info.php to test. I actually showed how you can manually switch PHP-FPM services. You can actually start different PHP-FPM versions without doing the unlink and link commands. But I prefer keeping the Command Line Version of PHP (php -v) in sync with the PHP-FPM version. Also, I don't know if there could be other side affects from not unlinking and linking.

Using phpctl instead of the brew services command

I wrote a shell script, phpctl, to have a little finer control for stopping and starting PHP-FPM. I can easily use it to switch the running version of PHP-FPM (currently, version 7.4 or 8.0). You simply stop the running version and start the new version. Here is my phpctl shell script:

  1#!/bin/zsh
  2
  3# Usage: phpctl start [7.4|8.0|8.1] -- starts 7.4, 8.0, or 8.1
  4#          (defaults to currently linked version)
  5#        phpctl restart -- restarts the currently linked version
  6#        phpctl stop -- stops the currently linked and running version
  7#        phpctl status -- get info about running php-fpm processes
  8#
  9#        The start action will unlink the current version and link
 10#        the requested version if different from the linked version.
 11#        The restart and stop actions only work with the
 12#        currently linked version.
 13
 14error_exit()
 15{
 16  echo -e "$1" 1>&2
 17  exit 1
 18}
 19
 20# Get the currently linked PHP version ...
 21linked="php@$(/usr/local/bin/php -v | grep ^PHP | cut -d' ' -f2 | cut -d'.' -f1,2)"
 22
 23# Get command parameters ...
 24if [[ ! $# -ge 1 || ! ($1 == "start" || $1 == "stop" || $1 == "restart" || $1 == "status" ) ]]; then
 25  error_exit "Usage:tphpctl start [7.4|8.0|8.1]ntphpctl restartntphpctl stopntphpctl status"
 26else
 27  action="$1"
 28  if [[ -z $2 ]]; then
 29    # Default to the linked version
 30    version=${linked}
 31  elif [[ ! ($2 == "7.4" || $2 == "8.0" || $2 == "8.1" ) ]]; then
 32    error_exit "Version $2 is not supported."
 33  else
 34    version="php@$2"
 35  fi
 36fi
 37
 38# plist file name format can change (e.g., php vs php@7.4)
 39plfile=$(find /usr/local/opt/${linked}/homebrew.mxcl.php*.plist)
 40if [[ $? -ne 0 ]]; then
 41  error_exit "Did not find linked ${linked} plist file ..."
 42fi
 43
 44pwait()
 45{
 46  process=$1
 47  action=$2
 48  count=0
 49
 50  if [[ $action == "stop" ]]
 51  then
 52    until ! pgrep -q $process || [[ $count -gt 5 ]]
 53    do
 54      echo "waiting for $process to stop $count ..."
 55      sleep 1
 56      ((count++))
 57    done
 58    if ! pgrep -q $process; then
 59      echo "$process stopped OK ..."
 60    else
 61      error_exit "$process failed to stop"
 62    fi
 63  else
 64    # action is start
 65    sleep 1 # give process time to die for configuration file errors ...
 66    until pgrep -q $process || [[ $count -gt 5 ]]
 67    do
 68      echo "waiting for $process to start $count ..."
 69      sleep 1
 70      ((count++))
 71    done
 72    if pgrep -q $process; then
 73      echo "$process started OK ..."
 74    else
 75      error_exit "$process failed to start"
 76    fi
 77  fi
 78}
 79
 80php_start()
 81{
 82  if pgrep -q php-fpm; then
 83    echo "${linked} is already started."
 84    exit 0
 85  fi
 86
 87  if [[ ${linked} != ${version} ]]; then
 88    brew unlink ${linked} > /dev/null 2>&1 &&
 89      brew link --force --overwrite ${version} > /dev/null 2>&1
 90    linked=${version}
 91    plfile=$(find /usr/local/opt/${linked}/homebrew.mxcl.php*.plist)
 92    if [[ $? -ne 0 ]]; then
 93      error_exit "Did not find linked ${linked} plist file ..."
 94    fi
 95  fi
 96
 97  cp ${plfile} ~/Library/LaunchAgents/
 98  if [[ $? -ne 0 ]]; then
 99    error_exit "${plfile} copy failed ..."
100  fi
101
102  launchctl load ~/Library/LaunchAgents/$(basename ${plfile}) > /dev/null 2>&1
103  pwait php-fpm start
104}
105
106php_stop()
107{
108  if ! pgrep -q php-fpm; then
109    echo "${linked} is already stopped ..."
110    exit 0
111  fi
112
113  launchctl unload ~/Library/LaunchAgents/$(basename ${plfile}) > /dev/null 2>&1
114  pwait php-fpm stop
115
116  # Don't start automatically
117  rm -f ~/Library/LaunchAgents/$(basename ${plfile})
118}
119
120php_restart()
121{
122
123  # First, stop PHP ...
124  if ! pgrep -q php-fpm; then
125    echo "${linked} is already stopped, so just start it up ..."
126    # Always restart the linked version
127    if [[ ${version} != ${linked} ]]; then
128      version=${linked}
129    fi
130    php_start
131    exit 0
132  fi
133
134  launchctl unload ~/Library/LaunchAgents/$(basename ${plfile}) > /dev/null 2>&1
135  pwait php-fom stop
136
137  # Then, start PHP ...
138  launchctl load ~/Library/LaunchAgents/$(basename ${plfile}) > /dev/null 2>&1
139  pwait php-fpm start
140}
141
142php_status()
143{
144  ps -axo user,pid,start,etime,time,nice,vsz,rss,command |
145    egrep '^USER|php-fpm' | sed '/grep/d'
146}
147
148case $action in
149  "start")
150    php_start
151    ;;
152  "stop")
153    php_stop
154    ;;
155  "restart")
156    php_restart
157    ;;
158  "status")
159    php_status
160    ;;
161  *)
162    # Should never happen ...
163    error_exit "Invalid action ..."
164    ;;
165esac
166
167exit 0

Here are some examples of how to use it (first, be sure to run brew services stop php@8.0 if you previously started PHP-FPM with the brew services start command):

 1# The currently linked version of PHP
 2$ php -v
 3PHP 8.0.10 (cli) (built: Aug 26 2021 15:36:17) ( NTS )
 4Copyright (c) The PHP Group
 5Zend Engine v4.0.10, Copyright (c) Zend Technologies
 6    with Zend OPcache v8.0.10, Copyright (c), by Zend Technologies
 7
 8# Start the currently linked version of PHP-FPM
 9$ phpctl start
10php-fpm started OK ...
11
12# Trying to start again ...
13$ phpctl start
14php@8.0 is already started.
15
16# Get info about our running PHP-FPM processes
17$ phpctl status
18USER               PID STARTED  ELAPSED      TIME NI      VSZ    RSS COMMAND
19george           16461 12:21PM    00:24   0:00.04  0  4749016  13796 /usr/local/opt/php/sbin/php-fpm --nodaemonize
20george           16463 12:21PM    00:24   0:00.00  0  4748760    752 /usr/local/opt/php/sbin/php-fpm --nodaemonize
21george           16464 12:21PM    00:24   0:00.00  0  4757976    756 /usr/local/opt/php/sbin/php-fpm --nodaemonize
22
23# Stop the running version
24$ phpctl stop
25php-fpm stopped OK ...
26
27# Start a different version (and unlink and link)
28phpctl start 7.4
29php-fpm started OK ...
30
31# Confirm new linked version
32$ php -v
33PHP 7.4.23 (cli) (built: Aug 27 2021 09:20:14) ( NTS )
34Copyright (c) The PHP Group
35Zend Engine v3.4.0, Copyright (c) Zend Technologies
36    with Zend OPcache v7.4.23, Copyright (c), by Zend Technologies
37
38# Get info about our new running PHP-FPM processes
39$ phpctl status
40USER               PID STARTED  ELAPSED      TIME NI      VSZ    RSS COMMAND
41george           16824 12:24PM    00:28   0:00.04  0  5018696  13884 /usr/local/opt/php@7.4/sbin/php-fpm --nodaemonize
42george           16826 12:24PM    00:28   0:00.00  0  5017416    744 /usr/local/opt/php@7.4/sbin/php-fpm --nodaemonize
43george           16827 12:24PM    00:28   0:00.00  0  5026632    732 /usr/local/opt/php@7.4/sbin/php-fpm --nodaemonize

Setting up Virtual Hosts

At this point, Apache and PHP-FPM is working with our Mac localhost. The last item that we want to configure is our virtual hosts environment. We want to be able to create testing host domains that are local to our Mac. For example, I have created a local test domain called altoplace.tst.

For each virtual host, we need a DNS entry, so that we're able to enter that name into our Safari (or your favorite) browser on your Mac. We could add an entry for each virtual host to our /etc/hosts file, but instead of doing that, we will install a lightweight DNS server on our Mac that will resolve any domain name ending in txt (or test, or whatever local TLD you choose). Of course, don't pick a real TLD. Also, I understand that the current version of Google Chrome forces all .dev domains to use SSL, so .dev might not be a good choice. We are going to install and use dnsmasq, using Homebrew. Also, I will show you my shell script, dnsctl, that I use to start and stop dnsmasq. Dnsmasq requires root privileges, and as I noted before, the brew services command does not play well with the sudo command.

Using a lightweight DHCP and caching DNS server, dnsmasq

Run the following commands to install and configure dnsmasq:

 1$ brew install dnsmasq
 2
 3$ sudo mkdir -p /etc/resolver
 4
 5$ cd /usr/local/etc
 6
 7$ mkdir -p ~/orig/usr/local/etc
 8
 9$ cp dnsmasq.conf ~/orig/usr/local/etc
10
11$ vi dnsmasq.conf
12
13$ diff ~/orig/usr/local/etc/dnsmasq.conf .
1479a80
15> address=/.tst/127.0.0.1
16
17$ sudo bash -c 'echo "nameserver 127.0.0.1" > /etc/resolver/tst'

The last three lines creates our .tst domain. Actually, I made this a bit more complicated than needed. You could have just created a one line dnsmasq.conf file that contains address=/.tst/127.0.0.1. None of the other configuration options in this file are turned on for our simple application, but I wanted to keep the file as is in case I choose to make other configuration changes in the future.

At this point, we are ready to start the dnsmasq service. However, it needs to be started with root privileges, so I am going to again create my own shell script to start and stop dnsmasq. I called it dnsctl:

  1#!/bin/zsh
  2
  3# Usage: dnsctl start|stop|restart|status
  4
  5error_exit()
  6{
  7  echo -e "$1" 1>&2
  8  exit 1
  9}
 10
 11# Usage
 12if [[ ! $# -eq 1 || ! ($1 == "start" || $1 == "stop" || $1 == "restart" || $1 == "status") ]]
 13then
 14  error_exit "Usage: dnsctl start|stop|restart|status"
 15else
 16  action="$1"
 17fi
 18
 19pwait()
 20{
 21  process=$1
 22  action=$2
 23  count=0
 24
 25  if [[ $action == "stop" ]]
 26  then
 27    until ! pgrep -q $process || [[ $count -gt 5 ]]
 28    do
 29      echo "waiting for $process to stop $count ..."
 30      sleep 1
 31      ((count++))
 32    done
 33    if ! pgrep -q $process; then
 34      echo "$process stopped OK ..."
 35    else
 36      error_exit "$process failed to stop"
 37    fi
 38  else
 39    # action is start
 40    sleep 1 # give process time to die for configuration file errors ...
 41    until pgrep -q $process || [[ $count -gt 5 ]]
 42    do
 43      echo "waiting for $process to start $count ..."
 44      sleep 1
 45      ((count++))
 46    done
 47    if pgrep -q $process; then
 48      echo "$process started OK ..."
 49    else
 50      error_exit "$process failed to start"
 51    fi
 52  fi
 53}
 54
 55dnsstart()
 56{
 57  if pgrep -q dnsmasq; then
 58    echo "dnsmasq is already started ..."
 59    exit 0
 60  fi
 61
 62  if [ ! -f "/usr/local/opt/dnsmasq/homebrew.mxcl.dnsmasq.plist" ]; then
 63    error_exit "Cannot start dnsmasq -- homebrew.mxcl.dnsmasq.plist is missing ..."
 64  fi
 65
 66  sudo cp /usr/local/opt/dnsmasq/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons/
 67  if [ "$?" != "0" ]; then
 68    error_exit "homebrew.mxcl.dnsmasq.plist copy failed ..."
 69  fi
 70
 71  sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist > /dev/null 2>&1
 72  pwait dnsmasq start
 73}
 74
 75dnsstop()
 76{
 77  if ! pgrep -q dnsmasq; then
 78    echo "dnsmasq is already stopped ..."
 79    exit 0
 80  fi
 81
 82  sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist > /dev/null 2>&1
 83  pwait dnsmasq stop
 84
 85  # Don't start automatically
 86  sudo rm -f /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
 87}
 88
 89dnsrestart()
 90{
 91  # First, stop dnsmasq ...
 92  if ! pgrep -q dnsmasq; then
 93    echo "dnsmasq is already stopped, so just start it up ..."
 94    dnsstart
 95    exit 0
 96  fi
 97
 98  sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist > /dev/null 2>&1
 99  pwait dnsmasq stop
100
101  # Then, start dnsmasq ...
102  sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist > /dev/null 2>&1
103  pwait dnsmasq start
104}
105
106dnsstatus()
107{
108  # Get info about running dnsmasq processes
109  ps -axo user,pid,start,etime,time,nice,vsz,rss,command |
110    egrep '^USER|dnsmasq' | sed '/grep/d'
111}
112
113case $action in
114  "start")
115    dnsstart
116    ;;
117  "stop")
118    dnsstop
119    ;;
120  "restart")
121    dnsrestart
122    ;;
123  "status")
124    dnsstatus
125    ;;
126  *)
127    # Should never happen ...
128    error_exit "Invalid action ..."
129    ;;
130esac
131
132exit 0

The following shows how to start up dnsmasq with the dnsctl shell script:

 1$ dnsctl
 2Usage: dnsctl start|stop|restart|status
 3
 4$ dnsctl start
 5Password:
 6dnsmasq started OK ...
 7
 8$ dnsctl status 
 9USER               PID STARTED     ELAPSED      TIME NI      VSZ    RSS COMMAND
10nobody           37845  3:16PM       00:10   0:00.01  0  4427444   1060 /usr/local/opt/dnsmasq/sbin/dnsmasq --keep-in-foreground -C /usr/local/etc/dnsmasq.conf -7 /usr/local/etc/dnsmasq.d,*.conf
11
12$ ping -c1 anydoman.tst
13PING anydoman.tst (127.0.0.1): 56 data bytes
1464 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.025 ms
15
16--- anydoman.tst ping statistics ---
171 packets transmitted, 1 packets received, 0.0% packet loss
18round-trip min/avg/max/stddev = 0.025/0.025/0.025/0.000 ms

Try it out! You can ping any domain that you can dream of (if it ends in .tst). The dnsctl script doesn't change file permissions like the sudo brew services ... command will do (when using sudo). Also, dnsutl tries to verify that the service successfully started (or stopped). I have seen cases where the brew services command said that the service started, but it actually didn't (or died right away).

Enhanced Dnsmasq Configuration

All of the above is still good. However, I learned that there are certain applications, such as the WordPress Site Health check, that do not work with the resolver that we set up at /etc/resolver/tst. The WordPress Site Health check will report critical errors that are failing with this error:

Error: cURL error 6: Could not resolve: yourdomain.tst (Domain name not found) (http_request_failed)

I learned that this error is coming from the brew-installed curl-openssl program. You may not have WordPress installed. Another way to reproduce this issue is with the built-in host command:

 1$ host altoplace.tst         
 2Host altoplace.tst not found: 3(NXDOMAIN)
 3
 4$ ping -c1 altoplace.tst
 5PING altoplace.tst (127.0.0.1): 56 data bytes
 664 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.021 ms
 7
 8--- altoplace.tst ping statistics ---
 91 packets transmitted, 1 packets received, 0.0% packet loss
10round-trip min/avg/max/stddev = 0.021/0.021/0.021/0.000 ms

The host command fails, but the ping command still works. There is a simple solution. We need to update the MacOS Network DNS setting to add the dnsmasq server IP, which is 127.0.0.1. This can be done through MacOS System Preferences Network panel (click on Advanced... and then the DNS tab). However, we can use the command line to easily change the MacOS DNS setting:

1networksetup -setdnsservers Ethernet 127.0.0.1 192.168.1.1
2networksetup -setdnsservers Wi-Fi 127.0.0.1 192.168.1.1

A couple of comments. The 127.0.0.1 address must be first. The second address(es) should be a list of your current DNS addresses. You can look at the Network Panel before executing the above commands to determine what those addresses are. If the address is grayed-out, as it was in my case, it means that the DNS address was automatically setup by the DHCP service. You only need to execute the above command for the network interfaces that you are using. In my case, I have setup both Ethernet and Wi-Fi (defaults to using Ethernet).

Now, you can try the same host command and see that the issue is resolved:

1$ host altoplace.tst                                        
2altoplace.tst has address 127.0.0.1

Also, with this updated configuration, DNS queries are now being cached by dnsmasq:

 1$ dig +noall +stats altoplace.net
 2;; Query time: 53 msec
 3;; SERVER: 127.0.0.1#53(127.0.0.1)
 4;; WHEN: Thu May 13 15:31:07 CDT 2021
 5;; MSG SIZE  rcvd: 58
 6
 7$ dig +noall +stats altoplace.net
 8;; Query time: 0 msec
 9;; SERVER: 127.0.0.1#53(127.0.0.1)
10;; WHEN: Thu May 13 15:31:17 CDT 2021
11;; MSG SIZE  rcvd: 58

Note: The second Query time is 0 msec. It's not a big savings, but I guess that it might add up over time. Anyway, the critical WordPress Site Health check issues are resolved. And a final note, if you do make this network DNS update, the /etc/resolver/tst file is no longer needed and can be safely removed.

Creating our First Virtual Host

We first need to make some additional changes to our Apache configuration file and restart Apache:

 1$ cd /usr/local/etc/httpd
 2
 3$ vi httpd.conf
 4
 5$ diff ~/orig/usr/local/etc/httpd/httpd.conf .
 6 o o o
 7175c175
 8< #LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so
 9---
10> LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so
11182c182
12< #LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
13---
14> LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
15 o o o
16507c514
17< #Include /usr/local/etc/httpd/extra/httpd-vhosts.conf
18---
19> Include /usr/local/etc/httpd/extra/httpd-vhosts.conf

The above configuration changes enable the vhosts module and, while we were at it, the rewrite module. Now, we can add our virtual hosts to httpd-vhosts.conf:

 1$ cd /usr/local/etc/httpd/extra
 2
 3$ vi httpd-vhosts.conf
 4
 5# Delete the example virtual hosts and add:
 6
 7<VirtualHost *:80>
 8    DocumentRoot "/Users/george/Sites"
 9    ServerName localhost
10</VirtualHost>
11
12<VirtualHost *:80>
13    DocumentRoot "/Users/george/Sites/altoplace.tst"
14    ServerName altoplace.tst
15</VirtualHost>
16
17<VirtualHost *:80>
18    DocumentRoot "/Users/george/Sites"
19    ServerName imac1.local
20</VirtualHost>

Check you configuration changes:

 1$ apachectl configtest 
 2Syntax OK
 3
 4# Restart Apache
 5# a2ctl may prompt for your system password depending on the
 6# last time you used sudo ...
 7
 8$ a2ctl restart
 9httpd stopped OK ...
10httpd started OK ...

Let me try to explain my changes. Since we turned on virtual hosts, I added our top-level ~/Sites directory as a virtual host. I defined my new altoplace.tst virtual host. And I added a special virtual host, imac1.local, to my vhosts file. The imac1.local domain is my Mac domain that is reachable from any system on my local network. It also points to my top-level ~/Sites directory. You can now add an index.html file and a info.php file to your new vhost directory (e.g., ~/Sites/altoplace.tst) and test that your new Virtual Host is working (http://altoplace.tst).

Setting up SSL (TLS)

SSL is the last topic that I am going to discuss in this post. It adds the ability to test SSL enabled virtual hosts (e.g., https://altoplace.tst). This is an important topic since the use of SSL is highly encouraged and is inexpensive to do. These days, you will see a new term, TLS, replacing SSL. You can read about SSL/TLS at the SSL link that I just provided. They are both protocols for establishing authenticated and encrypted links between networked computers.

Updating httpd.conf and extra/httpd-ssl.conf to support SSL

We just need to make a few more changes to our Apache httpd.conf and extra/httpd-ssl.conf configuration files to support the use of SSL:

 1# Update httpd.conf ...
 2
 3$ diff ~/orig/usr/local/etc/httpd/httpd.conf .
 4 o o o
 592c92
 6< #LoadModule socache_shmcb_module lib/httpd/modules/mod_socache_shmcb.so
 7---
 8> LoadModule socache_shmcb_module lib/httpd/modules/mod_socache_shmcb.so
 9 o o o
10150c150
11< #LoadModule ssl_module lib/httpd/modules/mod_ssl.so
12---
13> LoadModule ssl_module lib/httpd/modules/mod_ssl.so
14 o o o
15524c531
16< #Include /usr/local/etc/httpd/extra/httpd-ssl.conf
17---
18> Include /usr/local/etc/httpd/extra/httpd-ssl.conf
19
20# Update extra/httpd-ssl.conf ...
21
22$ cd /usr/local/etc/httpd/extra               
23
24$ cp httpd-ssl.conf ~/orig/usr/local/etc/httpd/extra
25
26$ vi httpd-ssl.conf
27
28$ diff ~/orig/usr/local/etc/httpd/extra/httpd-ssl.conf .
2936c36
30< Listen 8443
31---
32> Listen 443
33121c121
34< <VirtualHost _default_:8443>
35---
36> <VirtualHost _default_:443>
37124,125c124,125
38< DocumentRoot "/usr/local/var/www"
39< ServerName www.example.com:8443
40---
41> #DocumentRoot "/usr/local/var/www"
42> #ServerName www.example.com:8443
43144c144
44< SSLCertificateFile "/usr/local/etc/httpd/server.crt"
45---
46> SSLCertificateFile "/usr/local/etc/httpd/ssl/localhost+2.pem"
47154c154
48< SSLCertificateKeyFile "/usr/local/etc/httpd/server.key"
49---
50> SSLCertificateKeyFile "/usr/local/etc/httpd/ssl/localhost+2-key.pem"

We just uncommented two LoadModule lines to enable SSL. We also uncommented and updated the httpd-ssl.conf include file (don't try to restart Apache yet). We changed the Homebrew defined SSL port number (8443) back to the default SSL port number (443). We still need to create the SSL certificate and key files before restarting Apache.

Updating our Virtual Hosts to use SSL

We need to update our Virtual Hosts configuration to add SSL-enabled virtual hosts to our Apache configuration:

 1$ cd /usr/local/etc/httpd/extra
 2
 3$ vi httpd-vhosts.conf
 4
 5# Add the following ...
 6
 7<VirtualHost *:443>
 8    DocumentRoot "/Users/george/Sites"
 9    ServerName localhost
10    SSLEngine on
11    SSLCertificateFile "/usr/local/etc/httpd/ssl/localhost+2.pem"
12    SSLCertificateKeyFile "/usr/local/etc/httpd/ssl/localhost+2-key.pem"
13</VirtualHost>
14
15<VirtualHost *:443>
16    DocumentRoot "/Users/george/Sites"
17    ServerName imac1.local
18    SSLEngine on
19    SSLCertificateFile "/usr/local/etc/httpd/ssl/imac1.local.pem"
20    SSLCertificateKeyFile "/usr/local/etc/httpd/ssl/imac1.local-key.pem"
21</VirtualHost>
22
23<VirtualHost *:443>
24    DocumentRoot "/Users/george/Sites/altoplace.tst"
25    ServerName altoplace.tst
26    SSLEngine on
27    SSLCertificateFile "/usr/local/etc/httpd/ssl/altoplace.tst.pem"
28    SSLCertificateKeyFile "/usr/local/etc/httpd/ssl/altoplace.tst-key.pem"
29</VirtualHost>

Again, we still are not ready to restart Apache. We will now create the local SSL certificate and key files referenced in the httpd-vhosts.conf and extra/httpd-ssl.conf files.

Creating the local SSL certificates and keys

We could use OpenSSL to create a self-signed certificate. But instead, we are going to use mkcert to create our SSL certificates. It is very easy to use to create locally trusted development certificates. We can use Homebrew to install mkcert. Here's how to install and use mkcert:

 1$ brew install mkcert nss
 2
 3$ mkcert -install                                           
 4Created a new local CA ๐Ÿ’ฅ
 5Sudo password:
 6The local CA is now installed in the system trust store! โšก๏ธ
 7The local CA is now installed in the Firefox trust store (requires browser restart)! ๐ŸฆŠ
 8
 9$ cd /usr/local/etc/httpd
10
11$ mkdir ssl
12
13$ cd ssl
14
15$ mkcert localhost 127.0.0.1 ::1
16
17Created a new certificate valid for the following names ๐Ÿ“œ
18 - "localhost"
19 - "127.0.0.1"
20 - "::1"
21
22The certificate is at "./localhost+2.pem" and the key at "./localhost+2-key.pem" โœ…
23
24It will expire on 13 August 2023 ๐Ÿ—“
25
26$ mkcert altoplace.tst
27
28Created a new certificate valid for the following names ๐Ÿ“œ
29 - "altoplace.tst"
30
31The certificate is at "./altoplace.tst.pem" and the key at "./altoplace.tst-key.pem" โœ…
32
33It will expire on 13 August 2023 ๐Ÿ—“
34
35$ mkcert imac1.local
36
37Created a new certificate valid for the following names ๐Ÿ“œ
38 - "imac1.local"
39
40The certificate is at "./imac1.local.pem" and the key at "./imac1.local-key.pem" โœ…
41
42It will expire on 13 August 2023 ๐Ÿ—“

The nss formal provides libraries that Firefox needs for using SSL. We ran mkcert -install one time to install a locally trusted certificate in our system trust store. Then we created a certificate for localhost. The last two examples shows how easy it is to create additional certificates for locally hosted domains. Of course, you will use your own local development domains. At this point, you are ready to restart Apache and test your newly SSL-enabled domains. I would suggest running the Apache configuration test first just to verify that you didn't make any configuration errors. I certainly did find errors the first time around:

 1$ apachectl configtest                                  
 2Syntax OK
 3
 4$ a2ctl restart
 5Password:
 6httpd stopped OK ...
 7httpd started OK ...
 8
 9# Now test your SSL URLs:
10
11# https://localhost
12# https://imac1.local
13# https://altoplace.tst

We ran the Apache configuration test, restarted Apache, and for good measure, we verified that our httpd processes were running (and had just started). If you encounter any errors, be sure to check your Apache error log (_/usr/local/var/log/httpd/error_log_).

Final Thoughts

At this point, you should have a basic Apache/PHP-FPM working environment, supporting SSL-enabled virtual hosts. You can start testing website environments, such as Grav or Hugo, that don't require a MySQL database. I will write a different post about setting up MySQL to complete our local development LAMP stack (but for the Mac instead of Linux) environment. Then I will write about how I installed WordPress using WP-CLI. All coming later. Please stay tuned ...