OS X: Automatic multi-version PHP + Nginx dev stack

This is a pretty old post. I haven't used this for a long time, and I'm not sure it still works the way it did. Since Docker got a huge popularity, you'll probably find a better tutorial with that. Who installs PHP on a host machine nowadays anyway, huh? 😁

Hey there! After a long time I have managed myself to provide you my current local php development setup. Why you should be interested? Well, it has the following features:

  • Based on a domain, it runs the code under php 5.6 or php 7.0. Running project on different php version is as easy as changing example.dev5 to example.dev7, without a need to change a single line in config. Other php versions are really easy to add.
  • It has support for common php frameworks as well as for custom directory structures. By default, it knows how to run Symfony 2, Laravel & Nette project.
  • Running new project is as easy as checking it out to ~/Sites/project directory and running it at project.dev7.

Best of all? Setting this up takes you only few minutes.

Step one — Dnsmasq

You will need to setup local DNS server, to point *.devX requests to localhost. You will find instructions how to setup dnsmasq on OS X in this Passing Curiosity article.

Are you done? Great! Now update your /usr/local/etc/dnsmasq.conf file like this:

address=/.dev5/127.0.0.1
address=/.dev7/127.0.0.1

This will allow us to handle multiple domains in next step. Specifically dev5 and dev7. Save the file and restart the dnsmasq by running sudo brew services restart dnsmasq. Sudo is important here as it’s system service. Now let’s handle these domains in nginx.

Step two — PHP-FPM

First, run this command to make php packages available in homebrew: brew tap homebrew/homebrew-php

Then install PHP 7 using brew: brew install php70. Now, open /usr/local/etc/php/7.0/php-fpm.conf and update it with this. Make sure you replace user value with your OS X username.

[global]
error_log = /usr/local/etc/php/7.0/logs/php-fpm.log
daemonize = no
[www]
user = wod
group = staff
listen = 127.0.0.1:9001
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[error_log] = /usr/local/etc/php/7.0/logs/error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M

I will not go into details of this, only important line is the one setting listen to localhost at port 9001. This needs to be unique across your php versions.

Also, make sure to create logs directory:

mkdir /usr/local/etc/php/7.0/logs

Now, continue by installing php 5.6: brew install php56. and configuring it in similar way (/usr/local/etc/php/5.6/php-fpm.conf). Again, remember to replace wod with your username.

[global]
error_log = /usr/local/etc/php/5.6/logs/php-fpm.log
daemonize = no
[www]
user = wod
group = staff
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[error_log] = /usr/local/etc/php/5.6/logs/error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M

Notice that we run this one on port 9000.

Again, create logs directory:

mkdir /usr/local/etc/php/5.6/logs/

Don’t forget to restart services: brew services restart php56 and brew services restart php70 .

Step 3 — Nginx

Install nginx: brew install nginx and update the /usr/local/etc/nginx/nginx.conf. Again, you need to replace wod with your username.

worker_processes  1;
user wod staff;
error_log  /usr/local/etc/nginx/logs/error.log debug;
events {
    worker_connections  1024;
}
http {
    include             mime.types;
    default_type        application/octet-stream;
    client_max_body_size 10M;
log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
access_log  /usr/local/etc/nginx/logs/access.log  main;
    error_log   /usr/local/etc/nginx/logs/wildcard-error.log debug;
index index.html index.php app_dev.php;
proxy_connect_timeout       600;
    proxy_send_timeout          600;
    proxy_read_timeout          600;
    send_timeout                600;
    fastcgi_read_timeout        600;
server {
        listen 80;
set $domain $host;
# defaults to php 5.6
        set $passto "127.0.0.1:9000";
# update with your username!
        set $baseroot "/Users/wod/Sites";
set $index "index.php";
# example.dev5 should go to example directory
        if ($domain ~ "^(.[^.]*)\.dev5$") {
            set $domain $1;
            set $servername "${domain}.dev5";
        }
# whatever.example.dev5 should still go to example directory
        if ($domain ~ "^(.[^.]*).(.[^.]*)\.dev5$") {
            set $domain $2;
            set $servername "$1.${domain}.dev5";
        }
# example.dev7 to example
        if ($domain ~ "^(.[^.]*)\.dev7$") {
            set $domain $1;
            set $passto "127.0.0.1:9001";
            set $servername "${domain}.dev7";
        }
# whatever.example.dev7 to example
        if ($domain ~ "^(.[^.]*).(.[^.]*)\.dev7$") {
            set $domain $2;
            set $passto "127.0.0.1:9001";
            set $servername "$1.${domain}.dev7";
        }
set $root "${baseroot}/$domain";
# Nette Sandbox structure
        if (-d "${root}/www"){
            set $root "${root}/www";
            set $index "index.php";
        }
# Laravel structure
        if (-d "${root}/public"){
            set $root "${root}/public";
            set $index "index.php";
        }
# Symfony app
        if (-d "${root}/web"){
            set $root "${root}/web";
            set $index "app_dev.php";
        }
server_name $servername sandbox.dev;
root $root;
        access_log  /usr/local/etc/nginx/logs/$domain-access.log  main;
charset utf-8;
location / {
            try_files $uri $uri/ /$index$is_args$args;
        }
location = /favicon.ico { access_log off; log_not_found off; }
        location = /robots.txt  { access_log off; log_not_found off; }
location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            fastcgi_pass $passto;
            fastcgi_index $index;
            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
 }

Important parts have description in comment above, basically what we are doing here is that based on domain we set some variables. That’s all.

Save & restart nginx: sudo brew services restart nginx. sudo is required for nginx because it listens at port 80.

Well, I think we are done — you should be able to access project in ~/Sites/project directory at project.dev5 and project.dev7 now.

Please leave me a response if you are having any troubles following these steps.

Thank you for taking time to go through my post!