Serving local WordPress sites on macOS with nginx and php-fpm
Table Of Contents
I show how to set up a WordPress server on macOS using nginx, MariaDB, and php-fpm without using Docker or any other third-party program
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
- 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.
- I'm not going to walk you through how to download and install WordPress; there's plenty of tutorials on that already.
- SSL is left as an exercise for you, dear reader. It might also be another blog, not sure yet.
- I use
127.0.0.1
rather thanlocalhost
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. - This tutorial is written and tested on Big Sur; I'm reasonably confident that it will work on earlier versions as well.
- 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.
- 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 - 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
- Restart nginx:
sudo brew services restart nginx
- 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.