Speed Up Your WordPress Development With Docker


WordPress
Docker
DevX

Working with WordPress can often be time-consuming, particularly when managing multiple environments like local, staging, and production. In our team at DEside, we used to follow a traditional workflow:

  1. Install WordPress locally using XAMPP, MAMP, or another local server.
  1. Develop and test the site locally.
  2. Build themes, plugins, and other assets locally.
  3. Create a backup (we used to do this manually by exporting the WordPress folder and the SQL database, but recently we switched to Akeeba Backup)
  4. Deploy the site to a staging server
  5. Test on the staging server, often with manual user tests by team members.
  6. Share the staging site with the client for feedback.
LocalStagingProduction

The Problem with Traditional Workflows

Typically, after the client reviews the staging site, they request changes. At this point, you face two options:

  • Make the changes locally and go through the entire deployment process again. This is time-consuming and can become frustrating.
  • Make the changes directly on the staging server, which requires rebuilding and uploading files for every change — an equally time-consuming process.

Both approaches lead to inefficiencies. For example, in our projects, even with optimizations like switching to Vite, building could take about 5 seconds, and uploading via FTP another 30 seconds. This adds up quickly with frequent changes. Moreover, not keeping local and staging environments in sync leads to further complications, ultimately degrading Developer Experience (DevX).

Rethinking the Workflow

Why do we typically work locally before deploying to staging? The main reason is to minimize idle time between uploads and builds. Working offline is generally not a concern since most of us are usually connected to a network anyway.

So, why not connect the local and the staging environments to the same database? Since both are used solely for development, sharing a database could save considerable time by keeping the environments in sync. While this sacrifices the ability to work offline, the trade-off may be worth it considering the time saved.

A Basic Solution: Modifying wp-config.php

One simple solution is to connect your local WordPress installation to the remote database by editing the wp-config.php file. You can modify parameters such as DB_HOST, DB_NAME, DB_USER, and DB_PASSWORD, and potentially adjust the $table_prefix. Additionally, you should redefine the WP_HOME and WP_SITEURL constants in wp-config.php. After making these changes, restart your server to apply them.

While this works, it has limitations, as you need to configure each new local environment manually.

A Better Solution: Automating with Docker

A more efficient solution is to automate the entire process using Docker. I’ve created a simple configuration available on my GitHub repo that automates these tasks.

The repository contains:

  • docker-compose.yml: Defines the Docker containers
  • change-url.sh: A custom script to modify wp-config.php for setting WP_HOME, WP_SITEURL, and addressing the media uploads issue.
  • .env.sample: Defines necessary environment variables.

Benefits of This Approach

  1. Automation: The Docker setup automatically handles modifications to the wp-config.php file, reducing manual effort.
  2. Easier Environment Management: Instead of setting up a new local environment with a fresh WordPress installation every time, Docker allows you to streamline this process significantly. By simply adding a new service in the docker-compose.yml file and creating a corresponding .env file, you can quickly spin up new environments without the hassle of recreating servers from scratch. This not only saves time but also ensures consistency across different environments.

Code Implementation

Let’s dive into the implementation details.

The docker-compose.yml file is structured as follows:

docker-compose.yml
version: "3.6"
services:
your-site:
image: wordpress:latest
container_name: your-site
volumes:
- ./wordpress:/var/www/html
- ./change-url.sh:/usr/local/bin/change-url.sh
- ./.env:/usr/local/bin/.env
environment:
- WORDPRESS_DB_NAME
- WORDPRESS_TABLE_PREFIX
- WORDPRESS_DB_HOST
- WORDPRESS_DB_USER
- WORDPRESS_DB_PASSWORD
- WORDPRESS_PORT
- WORDPRESS_DEBUG=1
restart: always
ports:
- "${WORDPRESS_PORT}:80"
entrypoint: ["/bin/bash", "-c", "/usr/local/bin/change-url.sh"]

This configuration pulls the latest WordPress image, sets up the necessary environment variables, and runs the change-url.sh script, which modifies the wp-config.php file as needed.

The .env.sample file should be configured with the environment variables necessary for the Docker image.

The change-url.sh script is critical for this setup:

