OS X 10.9 Local Development Environment: Apache, PHP, and MySQL with Homebrew
code {display:inline;padding:0;margin:0;border:none;}
There's nothing quite like setting up everything on your Mac for Drupal (or other PHP) development in a way that things just work and don't need constant fiddling. This guide will walk you through using Homebrew to install Apache, PHP, and MySQL for a "MAMP" development environment. We'll also use DNSMasq and Apache's VirtualDocumentRoot to set up "auto-VirtualHosts," and add a firewall rule to allow the default http port 80 to be used without running Apache as root.
At the conclusion of this guide, you'll be able to create a directory like ~/Sites/project and access it at http://project.dev without editing your /etc/hosts file or editing any Apache configuration. You'll also be able to use xip.io with auto-VirtualHosts for accessing your sites on other devices in your local network.
We also configure PHP and MySQL to allow for enough flexibility for complex operations generally only reserved for development and not production.
The OS X operating system comes with Apache and PHP pre-installed, and I've previously recommended utilizing them to some degree for getting a local PHP development environment on your Mac. Since then, the community around Homebrew has improved dramatically and I now recommend that our developers at Echo & Co. use Homebrew exclusively for all components.
Homebrew Setup
If you've not already install Homebrew, you can follow the instructions at brew.sh, or simply run the following command:
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
brew doctor
brew update
Also of note, if you do not have git
available on your system, either from Homebrew, Xcode, or another source, you can install it with Homebrew now (if you already have it installed, feel free to skip this step to keep the version of git
you already have):
brew install -v git
PATH Variable
Since OS X already comes with PHP and Apache, we'll want to make sure that our brew
-installed versions appear in the shell path before the built-in ones. The following command adds logic to your ~/.bash_profile to ensure the Homebrew directory is in the beginning of $PATH.
echo "export PATH=\$(echo \$PATH | sed 's|/usr/local/bin||; s|/usr/local/sbin||; s|::|:|; s|^:||; s|(.*)|/usr/local/bin:/usr/local/sbin:\1|')" >> ~/.bash_profile && source ~/.bash_profile
MySQL
The following commands will download and install the latest version of MySQL and do some basic configuration to allow for large imports and a couple other miscellaneous configuration changes.
brew install -v mysql
cp -v $(brew --prefix mysql)/support-files/my-default.cnf $(brew --prefix mysql)/my.cnf
cat >> $(brew --prefix mysql)/my.cnf <<'EOF'
# EchoDitto changes
max_allowed_packet = 2G
innodb_file_per_table = 1
EOF
sed -i '' 's/^# (innodb_buffer_pool_size)/\1/' $(brew --prefix mysql)/my.cnf
Now we need to start MySQL using OS X's launchd, and we'll set it to start when you login.
[ ! -d ~/Library/LaunchAgents ] && mkdir -v ~/Library/LaunchAgents
[ -f $(brew --prefix mysql)/homebrew.mxcl.mysql.plist ] && ln -sfv $(brew --prefix mysql)/homebrew.mxcl.mysql.plist ~/Library/LaunchAgents/
[ -e ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist ] && launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist
By default, MySQL's root user has an empty password from any connection. You are advised to run mysql_secure_installation
and at least set a password for the root user.
$(brew --prefix mysql)/bin/mysql_secure_installation
Apache
Start by stopping the built-in Apache, if it's running, and prevent it from starting on boot. This is one of very few times you'll need to use sudo
.
sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist 2>/dev/null
Apache is not in the default repository of Homebrew formulas, nor are some dependencies, so we use the brew tap
command to add other formula repositories.
brew tap homebrew/dupes
brew tap homebrew/apache
We'll install Apache 2.2 with Apr and OpenSSL from Homebrew as well instead of utilizing the built-in versions of those tools.
brew install -v httpd22 --with-brewed-apr --with-brewed-openssl
We'll be using the file ~/Sites/httpd-vhosts.conf to configure our VirtualHosts, so we create necessary directories and then include the file in httpd.conf.
[ ! -d ~/Sites ] && mkdir -pv ~/Sites
touch ~/Sites/httpd-vhosts.conf
USERHOME=$(dscl . -read /Users/<code>whoami</code> NFSHomeDirectory | awk -F": " '{print $2}') cat >> $(brew --prefix)/etc/apache2/2.2/httpd.conf <<EOF
# Include our VirtualHosts
Include ${USERHOME}/Sites/httpd-vhosts.conf
EOF
We'll create a folder for our logs in ~/Sites as well.
[ ! -d ~/Sites/logs ] && mkdir -pv ~/Sites/logs
Now to fill in the contents of ~/Sites/httpd-vhosts.conf that we included in httpd.conf earlier. Note that this is one command to copy and paste! Start with USERHOME
through the second EOF
has a single copy and paste block for the terminal.
USERHOME=$(dscl . -read /Users/<code>whoami</code> NFSHomeDirectory | awk -F": " '{print $2}') cat > ~/Sites/httpd-vhosts.conf <<EOF
#
# Use name-based virtual hosting.
#
NameVirtualHost *:80
#
# Set up permissions for VirtualHosts in ~/Sites
#
<Directory "${USERHOME}/Sites">
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
Allow from all
</Directory>
# For http://localhost in the users' Sites folder
<VirtualHost _default_:80>
ServerName localhost
DocumentRoot "${USERHOME}/Sites"
</VirtualHost>
#
# VirtualHosts
#
## Manual VirtualHost template
#<VirtualHost *:80>
# ServerName project.dev
# CustomLog "${USERHOME}/Sites/logs/project.dev-access_log" combined
# ErrorLog "${USERHOME}/Sites/logs/project.dev-error_log"
# DocumentRoot "${USERHOME}/Sites/project.dev"
#</VirtualHost>
#
# Automatic VirtualHosts
# A directory at ${USERHOME}/Sites/webroot can be accessed at http://webroot.dev
# In Drupal, uncomment the line with: RewriteBase /
# This log format will display the per-virtual-host as the first field followed by a typical log line
LogFormat "%V %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combinedmassvhost
# Auto-VirtualHosts with .dev
<VirtualHost *:80>
ServerName dev
ServerAlias *.dev
CustomLog "${USERHOME}/Sites/logs/dev-access_log" combinedmassvhost
ErrorLog "${USERHOME}/Sites/logs/dev-error_log"
VirtualDocumentRoot ${USERHOME}/Sites/%-2+
</VirtualHost>
# Auto-VirtualHosts with xip.io
<VirtualHost *:80>
ServerName xip
ServerAlias *.xip.io
CustomLog "${USERHOME}/Sites/logs/dev-access_log" combinedmassvhost
ErrorLog "${USERHOME}/Sites/logs/dev-error_log"
VirtualDocumentRoot ${USERHOME}/Sites/%-7+
</VirtualHost>
EOF
Run with port 80
You may notice that httpd.conf is running Apache on port 8080, but the above are using port 80. The next two commands will create and load a firewall rule to forward 8080 requests to 80. The end result is that we can use port 80 in VirtualHosts without needing to run Apache as root.
The following single command will create the file /Library/LaunchDaemons/com.echoditto.httpdfwd.plist as root, and owned by root, since it needs elevated privileges.
sudo bash -c 'export TAB=$'"'"'\t'"'"'
cat > /Library/LaunchDaemons/com.echoditto.httpdfwd.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
${TAB}<key>Label</key>
${TAB}<string>com.echoditto.httpdfwd</string>
${TAB}<key>ProgramArguments</key>
${TAB}<array>
${TAB}${TAB}<string>sh</string>
${TAB}${TAB}<string>-c</string>
${TAB}${TAB}<string>ipfw add fwd 127.0.0.1,8080 tcp from any to me dst-port 80 in && sysctl -w net.inet.ip.forwarding=1</string>
${TAB}</array>
${TAB}<key>RunAtLoad</key>
${TAB}<true/>
${TAB}<key>UserName</key>
${TAB}<string>root</string>
</dict>
</plist>
EOF'
This file will be loaded on login and set up the 80->8080 port forward, but we can load it manually now so we don't need to log out and back in.
sudo launchctl load -w /Library/LaunchDaemons/com.echoditto.httpdfwd.plist
PHP
The following is for the latest release of PHP, version 5.5. If you'd like to use 5.3, 5.4 or 5.6, simply change the "5.5" and "php55" values below appropriately. (Note: if you use 5.3, the OpCache extension instructions are different. They will be posted below after the instructions for newer versions.)
Start by adding the PHP tap for Homebrew. PHP 5.3 needs an additional tap, so skip the second command if you are using 5.4 or higher.
brew tap homebrew/php
# Skip this if using PHP 5.4 or higher
brew tap homebrew/versions
Install PHP and mod_php. This command will also load the PHP module in the httpd.conf file for you.
brew install -v php55 --homebrew-apxs --with-apache
Add PHP configuration to Apache's httpd.conf file.
cat >> $(brew --prefix)/etc/apache2/2.2/httpd.conf <<EOF
# Send PHP extensions to mod_php
AddHandler php5-script .php
AddType text/html .php
DirectoryIndex index.php index.html
EOF
Set timezone and change other PHP settings (this is a single command). sudo
is needed here to get the current timezone on OS X (in previous versions of OS X it wasn't needed, I'm not sure why it is now).
sed -i '-default' "s|^;(date.timezone[[:space:]]<em>=).</em>|\1 \"$(sudo systemsetup -gettimezone|awk -F": " '{print $2}')\"|; s|^(memory_limit[[:space:]]<em>=).</em>|\1 256M|; s|^(post_max_size[[:space:]]<em>=).</em>|\1 200M|; s|^(upload_max_filesize[[:space:]]<em>=).</em>|\1 100M|; s|^(default_socket_timeout[[:space:]]<em>=).</em>|\1 600|; s|^(max_execution_time[[:space:]]<em>=).</em>|\1 300|; s|^(max_input_time[[:space:]]<em>=).</em>|\1 600|;" $(brew --prefix)/etc/php/5.5/php.ini
Add a PHP error log; without this, you may get Internal Server Errors if PHP has errors to write and no logs to write to (this is a single command; be sure to copy and paste the lines containing USERHOME
through the last EOF
as a single command).
USERHOME=$(dscl . -read /Users/<code>whoami</code> NFSHomeDirectory | awk -F": " '{print $2}') cat >> $(brew --prefix)/etc/php/5.5/php.ini <<EOF
; PHP Error log
error_log = ${USERHOME}/Sites/logs/php-error_log
EOF
This weird little "hack" is needed to fix a permissions problem with using pear
or pecl
.
touch $(brew --prefix php55)/lib/php/.lock && chmod 0644 $(brew --prefix php55)/lib/php/.lock
The OpCache extension will speed up your PHP environment dramatically, and it's easy to install for 5.4 and higher. If you are looking to install PHP 5.3, skip this block.
brew install -v php55-opcache
Skip this block unless you are using PHP 5.3. Because there is no php53-opcache Homebrew formula, we can install it with pecl
and replicate the same configuration file.
pecl install zendopcache-beta
sed -i '' "s|^(zend_extension=\")(opcache.so\")|\1$(php -r 'print(ini_get("extension_dir")."/");')\2|" $(brew --prefix)/etc/php/5.3/php.ini
echo "[opcache]" > $(brew --prefix)/etc/php/5.3/conf.d/ext-opcache.ini
grep -E '^zend_extension.<em>opcache.so' $(brew --prefix)/etc/php/5.3/php.ini >> $(brew --prefix)/etc/php/5.3/conf.d/ext-opcache.ini
sed -i '' '/^zend_extension.</em>opcache.so/d' $(brew --prefix)/etc/php/5.3/php.ini
# "php54" is not a typo here- I'm using a sample config file from
# another recipe for my config file in php53
grep -E '^[[:space:]]<em>opcache.' \
$(brew --prefix)/Library/Taps/homebrew/homebrew-php/Formula/php54-opcache.rb \
| sed 's/^[[:space:]]</em>//g' >> $(brew --prefix)/etc/php/5.3/conf.d/ext-opcache.ini
And continue with steps for all PHP versions: give OpCache some more memory to keep more opcode caches.
sed -i '' "s|^(opcache.memory_consumption=)[0-9]*|\1256|;" $(brew --prefix)/etc/php/5.5/conf.d/ext-opcache.ini
Start Apache
Start Homebrew's Apache and set to start on boot.
ln -sfv $(brew --prefix httpd22)/homebrew.mxcl.httpd22.plist ~/Library/LaunchAgents
launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.httpd22.plist
DNSMasq
I've covered this before, but we'll list the commands here again for completeness. This example will have any DNS request ending in .dev reply with the IP address 127.0.0.1.
brew install -v dnsmasq
echo 'address=/.dev/127.0.0.1' > $(brew --prefix)/etc/dnsmasq.conf
echo 'listen-address=127.0.0.1' >> $(brew --prefix)/etc/dnsmasq.conf
Because DNS services run on a lower port, we need to have this run out of /Library/LaunchDaemons, so we do need to use sudo
for the initial setup.
sudo cp -v $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons
sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
With DNSMasq running, configure OS X to use your local host for DNS queries ending in .dev
sudo mkdir -v /etc/resolver
sudo bash -c 'echo "nameserver 127.0.0.1" > /etc/resolver/dev'
Great! So, what did I do?
We set up Apache to run on boot on port 8080 with mod_php with auto-VirtualHosts for directories in the ~/Sites folder. The OS X firewall will forward all port 80 traffic to port 8080, so we don't have specify the port number when visiting web pages in web browsers. MySQL is installed and set to run on boot as well. DNSMasq and some OS X configuration is used to direct any hostname ending in .dev to the local system to work in conjunction with Apache's auto-VirtualHosts.
What do I do now?
You shouldn't need to edit the Apache configuration or edit /etc/hosts for new local development sites. Simply create a directory in ~/Sites and then reference that foldername + .dev in your browser to access it.
For example, use drush
to download Drupal 7 to the directory ~/Sites/firstproject, and it can then be accessed at http://firstproject.dev/ without any additional configuration. A caveat - you will need to uncomment the line in Drupal's .htaccess containing "RewriteBase /" to work with the auto-VirtualHosts configuration.
What about this xip.io thing?
If your Mac's LAN IP address is 192.168.0.10, you can access sites from any other device on your local network using http://firstproject.192.168.0.10.xip.io/. You can test a websites on your Mac with your phone/tablet/etc. Pretty great, right?
What if this "auto-VirtualHost" doesn't work for me?
If you need to create a manual VirtualHost in Apache because the auto-VirtualHost does not work for your configuration, you may need to declare it before the auto-VirtualHosts if you're also going to use .dev as the TLD. Otherwise the auto-VirtualHost block will be accessed first.
Photo by Florian Klien