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.
- Installing Homebrew (a sane package manager for macOS)
- Installing Apache HTTP server 2.4
- Installing PHP 7 and PHP-FPM
- Putting all together
- 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
tohttpd
and the corresponding folders are also changed fromapache2
tohttpd
. 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_proxy
and 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 directivelisten
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 Apachehttpd
is listening.
- Here I use the wildcard
-
ServerName
- ServerName is recommended in each
<VirtualHost>
for resolution and matching. If absent, Apache will use the globalServerName
stated inhttpd.conf
. - In my case, I assign different
ServerName
,php7.test
andhtmljs.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 forphp7.test
andhtmljs.test
using/etc/hosts
or DNSMasq.
- ServerName is recommended in each
-
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.
- DocumentRoot specifies the place where Apache
-
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 theDocumentRoot
, we must addDocumentRoot
path before$1
to form an exact absolute path and hand it over to PHP-FPM by adding the prefixfcgi://127.0.0.1:9072
. Note that PHP-FPM is serving at127.0.0.1:9072
andfcgi://
denotes the scheme FastCGI provided bymod_proxy_fcgi
.
- Syntax:
- The project htmljs.test does not need PHP processing, therefore I do not set the directive
ProxyPassMatch
there.
- ProxyPassMatch is part of
-
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 defaultindex.html
. Note thatDirectoryIndex
can also be set globally inhttpd.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 inhttpd.conf
.
-
-
<Directory>
- encloses the settings for folders, subfolders, and their contents.
Require all granted
: access is allowed unconditionally (authorizationmod_authz_core
). We need this, otherwise Apache will return an error403 Forbidden
.
- encloses the settings for folders, subfolders, and their contents.
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.
- 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
- 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>
- Syntax:
- SetHandler: forces matching files to be processed by a handler.
- Syntax:
SetHandler handler-name|none|expression
- Syntax:
- 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