Em's Site

Serving local WordPress sites on macOS with nginx and php-fpm

I switch between Windows and macOS for developing WordPress sites. Normally I use Windows because my Windows machine is more powerful and can drive two monitors, whereas my MacBook Air is the 13-inch 2017 model (that I bought last year because it's the last model with MagSafe power and a proper keyboard). However, I will occasionally use my Mac when I'm testing sites on Safari or an iOS emulator.

On both my Windows machine and MacBook, I use nginx, PHP, and MariaDB to serve my WordPress sites. Today I'll show you how I set it up on a Mac.

"But Em," I hear you say, "Why do all this low-level stuff when there are tools like Docker, LocalWP, MAMP, etc., that are way easier and more intuitive?"

Well, firstly, because I can. Secondly, I was a Linux sysadmin for about 3 years, running WordPress hosting. As it turns out, once I knew how to use low-level programs, I realized that it's actually easier to configure and maintain my setup the way I want to do it, rather than the way other people make me use their tools. I've tried Docker, Local, and MAMP before, and the limitations and workarounds I have to use frustrate me, and I end up going back to this setup because I have total flexibility and control over how it works.

Disclaimers and assumptions

  1. Some parts of this setup are bad practice from a security standpoint. But we're doing this on a personal laptop, not a production server, so I prioritize convenience over security.
  2. I'm not going to walk you through how to download and install WordPress; there's plenty of tutorials on that already.
  3. SSL is left as an exercise for you, dear reader. It might also be another blog, not sure yet.
  4. I use 127.0.0.1 rather than localhost because it's guaranteed to be there. localhost might have IPv6, it might not, some programs might think it does when it doesn't. 127.0.0.1 will always exist and be usable, so I use it.
  5. This tutorial is written and tested on Big Sur; I'm reasonably confident that it will work on earlier versions as well.
  6. M1 Macs might behave or be configured differently. I don't have a way to test that, although if you want to donate one to me... ;)

Throughout this tutorial, I'll be using my username (emerson) and path to my sites (/Users/emerson/repos/sites/). You should change those to be your own.

Installing programs

I use homebrew to install everything: brew install nginx mariadb php@7.4

Note: If you're visiting from the future and there's a newer PHP version that works with WordPress, use that instead.

Once those are installed, let's set up PHP first.

Setting up PHP

There are two changes we need to make. First, we need to change the user/group that PHP runs processes as. Homebrew adds the default configuration in /usr/local/etc/php/7.4/php-fpm.d/www.conf. Open that, and you'll see a couple of lines like this:

user = _www
group = _www

Change user to your user and group to the admin group, so it looks like:

user = emerson
group = admin

This is one of those times where I put convenience over security. I know that I shouldn't have PHP or nginx running as me, but in practice, having PHP and nginx running as me means that I don't have to mess around with permissions and groups. If I can read and write to a folder, so can they. If you would prefer not to do this, you should be able to leave the user/group as _www and add your user to the _www group, changing folder permissions accordingly.

Next, we need to make sure that PHP-FPM is listening on the correct host/port. PHP-FPM can listen to either a TCP socket or Unix socket. Unix sockets are more common and secure, but they are also harder to do correctly. I've taken the easy route and have FPM listening on 127.0.0.1 port 9000:

listen = 127.0.0.1:9000

It's a good idea to change some of the php.ini configuration files. If you know your host's setup, use those values. I usually set upload_max_filesize, post_max_size, and memory_limit to 2G so I don't have to worry about them. A quick search for "wordpress php-fpm settings" should point you in the right direction (although most of them will show the configuration location as /etc/php/, remember that we're using /usr/local/etc/php/).

Once you're all done, start PHP by running brew services start php@7.4

Setting up nginx

Homebrew puts the configuration files in /usr/local/etc/nginx/, so cd there and start by editing nginx.conf. The only necessary change is the user line. Uncomment this and change it like so:

user: emerson admin

See note about users and groups in the PHP section if you would prefer not to run nginx as your user. It should be possible to run nginx as the _www user and group, although I haven't tried it.

