For the past couple of months I’ve been working on Object Partners’ new WordPress site and I’ve learned a couple of things about doing WordPress development that I thought would be helpful to share. In this post I’m going to focus on how I got my local environment setup.
This was actually the hardest part for me. I had to learn some basics about WordPress and Docker, and refresh myself a bit on reverse proxying for local development. It took me a bit, but now I know I could do it in minutes.
I knew from the start that I wanted to leverage Docker for my local development so that I wouldn’t have LAMP stack remnants sitting on my computer when I was done working on the site. There are a lot of different ways to go about doing this but I chose to create a docker-compose.yml
where I could manage all of my containers collectively.
TLDR;
For those who don’t want an explanation here’s the entire docker-compose.yml
:
version: "3.8"
services:
db:
image: mariadb
restart: always
container_name: db
env_file: .env
volumes:
- db:/var/lib/mysq
- ./database/initdb.d:/docker-entrypoint-initdb.d
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "1"
MYSQL_DATABASE: $MYSQL_USER
MYSQL_USER: $MYSQL_USER
MYSQL_PASSWORD: $MYSQL_PASSWORD
adminer:
image: adminer
container_name: adminer
restart: always
depends_on:
- db
links:
- db
ports:
- 8080:8080
wordpress:
container_name: wordpress
image: wordpress:php7.2
restart: always
env_file: .env
depends_on:
- db
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: $MYSQL_USER
WORDPRESS_DB_NAME: $MYSQL_USER
WORDPRESS_DB_PASSWORD: $MYSQL_PASSWORD
volumes:
- ./wp-content:/var/www/html/wp-content
- wordpress:/var/www/html
wp-cli:
image: wordpress:cli-php7.2
container_name: wp-cli
env_file: .env
restart: on-failure
depends_on:
- db
- wordpress
volumes:
- wordpress:/var/www/html
command: wp search-replace 'https://{your domain goes here}' 'https://localhost.{your domain goes here}'
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: $MYSQL_USER
WORDPRESS_DB_NAME: $MYSQL_USER
WORDPRESS_DB_PASSWORD: $MYSQL_PASSWORD
https-portal:
container_name: https-portal
image: steveltn/https-portal:1
links:
- wordpress
ports:
- "80:80"
- "443:443"
environment:
STAGE: local
DOMAINS: "localhost.{your domain goes here} -> http://wordpress:80"
volumes:
db:
wordpress:
For those who don’t want to decipher what I’m doing by looking at the code above what follows is a breakdown service-by-service.
First things first
I feel it’s important to stress that this is only to get a local version of a site running. The assumption is that you already have a functioning WordPress site, like I did with Object Partners’.
Before getting started you’ll want to get yourself a dump of the database. There are lots of great resources out there on how to do that so I won’t cover it here.
Once you have your database dump (a file that ends in .sql
or .sql.gz
) you’ll want to place it inside your project. I created a database/initdb.d/
folder and moved my dump into there.
I also want to point out that I’m using a .env
file to store my secrets. When using a .env
with a Docker service you’ll need to assign the path to that file to the env_file
property for whichever service needs access to the secrets the .env
contains. You can then reference the variables by using a $
prefix.
Finally, if you’re using some sort of version control, you’ll want to ignore your dump and your .env
. Best not to leave any copies of a database or secrets lingering on the web for folks to wander across. Database dumps can also add a lot of bloat to your version control. If you’re part of a team there are other ways to share the dump and secrets can often be shared via a password manager or other secure means.
Okay, let’s dive in.
Setting up the database
db:
image: mariadb
restart: always
container_name: db
env_file: .env
volumes:
- db:/var/lib/mysq
- ./database/initdb.d:/docker-entrypoint-initdb.d
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "1"
MYSQL_DATABASE: $MYSQL_USER
MYSQL_USER: $MYSQL_USER
MYSQL_PASSWORD: $MYSQL_PASSWORD
A few things worth pointing out here. First I’m using MariaDB as opposed to MySQL. MariaDB tends to be a little more performant than MySQL so I tend to opt for it instead. Use whichever you prefer, the setup will be the same.
Second, notice the volume that is linking my database/initdb.d
directory to the initdb.d
in the container? When the container starts up it’ll use the dump that is in that directory to create the database that’ll be used for the WordPress site. The other volume is a virtual one that’s used for keeping the database around between starts/stops of our services so we don’t have to import each time.
Third, there are a lot of environment
variables here. If you’re confused as to what these are, have a look at the Docker Hub docs for MySQL. When specifying a MYSQL_DATABASE
with MYSQL_USER
and MYSQL_PASSWORD
the user that is created will have superuser privileges and a database will be created with the specified name. The MYSQL_RANDOM_ROOT_PASSWORD
will print out to the service’s console a randomly generated password that can be used for root access. It’s not really needed in this case but it’s nice to have for emergencies.
It’s important here that the name of the database that was dumped from the production site matches with the MYSQL_DATABASE
environment variable. If you’re not sure what it’s called you can often find it in the wp-config.php
file located in the root of the WordPress project directory.
Setting up Adminer
Adminer is an alternative to the oft-used phpMyAdmin. Adminer seemed to have a simpler interface and all I really needed it for was as a GUI for managing my database. If you prefer to use phpMyAdmin you’ll need to modify this section.
Adminer won’t really be needed when setting up the project so this part can be omitted, if desired. I find it helpful to have around when the need arises so I tend to keep it.
adminer:
image: adminer
container_name: adminer
restart: always
depends_on:
- db
links:
- db
ports:
- 8080:8080
The adminer
service has the db
as a dependency and they’re linked together. This means that Docker won’t start adminer
until after the database is started and it’ll link the two together so Adminer can use the created database. Port 8080
on this container is being linked to the same port on my computer. This way I can navigate to localhost:8080 to access the Adminer interface.
Setting up WordPress
I suppose we should get around to actually creating our WordPress service, huh?
wordpress:
container_name: wordpress
image: wordpress:php7.2
restart: always
env_file: .env
depends_on:
- db
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: $MYSQL_USER
WORDPRESS_DB_NAME: $MYSQL_USER
WORDPRESS_DB_PASSWORD: $MYSQL_PASSWORD
volumes:
- ./wp-content:/var/www/html/wp-content
- wordpress:/var/www/html
Like Adminer, this service depends on the db
service.
It’s important to note that I’m using PHP 7.2 for this service; matching the version the production site is using. Not doing so can lead to bugs, locally. Remember that the point of this is to get a local copy of the production site running; best to match them up in this case. This can be modified to match whichever PHP version your project is using.
The WORDPRESS_DB_HOST
environment variable tells WordPress where to look for the database it’ll be connecting to. In this case it’ll be looking at the db
service directly. The other three variables are aliases for the ones set on the db
service.
The first volume listed here is probably the most important. It sets the the service to mirror whatever is placed in the wp-content
of our WordPress project to the same directory within the service. This means that any update to the theme or plugins in the project will be directly reflected on the local site.
The second volume is virtual, similar to the database. Its importance will become clear in the next service.
Setting up WP-CLI
When working on a preexisting site, like I was, you’ll need to replace all of the domain names with whatever you’ve opted to use as your local. For me, I opted to go with https://localhost.objectpartners.com
. This means I needed to change every reference of https://objectpartners.com
to the one prefixed by localhost
. After talking to some of my colleagues I found out that the WordPress CLI has a search-replace
function that can be used to do this.
wp-cli:
image: wordpress:cli-php7.2
container_name: wp-cli
env_file: .env
restart: on-failure
depends_on:
- db
- wordpress
volumes:
- wordpress:/var/www/html
command: wp search-replace 'https://{your domain goes here}' 'https://localhost.{your domain goes here}'
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: $MYSQL_USER
WORDPRESS_DB_NAME: $MYSQL_USER
WORDPRESS_DB_PASSWORD: $MYSQL_PASSWORD
This service is a bit unique. I only had a single purpose for the WordPress CLI: to update the domain name. Once that was done I didn’t want it to keep around using my computer’s resources so I set its restart
property to on-failure
. This means that this service will continue to restart until it succeeds. For this service success means running the command
property which is set to the WordPress CLI command for searching and replacing; in this case the domain name is being replaced with the local one.
Here’s where that wordpress
virtual volume comes in handy. WordPress CLI only works if it detects a running WordPress site so it needs access to the WordPress instance running in the wordpress
service. With the two sharing their /var/www/html/
directories they’re virtually running in parity with one another.
The WordPress CLI image is, in essence, a WordPress image so it too needs access to the database, which is why the environment
variables and PHP version are identical to the wordpress
service.
Setting up the proxy
You’ll want to create some reverse proxy so that references to https://localhost.{your domain}/{some asset}
resolve. If you don’t, your site might not even work from the onset since your browser won’t understand that the URL you entered is a reference to the wordpress
services’ port:80.
I used the https-portal image for setting up my proxy. If you have the know-how and want to do this on your own, you’re welcome to it. For me, it was unnecessary overhead.
https-portal:
container_name: https-portal
image: steveltn/https-portal:1
depends_on: wordpress
links:
- wordpress
ports:
- '80:80'
- '443:443'
environment:
STAGE: local
DOMAINS: 'localhost.{your domain goes here} -> http://wordpress:80'
This service is waiting for the wordpress
service to startup. They’re also being linked together.
Here the ports 80
and 443
on the service are being tied to the same ports on my computer. The STAGE: local
environment variable is used by https-portal
for determining what environment is being worked in since some will need to be setup differently.
Finally, the DOMAINS
environment variable assignment is for telling https-portal
that port 80
on the wordpress
service should be aliased to localhost.{your domain goes here}
so that navigating to that subdomain will resolve to the local WordPress site.
Handling the virtual volumes
As I’ve already mentioned, there are a couple of virtual volumes being used: db
and wordpress
. So they’ll need to be added to the docker-compose.yml
, too.
volumes:
db:
wordpress:
Almost there!
Now that the file is complete all that is needed is to navigate to the root of the WordPress project directory, where the docker-compose.yml
is located, and run docker-compose up -d
. This runs docker-compose
in detached mode.
If at this point you immediately attempted to navigate to https://localhost.{your domain goes here}
things will probably look a little broken. This is because it takes a bit for the database to get all setup and import the dump from the production site. It also takes a bit for https-proxy
to complete its setup script on the first run. Once the database is setup WordPress CLI will update all of the domains to reference your local copy and you should be off to the races. Enjoy your locally running WordPress site!