One of my obsessions lately is keeping my operating system as clean as possible. In order to do that, I perfer to install as few as possible anything on it.
Node.js makes that quest so easy as I only need
node installed globally and everything else can be installed locally inside project directory. However, for many other languages, such as
PHP, it’s often not the case. Furthermore, when each project requires different environemnt setup, it quickly becomes a painful job managing everything and sometime you break the whole system.
Docker to the rescue.
While mostly used for controlling production and teamwork environments, Docker is also very useful for encapsulating development environment and providing a “clean slate” to make sure nothing breaks unintentionally.
When building a docker development system, I often consider the following requirements:
- Minimum volume mounting: File IO operations on mounted host volumes are slow (on Windows and Mac) so you would only want to mount the very sourcecode files you are going to edit. Others should be copied when builing the image.
- No runtime installation: Dependencies and 3rd-party plugins should not be installed at container run time, especially if they require internet connection, to ensure fastest booting time when you resume the development. They should be installed when building the image.
Minimum 3rd-party on host: If possible, dependencies and 3rd-party plugins should be fetched from the internet (via a configuration file like
Gemfileor even listed inside
Dockerfile). Otherwise they should be kept as zip files and copied/extracted when building the image. There are two reasons for this:
- Copying a big amount of files from host to image is very slow.
- Keeping a big amount of 3rd-party files in your git directory is messy.
- Trackable snapshot: You should be able to take snapshots of your current development progress (especially in Wordpress case as explained below), and the they should be able to be tracked in git repository.
For Wordpress development, I use
chriszarate/wordpress since it provides a bunch of useful features via envinronment varaibles including:
- Automatically activate plugins when container starts. Because no one wants to activate theme manually every single time. However, this becomes irrelevant once you start using database snapshots.
- Automatically activate a theme when container starts. Similarly, this becomes irrelevant once you start using database snapshots.
- Append additional PHP to
- Preconfigure admin user, admin password, admin email and site url to skip the boring welcome setup screen.
The magic behind this docker image is a set of
wp-cli scripts appended into the default entrypoint
docker-entrypoint.sh of the official
wordpress docker image.
Here is the folder structure of my Wordpress development directory, there are 3 main sections (which are poorly named in the screenshot but explained nicely in the picture below):
Sourcecodes: The plugins and themes you are currently developing. They are text files (
.xml, etc.) that you are going to create and change during most of your project timeline.
3rd-parties: They are the 3rd-party plugins that your site needs, the dependencies of your project or the parent theme for which you are creating child theme. They should be kept as zip files in these folders or specified as urls in
Snapshots: Including the sql dump from wordpress database and the compressed file of
wp-content/uploadsfolder (in case you are building contents or starting with demo content of a theme).
With this structure, 3rd-parties and Snapshots will be built into the image, while sourcecodes are mounted to allow changes in development.
docker-entrypoint.sh is the default entrypoint
docker-entrypoint.sh of the original docker image and will be modified (see below) to fit our needs.
For a sample
Dockerfile, checkout this gist.
For Dockerfile configuration, I install the following packages:
unzip: As noted above, snapshots and 3rd-parties are saved as zip files (and in the case of remotely fetched, also in zip format). We would need this utility to decompress them.
mysql-client: This package provides neccessary commands required by
wp dbto export and import wordpress database as
wget: In the case where dependencires are remotely fetched, we use
wgetto download them from specified urls.
FROM chriszarate/wordpress:4.9.1 RUN \ apt-get update && \ apt-get install unzip wget mysql-client -y && \ rm -rf /var/lib/apt/lists/*
We copy each 3rd-party directory of zip files to a temporary folder on the image (or download via
wget), extract and delete all zip files. The content of these folders will be copied to real target directories at runtime by
The reason we are not copying them directly to the real target folders is that they would be overwritten by commands in official wordpress image’s
# If copied from folder COPY ./plugins/ /temp/plugins # If downloaded via url wget -P /temp/plugins/ https://downloads.wordpress.org/plugin/jetpack.5.9.zip # Extract and delete zip files RUN unzip '/temp/plugins/*.zip' -d /temp/plugins && rm /temp/plugins/*.zip || true;
|| trueis added to make sure that when there are no zip file the script would not fail and terminate image build process.
Similarly we copy snapshot files (if available) to the image. The
uploads.zip file should be decompressed like above, while
wordpress.sql would later be processed by
We need to replace the default
docker-entrypoint.sh with our modified script. We don’t have to specify
ENTRYPOINT since it is already declared in the official docker image.
COPY docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
It is also useful to have your own php uploads configuration as
uploads.ini in your repository and insert into the image. Obviously you can config other internal system files using the same method: copy a custom file to the absolute path inside the image.
COPY ./uploads.ini /usr/local/etc/php/conf.d/uploads.ini
For a sample
docker-entrypoint.sh, checkout this gist.
It’s time to modify
docker-entrypoint.sh. Starting with a clone of the script from
chriszarate/wordpress, we would need to copy all files in
/temp/ directory to where they need to be. These lines need to be placed before plugins and themes activation.
CONTENT_DIR=$ROOT_DIR/wp-content THEME_DIR=$CONTENT_DIR/themes PLUGIN_DIR=$CONTENT_DIR/plugins cp -r /temp/themes/* $THEME_DIR || true cp -r /temp/plugins/* $PLUGIN_DIR || true cp -r /temp/base/* $CONTENT_DIR || true # Activate plugins. Install if it cannot be found locally. # ...
One thing to notice here is that, all these files and folders are owned by
root user, hence will not be writable by Wordpress. If you need to give Wordpress permission over these directories, you need to insert something like this:
chown -R $WEB_USER:$WEB_USER $ROOT_DIR/wp-content/uploads
Finally, when everything is done, at the end of
docker-entrypoint.sh, we would want to import
wordpress.sql file. We place this line at the end of the file, just before
exec "$@", to ensure our database would not be overwritten by anything.
runuser $WEB_USER -s /bin/sh -c "wp db import $CONTENT_DIR/wordpress.sql" exec "$@"
docker-compose configuration, you can find an example at chriszarate’s repository. It’s a combination of 2 services:
wordpress for PHP wordpress installation and
mysql for database. There are only a few things to change here:
Build your Dockerfile: instead of using
services: wordpress: build: .
Mount your sourcecodes according to your project, one by one. Do not mount the entire
pluginsdirectory since their versions inside the container are already populated with other files.
volumes: - "./src/themes/my-custom-theme:/var/www/html/wp-content/themes/my-custom-theme" - "./src/plugins/my-custom-plugin:/var/www/html/wp-content/plugins/my-custom-plugin"
Local only: Only list plugins and themes that are installed inside docker image on
WORDPRESS_ACTIVATE_THEMEenvironment variables. Otherwise, the entrypoint script will attempt to download them from the internet and prolong the startup time.
Now we are set. Let’s go and clean off any XAMPP or MAMP stack, or just delete everything and reinstall your operating system, or even better throw your machine away for a brand new computer. Have fun.