Synchronise WordPress Uploads Across Environments

Synchronise WordPress Uploads Across Environments

Read it at


I was recently introduced to rsync to help copy only the missing images between environments. Wikipedia describes it well:

rsync is a utility software and network protocol for Unix-like systems (with a port to Microsoft Windows) that synchronizes files and directories from one location to another while minimizing data transfer by using delta encoding when appropriate.

rysnc needs to be run in the command line and connecting to your remote site needs to be with SSH.

First of all navigate to your local WordPress site:
cd wordpress-site

Move to your uploads folder:
cd wp-content/uploads

To perform the synchronisation (replacing the SSH credentials):
rsync -avz --rsh=ssh user@host:/path/to/site/wp-content/uploads/* .

rsync is a lot quicker as it uses an SSH connection and is a very powerful tool. Read more about the different options and configuration here.


The last two solutions are my favourites as they don’t require any form of copying of files across environments.

This involves adding an .htaccess file to the wp-content/uploads/ directory of your site with this code:

<IfModule mod_rewrite.c>
  RewriteEngine On

  RewriteBase /wp-content/uploads/
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^(.*)$1 [L,P]


So for any file requested within wp-content/uploads/, that does not exist, it will serve the image from

You may need to edit the RewriteBase to suit your site’s base path in your various environments.

WordPress Filters

This  is the equivalent of the .htaccess code but using core filters to serve the images. You need to add some extra config to your site’s wp-config.php:

Define your site’s url used by WP:

define('WP_SITEURL', 'http://' . $_SERVER['HTTP_HOST'] .
    str_replace(DIRECTORY_SEPARATOR, '/', str_replace(realpath($_SERVER['DOCUMENT_ROOT']), '', dirname(__FILE__))));

Define the url of your production site where most up to date media will be uploaded:

define('LIVE_SITEURL', '');

Then in your theme’s functions.php, a plugin or better still a site-wide functions plugin, add:

add_action('init', 'my_replace_image_urls' );
function my_replace_image_urls() {
    if ( defined('WP_SITEURL') && defined('LIVE_SITEURL') ) {
        if ( WP_SITEURL != LIVE_SITEURL ){
            add_filter('wp_get_attachment_url', 'my_wp_get_attachment_url', 10, 2 );

function my_wp_get_attachment_url( $url, $post_id) {
    if ( $file = get_post_meta( $post_id, '_wp_attached_file', true) ) {
        if ( ($uploads = wp_upload_dir()) && false === $uploads['error'] ) {
            if ( file_exists( $uploads['basedir'] .'/'. $file ) ) {
                return $url;
    return str_replace( WP_SITEURL, LIVE_SITEURL, $url );

This will work for uploaded media that is being served by WordPress’ standard image functions such as the_post_thumbnail() or wp_get_attachment_image().

The function my_wp_get_attachment_url() does the work, but will only be added if it is not the live site, as it will already have all the images.

The function then hooks into the WordPress filter wp_get_attachment_url, so for each time WordPress is serving an attachment url our function jumps in and checks if that attachment file exists in our site, and if not swaps out the site url with the live url.