Apache httpd 2.4 and PHP 7 in macOS

Together, Apache HTTP server, PHP, and MySQL form a powerful and popular combination for Web development. MacOS are often shipped with pre-installed versions of Apache HTTP server and PHP but these are often outdated and merely customised for macOS. The well-known bundles *AMP (e.g. WAMP for Windows, LAMP for Linux, MAMP for Mac) are commonly used but also considered a tad bloated for the beginners like me ;).

After few years of Java development, I turned myself into Web development, and in particular, PHP programming. As usual, I would start with pure PHP aspects that really help me to understand the fundamental concepts and techniques instead of being drown with everyone-known frameworks and their complexity and hard-to-understand magics. As a result, I first looked for a simple setup of Apache and PHP that best suits the beginning of my initial learning path. In this note, I write down what I learn from many sources on the Internet tweaked to suit my needs.

Goal

Let’s assume that we want to set up an exemplary Web development environment including Apache HTTP 2.4 server and PHP 7.

Strategies

A typical and well-documented approach is to load PHP processor as a module under Apache httpd using the directive LoadModule. It is so-called mod_php approach. However, this is now gradually out of favor of Web developers and hosting providers because the tight combination of PHP and Apache makes things difficult for monitoring, debugging, and scaling. One of the recent favorite strategies is to set up PHP as Fast-CGI using PHP-FPM (FastCGI Process Manager). This method also brings several advantages including good support for nginx integration and performance.

Based on the aforementioned analysis of strategies, I consider the following installation activities to fulfill that goal.

  1. Installing Homebrew (a sane package manager for macOS)
  2. Installing Apache HTTP server 2.4
  3. Installing PHP 7 and PHP-FPM
  4. Putting all together
  5. Installing DNSMasq (optional)

Installation and Configuration

Homebrew

Homebrew is currently a prominent and well-supported package managing solution for macOS. Installing Homebrew is rather straightforward. Nevertheless, you should be familiar with using the command line in order to easily get thing done. All you need is to launch the Terminal app (/Applications/Utilities/Terminal.app) or even the better iTerm 2 and start executing terminal commands.

Note: From now on, the sign $ (if exists) will denote the user’s command line prompt where you will execute the terminal commands, except that you must not type $.

Homebrew requires Xcode’s command line tooling so we must install it first.

# Install Xcode command line tooling
$ xcode-select --install

Next we start installing Homebrew using a one-line command.

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

By default, Homebrew will be installed into /usr/local/Cellar and use /usr/local and its sub-folders for the its installed binary and configuration files. For more advanced settings and manual configurations, please refer here. To ensure that Homebrew is properly installed, we can simply check.

$ brew --version
Homebrew 1.3.8
Homebrew/homebrew-core (git revision 22ac3; last commit 2017-12-02)

The command brew config shows more information about the installed Homebrew.

brew config

Now we should add some extra taps (i.e. package repositories in Homebrew’s world) that contain packages we need, for instance, Apache HTTP server, PHP, and MySQL along with tools for running them as macOS services.

brew tap homebrew/apache
brew tap homebrew/php
brew tap homebrew/services

Last but not least, always make sure Homebrew is up-to-date.

brew update
brew doctor

Apache HTTP server

Installing Apache HTTP server 2.4

We can also use the pre-installed Apache HTTP server (from now on, Apache httpd, Apache, httpd will be used interchangeably) shipped with macOS. Nevertheless, I want to play around with the newer version. Apart from the Homebrew-based installation commands, the steps for setting up Apache are the same in either way.

brew install httpd

NOTE 1: Since Sep 30th, 2017 the Apache HTTP server package in Homebrew has been renamed from httpd24 to httpd and the corresponding folders are also changed from apache2 to httpd. This note has been updated with the newer versions. Apart from the aforementioned updates, the configuration steps remain the same though.