Optionally, you can also change some other configurations. I like to change nginx's default log format, so it's easier to read and gives timing output to check if scripts are slow. If you want this, here it is:

log_format upstream '$remote_addr [$time_local] "$request" $server_protocol $status "$http_referer" "$http_user_agent" '
    'rt=$request_time uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time $body_bytes_sent $ssl_cipher';

log_format static '$remote_addr [$time_local] "$request" $server_protocol $status "$http_referer" "$http_user_agent" $body_bytes_sent $ssl_cipher';

Homebrew's default setup uses a servers/ folder to keep the site configuration files. Make a defaultsite.conf file with your nginx server block in it. I'm not going to go into detail about what your nginx settings should be; there's plenty of tutorials by people smarter than me. Here's what I use:

server {
  listen 80;
  server_name defaultsite.local;
  root /Users/emerson/repos/sites/defaultsite;

  access_log /var/log/nginx/defaultsite-access.log upstream;
  error_log /var/log/nginx/defaultsite-error.log notice;
  client_max_body_size 1G;
  index index.php index.html;
  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ \.php$ {
    access_log /var/log/nginx/defaultsite-access.log upstream;
    error_log /var/log/nginx/defaultsite-error.log notice;
    fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
    include fastcgi_params;
    fastcgi_intercept_errors on;
    fastcgi_pass 127.0.0.1:9000;
  }
}

This server block isn't going to be a site itself, but we'll use it as a template for new sites. Run sudo nginx -t to make sure your config is valid, and then sudo brew services start nginx. Note that the sudo is needed to listen on port 80, which also means you'll have to start it manually every time you start your computer, sadly.

Setting up MariaDB

Standard MariaDB setup, I go for convenience and make a wordpress user with the password wordpress to create new databases.

Start it with brew services start mariadb.

You should be able to log in using sudo mysql -uroot. Then make the wordpress user:

grant all privileges on *.* to 'wordpress'@'127.0.0.1' identified by 'wordpress';
flush privileges;

Setting up a new site

Now that we have all three programs running, let's create a new site. We'll call it testsite1.

If you don't have wp-cli installed, I highly recommend installing it. It will make your dev process so much easier.

  1. Make a testsite1 directory in your site folder (for reference, my site folder is /Users/emerson/repos/sites as seen in the nginx config I showed above) and put the WordPress files in it
  2. Go into /usr/local/etc/nginx/servers/ and use sed to create a new site from the template: sed 's/defaultsite/testsite1/g' defaultsite.conf > testsite1.conf
  3. Restart nginx: sudo brew services restart nginx
  4. Add testsite1.local to your hosts file: echo "127.0.0.1 testsite1.local" | sudo tee -a /etc/hosts

That's it. If you go to http://testsite1.local, you should see the setup screen. You can now install WP however you normally do.

Conclusion

And we're done! You now have WordPress served using nginx, php-fpm, and MariaDB. If you're wondering what's so great about this, there are several reasons. wp-cli works natively, rather than messing around with adding MAMP-specific paths to your $PATH or using the Dockerized version of it that never works correctly. Creating a new site can be easily scripted. You don't have to add a port number to your local URLs, and you can use the .local TLD to make URLs distinct. If you're switching between multiple sites, you don't have to stop and start Docker containers every time; all the sites are always accessible. You have full control over the PHP and nginx configuration to make necessary changes. There are probably others that I don't remember at the moment.

In fairness, some say it's not as user-friendly for beginners, and I understand and agree with that. In fact, when training new people at my job, I have them use LocalWP because they've got enough on their plate learning WordPress and our custom theme; I'm not going to add to the stress by making them learn nginx.

 

Thanks for reading this! If you liked it, please share it with places that will also like it. If you are so inclined, you can buy me a Ko-Fi (other ways to donate are available as well). If you have any questions or comments, you can contact me in various ways, and I'll do my best to help you out. Follow me on Twitter to be notified of future posts and hear my thoughts.