change-url.sh
#!/bin/bash
set -e
# Load environment variables from .env file if not already set
export $(grep -v '^#' /usr/local/bin/.env | xargs)
# Run the original entrypoint
docker-entrypoint.sh apache2-foreground &
# Wait for WordPress to be fully set up
while ! [ -f /var/www/html/wp-config.php ]; do
sleep 1
done
# Check if the custom lines are already present
if ! grep -q "define('WP_HOME', 'http://localhost:${WORDPRESS_PORT}');" /var/www/html/wp-config.php; then
# Insert custom lines before the specific line
sed -i "/\/\* That's all, stop editing! Happy publishing. \*\//i \\
define('WP_HOME', 'http://localhost:${WORDPRESS_PORT}');\\n\
define('WP_SITEURL', 'http://localhost:${WORDPRESS_PORT}');\\n\
define('REMOTE_MEDIA_URL', 'https://${WORDPRESS_URL}');\\n\
\\n\
ob_start('ob_replace_home_url');\\n\
function ob_replace_home_url(\$content)\\n\
{\\n\
\$home_urls = array(\\n\
'https://${WORDPRESS_URL}',\\n\
'http://${WORDPRESS_URL}',\\n\
);\\n\
\\n\
\\n\
\$remote_media_url = REMOTE_MEDIA_URL;\\n\
\\n\
foreach (\$home_urls as \$home_url) {\\n\
\$content = preg_replace_callback(\\n\
\"~\$home_url/(?!wp-content/uploads)~\",\\n\
function (\$matches) use (\$remote_media_url) {\\n\
return str_replace(\$matches[0], WP_HOME, \$matches[0]);\\n\
},\\n\
\$content\\n\
);\\n\
}\\n\
\\n\
return \$content;\\n\
}\\n\
" /var/www/html/wp-config.php
fi
# Keep the container running
wait

This script ensures that the WordPress installation on your local machine correctly reflects the configurations set for the staging environment, including handling media files.

//Define WP_HOME and WP_SITEURL
// These constants define the home URL and site URL of your WordPress installation.
define('WP_HOME', 'http://localhost:XXXX');
define('WP_SITEURL', 'http://localhost:XXXX');
//Define REMOTE_MEDIA_URL
// This constant defines the URL of the remote media location.
define('REMOTE_MEDIA_URL', 'https://your-staging-url.com');
// Start output buffering with a callback to ob_replace_home_url function
ob_start('ob_replace_home_url');
function ob_replace_home_url($content)
{
// Define an array of staging URLs that need to be replaced in the content in order to make the local environment work
$home_urls = array(
'https://your-staging-url.com',
'http://your-staging-url.com',
);
// The following code allows replacing staging URLs with the local WP_HOME URL
// except for media files located in wp-content/uploads directory.
$remote_media_url = REMOTE_MEDIA_URL;
foreach ($home_urls as $home_url) {
$content = preg_replace_callback(
"~$home_url/(?!wp-content/uploads)~",
function ($matches) use ($remote_media_url) {
// Replace the matched staging URL with the local WP_HOME URL
return str_replace($matches[0], WP_HOME, $matches[0]);
},
$content
);
}
return $content;
}

Potential Drawbacks

One limitation of this setup is the synchronization of plugins and themes between local and staging environments. Since WordPress stores active plugin and theme data in the database, discrepancies between environments can lead to plugins being deactivated when accessing the WordPress admin panel.

For themes, this is less of an issue because multiple themes can coexist in the local environment, even if they are not all present on the staging server. The correct theme will be activated each time you run the Docker container, avoiding any major disruptions.

However, for plugins, this can be more problematic. For instance, if you’re using Advanced Custom Fields (ACF) on the staging server, those fields won’t render locally unless ACF is also installed there. To mitigate this, always ensure that the necessary plugins are installed and activated in both environments to maintain consistency.

Conclusion

In this way, Docker can help us speed up WordPress development. At DEside, this approach has drastically improved our development speed, and we continue to explore additional optimizations.

If you have any feedback or suggestions, feel free to share them. You can contribute directly by opening an issue or a pull request on the GitHub repository.

Bonus: a small plugin to avoid media problems

To avoid confusion and ensure media files are always uploaded to the staging server, we developed a small plugin that blocks media uploads on the local server. This plugin serves as a reminder to only upload media on the staging server, ensuring consistency across environments. You can find the plugin on its GitHub repository.

Other articles