NOTE 2: When we use brew install <package_name without any further options, Homebrew often accomplishes the installation faster by downloading a bottled version (i.e. a pre-compiled package) from homebrew.bintray.com. If there are any extra compling options, Homebrew can download the package’s source code and compile the source. The compilation of a source package usually takes a a bit longer. For a simple and quick start, I mostly opt for the bottled versions of Apache server and PHP as the configured and compiled options are rather sufficient. After learning the fundamental aspects, I can turn to a more complex approach with lots of tweaks for further needs or experiments.

By default, the Homebrew-based Apache server will use the ports http/8080 and https/8443 that do not need system administrator privileges. In my setup, I will go with this option as I simply want to run Apache with my user account. Nonetheless, you can tell Homebrew to install a version that use ports 80 and 443 by the option --with-privileged-ports.

brew install httpd --with-privileged-ports

In case you want to experiment with the bleeding edge Apache server pulled from its development repository, use the option --HEAD along with other options. For example,

brew install httpd --HEAD --with-privileged-ports

Let’s assume the bottled httpd is installed. The Apache server will be installed in the following folder /usr/local/Cellar/httpd/2.4.29 (which might be different in your computer). You can check the Homebrew’s Cellar, i.e. where Homebrew puts installed packages, with brew --cellar. The precise location of httpd in a bit complex nerdy form is $(brew --cellar)/httpd/$(brew list --versions httpd | cut -d ' ' -f 2).

cd $(brew --cellar)/httpd/$(brew list --versions httpd | cut -d ' ' -f 2)

Configuring Apache server

The main configuration file httpd.conf can be located at /usr/local/etc/httpd. There are some directives and options that you may want to notice or change ( # starts a comment).

There is a default directive Listen 8080 denoting the port where Apache server will be listening. You can change it to your favourite one or the one that suits your projects. Note that if you change to a port in the range from 1-1023 (so-called privileged ports), you need an administrator role to run the httpd process. Because I need to create virtual hosts for various projects, I have to configure httpd to listen at a particular address and port, which is 127.0.0.1:8080 in my setting.

The directives LoadModule will enable some Apache HTTP modules we need for configuring PHP-FPM such as mod_proxyand mod_proxy_fcgi (disabled by default).

Listen 127.0.0.1:8080
...
# LoadModule proxy_module libexec/mod_proxy.so
...
# LoadModule proxy_fcgi_module libexec/mod_proxy_fcgi.so
...

Apache server can be started by using either apachectl start or brew services start httpd. Note that the latter also installs a snippet to run Apache httpd as a startup service.

brew services start httpd
brew services list

Running and Testing

We use the following commands to start, stop, or restart httpd, respectively.

brew services start httpd
brew services stop httpd
brew services restart httpd

To see whether Apache httpd is up and running, you just start your Web browser and point to the URL http://localhost:8080 and should see a simple Web page saying “It works”. We can also check from the command line as well.

$ launchctl list | grep httpd
91962	0	homebrew.mxcl.httpd

$ ps -ef | grep httpd
  501 91962     1   0  8:51AM ??         0:00.07 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
  501 91968 91962   0  8:51AM ??         0:00.04 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
  501 91969 91962   0  8:51AM ??         0:00.04 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
  501 91970 91962   0  8:51AM ??         0:00.04 /usr/local/opt/httpd/bin/httpd -D FOREGROUND
  
$ lsof -Pni4 | grep httpd
httpd     91962 huytran    5u  IPv4 0xd51255ae3385c3d7      0t0  TCP *:* (CLOSED)
httpd     91968 huytran    5u  IPv4 0xd51255ae3385c3d7      0t0  TCP *:* (CLOSED)
httpd     91969 huytran    5u  IPv4 0xd51255ae3385c3d7      0t0  TCP *:* (CLOSED)
httpd     91970 huytran    5u  IPv4 0xd51255ae3385c3d7      0t0  TCP *:* (CLOSED)

We can also examine whether Apache server configuration file is correct:

$ apachectl configtest
Syntax OK
$ httpd -t
Syntax OK

PHP 7

Install PHP and PHP-FPM

Likewise, I just use Homebrew to install newer/older versions of PHP ranging from 5.x to 7.x. I opt for the stable release of PHP 7.

brew install php72

The default installation of PHP 7 almost covers several useful aspects including a PHP module libphp7.so that can be integrated with Apache HTTP server via the directive LoadModule and PHP-FPM that can be used as FastCGI.

$ php -version
PHP 7.2.0 (cli) (built: Dec  2 2017 11:27:08) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2017 Zend Technologies

PHP configurations

The main configuration file of PHP is php.ini as shown below.

Configuration File (php.ini) Path: /usr/local/etc/php/7.2
Loaded Configuration File:         /usr/local/etc/php/7.2/php.ini
Scan for additional .ini files in: /usr/local/etc/php/7.2/conf.d
Additional .ini files parsed:      (none)

The default setting in /usr/local/etc/php/7.2/php.ini is quite sufficient for simple development purposes. We may tweak later if necessary but can leave it for now.

Starting PHP-FPM

The configuration for PHP-FPM is /usr/local/etc/php/7.2/php-fpm.conf. Note the last lines in this file

;include=/usr/local/etc/php/7.1/php-fpm.d/*.conf

You can remove the semi-colon to enable the inclusion of other configurations and/or might also change the path to whether it fits your development environment. In the folder /usr/local/etc/php/7.1/php-fpm.d/*, there is a file www.conf that can be used as a starting point for setting up your own PHP-FPM.

Now we can start PHP-FPM to see whether everything is fine before doing some extra configuration steps.

$ brew services start php72
==> Successfully started `php72` (label: homebrew.mxcl.php72)
$ brew services stop php72
Stopping `php72`... (might take a while)
==> Successfully stopped `php72` (label: homebrew.mxcl.php72)
$ brew services restart php72

The command brew services start php72 can create the file homebrew.mxcl.php72.plist in ~/Library/LaunchAgents for starting up.

$ launchctl list | grep php
47728	0	homebrew.mxcl.php72
$ ps -ef | grep php-fpm
...

Putting all together

After basically finishing the installation of Apache, PHP 7 and PHP-FPM, we start putting these pieces together.

Configurating a ‘PHP-FPM’ pool

PHP-FPM supports multiple resource pools. Each pool defines how PHP-FPM will create and manage processes. Let’s start with enabling the inclusion and handling of pools in /usr/local/etc/php/7.2/php-fpm.conf by changing the following line

;include=/usr/local/etc/php/7.2/php-fpm.d/*.conf

to (i.e. uncommenting it)

include=/usr/local/etc/php/7.2/php-fpm.d/*.conf

Then we can leverage the stock configuration in /usr/local/etc/php/7.2/php-fpm.d/www.conf. In my case, I have cleaned up www.conf and kept a simple configuration as following for the sake of readability. The configuration explains for itself.

[www]
user = _www
group = _www
listen = 127.0.0.1:9072
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

The most important setting of each pool is the TCP socket (including IP address and port) or Unix domain socket (UDS) that PHP-FPM will be receiving FastCGI requests. This will be set using the directive listen . A typical setting of PHP-FPM is listen = 127.0.0.1:9000. As I might want to install multiple PHP versions for testing, I will change the port to 9072 that corresponds to the PHP version 7.2.

Note that we can also configure PHP-FPM to serve at a Unix domain socket (UDS) because PHP-FPM and httpd processes are running in the same host. To do that, the directive listen must be changed, for example, listen = /usr/local/var/run/php72-fpm.sock. This approach would need extra effort to configure Apache to use PHP-FPM via UDS that we will visit later near the end of this guide.

Now as I want to check whether PHP-FPM will be up and running with the above changes, I restart PHP-FPM and check the open ports 9072.

$ brew services restart php72
$ ps -ef | grep php-fpm
...
$ lsof -Pni4 | grep php-fpm
php-fpm   53180 huytran    6u  IPv4 0xd51255ae2d4097b7      0t0  TCP 127.0.0.1:9072 (LISTEN)
php-fpm   53187 huytran    0u  IPv4 0xd51255ae2d4097b7      0t0  TCP 127.0.0.1:9072 (LISTEN)
php-fpm   53188 huytran    0u  IPv4 0xd51255ae2d4097b7      0t0  TCP 127.0.0.1:9072 (LISTEN)

PHP-FPM Delegation

I can define virtual hosts in Apache that suit my needs for separating different development projects. Instead of creating one large httpd.conf, I will create separate virtual host configurations. Again, I will start with the exemplary virtual host configuration provided in /usr/local/etc/httpd/extra/httpd-vhosts.conf.

Enabling ‘mod_proxy’ and ‘mod_proxy_fcgi’

Change the following lines in /usr/local/etc/httpd/httpd.conf

# LoadModule proxy_module libexec/mod_proxy.so
...
# LoadModule proxy_fcgi_module libexec/mod_proxy_fcgi.so

to

LoadModule proxy_module libexec/mod_proxy.so
...
LoadModule proxy_fcgi_module libexec/mod_proxy_fcgi.so

Defining Virtual Hosts

I want to define a PHP development project residing in /Users/huytran/working/dev/dev-web/php. Therefore, I change /usr/local/etc/httpd/httpd.conf

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

to

Include /usr/local/etc/httpd/extra/httpd-vhosts.conf

and then create a file /usr/local/etc/httpd/extra/httpd-vhosts.conf

<VirtualHost *:8080>
    ServerName php7.test
    DocumentRoot "/Users/huytran/working/dev/dev-web/php"
    ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9072/Users/huytran/working/dev/dev-web/php/$1
    DirectoryIndex index.php index.html
	<Directory "/Users/huytran/working/dev/dev-web/php">
		Require all granted
	</Directory>
</VirtualHost>

Further information on Apache virtual hosts can be found here or here. I can briefly explain some relevant directives used in my virtual host configuration:

  • <VirtualHost>

    • is used to define a new virtual host with respect to a specific hostname or IP address.
    • Syntax: <VirtualHost addr[:port] [addr[:port]] ...> ... </VirtualHost>
      • Here I use the wildcard *:* to match any IP addresses and ports. Note that the port specified in a <VirtualHost> does not affect the real port Apache httpd is listening.
  • ServerName

    • ServerName is recommended in each <VirtualHost> for resolution and matching. If absent, Apache will use the global ServerName stated in httpd.conf.
    • In my case, I assign different ServerName, php7.test and htmljs.test, respectively, for each project to distinguish them. As Apache mainly matches virtual hosts via IP addresses and it will resolve host names based on DNS servers, I will configure corresponding hostname resolution for php7.test and htmljs.test using /etc/hosts or DNSMasq.
  • DocumentRoot

    • DocumentRoot specifies the place where Apache httpd will look for files to serve relevant incoming request. In each case, DocumentRoot will be the absolute path of my development project.
  • ProxyPassMatch

    • ProxyPassMatch is part of mod_proxy that can map remote servers into local URLs using regular expressions.
      • Syntax: ProxyPassMatch [regex] !|url [key=value [key=value ...]]
      • When Apache httpd receives a request for a certain PHP file (e.g. test.php) then it needs PHP to handle that request. The regular expression ^/(.*\.php(/.*)?)$ is used to check whether the incoming request is for a .php file.
      • $1 is called back-reference, as it refers to the matched part of the regular expression corresponding to the outermost pair of parentheses. In case the expression matches the incoming request URI (i.e. the request corresponds to a file .php, for instance, /test.php) $1 will represent the request URI. As a request is often treated as relative path from the DocumentRoot, we must add DocumentRoot path before $1 to form an exact absolute path and hand it over to PHP-FPM by adding the prefix fcgi://127.0.0.1:9072. Note that PHP-FPM is serving at 127.0.0.1:9072 and fcgi:// denotes the scheme FastCGI provided by mod_proxy_fcgi.
    • The project htmljs.test does not need PHP processing, therefore I do not set the directive ProxyPassMatch there.
  • DirectoryIndex

    • DirectoryIndex sets the list of resources for an indexing request. As I want to use PHP, I set index.php as index resource backed up by the default index.html. Note that DirectoryIndex can also be set globally in httpd.conf that affects the main server and can be inherited in all virtual hosts.

      <IfModule dir_module>
          DirectoryIndex index.php index.html
      </IfModule>
      
    • Note that DirectoryIndex index.html is the default global option defined in httpd.conf.

  • <Directory>

    • encloses the settings for folders, subfolders, and their contents.
      • Require all granted : access is allowed unconditionally (authorization mod_authz_core). We need this, otherwise Apache will return an error 403 Forbidden.

Hostname Resolution

As mentioned before, I will use the virtual host php7.test in my macOS to refer to my corresponding Web development projects. The hostname is not known by Apache. Thus, I must tell it how to resolve this hostname. In my setting, all virtual hosts are sharing in the same local computer with the address 127.0.0.1. A simple solution is to change the file /etc/hosts where macOS will look for when resolving hostnames. This solution requires sufficient administration privilege.

$ sudo open -e /etc/hosts
Password: (enter your password here)

After entering the administrator password, I will add two following lines to /etc/hosts.

127.0.0.1    php7.test

In case either you do not want to mess up system files like /etc/hosts or you have several development projects, you can leverage dnsmasq for automatically resolving hostnames with very simple configurations. Note that this is also the reason I chose the .test for all of my development projects such that I can use dnsmasq and only one line directive to resolve the TLD (Top-Level Domain) test. You can surely pick any TLD other than test .

Testing Apache and PHP

Apart from the aforementioned tests, we need to check whether the integration of Apache and PHP-FPM is working. Let’s create a file index.php in /Users/huytran/working/dev/dev-web/php

<?php
  echo 'PHP Development Project';

Restart httpd and PHP-FPM to apply these changes.

$ brew services restart httpd
$ brew services restart php72

Given no problems, the servers should be restarted smoothly. Then open your Web browser to the following URLs http://php7.test:8080. If you see the content created above, our configuration is working.

DNSMasq

Instead of altering system hosts /etc/hosts, I can make use of dnsmasq to conveniently resolve many hostnames used in my virtual hosts.

Installing DNSMasq

# update homebrew and install dnsmasq
$ brew update && brew install dnsmasq
# Copy the dnsmasq daemon configuration file to be loaded at system startup time
$ sudo cp $(brew list dnsmasq | grep /homebrew.mxcl.dnsmasq.plist$) /Library/LaunchDaemons/
# start dnsmasq
$ sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist

Configuring DNSMasq

After installing DNSMasq, let’s open the configuration file

$ open -e /usr/local/etc/dnsmasq.conf

and add the following line at the end.

address=/app/127.0.0.1

The above configuration line tells DNSMasq to resolve the TLD dev to the local IP address 127.0.0.1. You can now use any hostnames like this.is.a.development.project.test in Apache virtual hosts.

Configuring macOS to use DNSMasq

We have to tell macOS that DNSMasq is now a nameserver running at the local host. The configuration file is /etc/resolv.conf specifies the nameservers for looking up hostnames. On the one hand, we should not mess up the system setting. Moreover, macOS might overwrite /etc/resolv.conf when there are changes in Network setting. An elegant solution is to create a file in the folder /etc/resolver/with the exact TLD managed by DNSMasq.

$ sudo mkdir -p /etc/resolver/
$ sudo sh -c 'echo "nameserver 127.0.0.1" > /etc/resolver/test'

The aforementioned commands needs administrator privileges and will create a file test with the content nameserver 127.0.0.1 in the folder /etc/resolver/.

Testing DNSMasq

Now we can delete the lines added before in /etc/hosts and start dnsmasq (as root) and test the resolving of hostnames.

# start dnsmasq
$ sudo brew services start dnsmasq

# ping the hostnames
$ ping -c 1 php7.test
PING php7.test (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.040 ms

# query hostname using dnsmasq running at 127.0.0.1
$ dig php7.test @127.0.0.1
...
;; ANSWER SECTION:
php7.test.		0	IN	A	127.0.0.1

# test arbitrary hostnames ending with .test
$ dig an.arbitrary.host.test @127.0.0.1

Using Proxy FastCGI via Handler

Instead of configuring ProxyPassMatch for each individual virtual host to handle .php files, we can also set up directives in Apache httpd.conf (global scope) or at the beginning of the virtual host configuration localhost.conf (virtual host scope) via handlers.

  1. First comment out or delete the line ProxyPassMatch ... in the virtual host configuration /usr/local/etc/httpd/extra/httpd-vhosts.conf.
# ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9072/Users/huytran/working/dev/dev-web/php/$1
  1. Then, add the following lines to either the end of /usr/local/etc/httpd/httpd.conf or the beginning of /usr/local/etc/httpd/extra/httpd-vhosts.conf.
<Proxy "fcgi://localhost:9072/" enablereuse=on max=10>
</Proxy>
<FilesMatch "\.php$">
    <If "-f %{REQUEST_FILENAME}">
        SetHandler "proxy:fcgi://127.0.0.1:9072/"
    </If>
</FilesMatch>
  • <FilesMatch> : defines the scope of the enclosed directives by filenames that match the regular expression.
    • Syntax: <FilesMatch regex> ... </FilesMatch>
  • SetHandler: forces matching files to be processed by a handler.
    • Syntax: SetHandler handler-name|none|expression
  1. Restart httpd

Handling PHP via Unix Domain Sockets

As mentioned before, PHP-FPM can serve on Unix domain sockets (UDS), too. UDS is widely used in the Unix/Linux world for inter-process communication. To enable PHP-FPM listening on UDS, open file /usr/local/etc/php/7.1/php-fpm.d/www.conf and change the directive listen .

[www]
...
listen = /usr/local/var/run/php72-fpm.sock
...

Now we must change the Apache configuration to adapt to UDS as well. In case we use ProxyPassMatch directive, change it like this:

ProxyPassMatch ^/(.*\.php(/.*)?)$ unix:/usr/local/var/run/php72-fpm.sock|fcgi://127.0.0.1:9072/Users/huytran/working/dev/dev-web/php

The change is to add unix:/usr/local/var/run/php72-fpm.sock| before fcgi://... and remove the captured request URI /$1 at the end.

Similarly, if we switch to Apache handler as above, we update the Apache configuration accordingly.

<Proxy "unix:/usr/local/var/run/php72-fpm.sock|fcgi://localhost:9072/" enablereuse=on max=10>
</Proxy>
<FilesMatch "\.php$">
    <If "-f %{REQUEST_FILENAME}">
        SetHandler "proxy:unix:/usr/local/var/run/php72-fpm.sock|fcgi://127.0.0.1:9072/"
    </If>
</FilesMatch>

We should ensure the configurations are good and then restart the servers afterwards.

# check Apache config
$ apachectl -t
Syntax OK
# check php-fpm config
$ php-fpm -t
[30-Sep-2017 19:39:46] NOTICE: configuration file /usr/local/etc/php/7.2/php-fpm.conf test is successful
# restart servers
$ brew services restart httpd
$ brew services restart php72
comments powered by Disqus