Bash script to mirror directory and database of website between servers
When you have a site that’s an on-going project, and you’re using a development, staging, and production server configuration, time and time again you’ll have a need to mirror the production site content with your other environments.
Previously, this either meant to manually (FTP) or semi-manually (rsync) update the files, export the database, and in the case of WordPress MU, update certain records in the database. While the entire act isn’t hard, it has a zero-fun factor.
I put some time aside to write a small bash script that could automate this for multiple sites with just two commands. The script could definitely be improved, but it’s functional and gets the job done nicely, but I’m always open to suggestions and improvements.
I’ll go through the entire script, and if you find it useful, you can adapt it to meet your needs. Any variables or data you may need to change, will be highlighted in red below the code box.
You can find the entire code in a Gist. Note that there are two versions. One for server to server, the other for server to localhost. We’ll discuss the server edition below.
Table of Contents
- Program Skeleton
- Server Environment
- Production/Local Server Variables
- Program Arguments
- Display Functions
- MySQL Functions
- Main Functions
- Download Files
- Export Database
- Upload Files
- Import Database
- Run Functions
- Run Synchronize
- Run Update
- Run
- Run Program
1. Program Skeleton
We start with the bash shebang, followed by some program meta data, which will print out on the screen when the program runs. You’ll find a sample output of this at the end of post.
#!/bin/bash
### Program meta data
# Program name
NAME='site'
# Program version
VERSION='1.0.0'
# Today's date
DATE=$(date +'%x %r %Z')
# Program header title
TITLE="Site v$VERSION on $HOSTNAME"
# Program header information
STATUS="$DATE by $USER"
# Program usage help
USAGE="Usage: $NAME [sync|update] [sitename]"
Things to edit:
- Line 21: sitename
2. Server Environment
Next, we determine on which server the program is being executed on. This allows us to use the very same program on all servers, meaning we only have to maintain one instance of it.
### Server environment
ENV=''
case $HOSTNAME in
DEV01.DOMAIN.COM)
ENV='DEV'
;;
STG01.DOMAIN.COM)
ENV='STG'
;;
esac
Things to edit:
- Line 5: DEV01.DOMAIN.COM
- Line 8: STG01.DOMAIN.COM
If you type in hostname
in Terminal on each of your servers, you’d replace DEV01.DOMAIN.COM
and STG01.DOMAIN.COM
with those values. You can add as many server environments as you have. What this will do is set a variable called ENV
that we can later check to determine which set of credentials and domain to use.
3. Production/Local Server Variables
The program needs to know where the production web and database server is, and with which username the program should attempt to connect to each. You’ll be prompted for those passwords when the time comes.
The program also needs to know where your sites live on the production server, where they live on your local server, and where you want to temporarily store the files that will be downloaded from the production server.
### Production database server
# Hostname or IP address
MYSQL_PRD_HOSTNAME='hostname'
# Username
MYSQL_PRD_USERNAME='username'
### Production web server
# Hostname or IP address
SSH_PRD_HOSTNAME='hostname'
# Username
SSH_PRD_USERNAME='username'
### Local server paths
# Absolute path to local sites directory
# Ex: /var/www/domains
LOCAL_SITES_PATH='/var/www/domains'
# Absolute path to production sites directory
# Ex: /var/www/domains
PRD_SITES_PATH='/var/www/domains'
# Absolute path to local temporary sites directory
# Ex: /opt/sites
TEMP_SITES_PATH='/opt/sites'
Things to edit:
- Line 4: hostname
- Line 7: username
- Line 12: hostname
- Line 15: username
- Line 21: /var/www/domains
- Line 25: /var/www/domains
- Line 29: /opt/sites
As a side note, I created a new database user that has read-only access to the production site, to ensure that nothing would be manipulated by accident.
4. Program Arguments
The program will take two arguments, which we will save as arg1
and arg2
. This allows us to reference those arguments in our functions later.
### Save program arguments
arg1=$1
arg2=$2
The first argument will tell the program what to do, and the second which site to do it to.
5. Display Functions
The following functions are purely for aesthetic reasons and help keep certain visual elements uniform across the program.
## Display functions
# Display divider
function display_divider {
echo '------------------------------------------------------------'
}
# Display new line
function display_newline {
echo
}
# Display output start
function display_output_start {
echo '> Starting...'
}
# Display output end
function display_output_end {
echo '> Done.'
}
# Display program header
function display_header {
display_newline
display_divider
echo " $TITLE"
echo " $STATUS"
echo " $ $NAME $arg1 $arg2"
display_divider
display_newline
}
# Display usage help
function display_usage {
echo $USAGE
display_newline
}
6. MySQL Functions
There is only one function here, and it will be executed when dealing with WordPress MU sites. You can see the arguments it takes below. It will essentially replace the production domain name with the domain you’re using in the environment the program is being executed on.
### MySQL functions
# Update imported database to match local environment
# $1 mysql hostname
# $2 mysql username
# $3 mysql password
# $4 mydql database
# $5 domain name
function wp_mu_db_update {
host=$1
user=$2
pass=$3
db=$4
domain=$5
query="mysql --host=$host --user=$user --password=$pass $db -e"
echo "> Setting 'domain' field in 'wp_blogs' table to '$domain'..."
$query "UPDATE wp_blogs SET domain = '$domain' WHERE blog_id = 1 AND site_id = 1";
echo "> Setting 'site_url' field in 'wp_options' table to 'http://$domain/wordpress'..."
$query "UPDATE wp_options SET option_value = 'http://$domain/wordpress' WHERE option_name = 'siteurl'";
echo "> Setting 'home' field in 'wp_options' to 'http://$domain'..."
$query "UPDATE wp_options SET option_value = 'http://$domain/' WHERE option_name = 'home'";
echo "> Setting 'domain' field in 'wp_site' to '$domain'..."
$query "UPDATE wp_site SET domain = '$domain' WHERE id = 1";
echo "> Setting 'site_url' field in 'wp_sitemeta' table to 'http://$domain/'..."
$query "UPDATE wp_sitemeta SET meta_value = 'http://$domain/' WHERE meta_key = 'siteurl' AND site_id = 1";
}
You could add more functions that perform different actions for different platforms.
7. Main Functions
Now we get into the meat and potatoes of this program. This section contains four functions:
- download_files – Download the files from production to a temporary directory on the local server
- export_database – Export the database from production to a temporary directory on the local server
- upload_files – Copy the files from the temporary directory on the local server to where they need to go on the local server
- import_database – Import the database into the local website that we want to update
As a side note, the reason we’re using a temporary directory in the first place is two-fold. One, it allows us to reset our local site easily at any point, and two, it allows you to pull those changes into another instance of that site. The beauty is that it can do all that without having to reconnect to production, i.e., less strain on production server.
7.1 Download Files
First, the program will check if the temporary directory you specified in step 3 exists. If not, it will create it.
Second, it will start synchronizing the src
location (production) with the dst
location (server you are on). It will delete anything that wasn’t present in the source, and skip any directories specified in ex
(exclude).
# rsync
# -a, --archive archive mode; same as -rlptgoD
# -r, --recursive recurse into directories
# -l, --links copy symlinks as symlinks
# -p, --perms preserve permissions
# -t, --times preserve times
# -g, --group preserve group
# -o, --owner preserve owner (super-user only)
# -D same as --devices --specials
# --devices preserve device files (super-user only)
# --specials preserve special files
# -v, --verbose increase verbosity
# --exclude=PATTERN exclude files matching PATTERN
# --delete delete files that don’t exist on sender
# --delete-excluded also delete excluded files on receiver
# Copy files from production web server to temporary directory
# $1 path to files on production server
# $2 path to files on local server
# $3 rsync exclude option(s)
function download_files {
src=$1
dst=$2
ex=$3
if [ ! -d "$dst" ]; then
echo "> Creating temporary directory..."
echo " $dst"
mkdir -p "$dst"
fi
if [ -d "$dst" ]; then
echo "> Synchronizing production files with temporary directory..."
echo " Source : $src"
echo " Destination : $dst"
rsync --archive --verbose $ex --delete --delete-excluded "$src" "$dst"
else
echo "> Temporary directory does not exist"
echo " $dst"
fi
}
7.2 Export Database
First, the program will ensure the temporary folder exists (as before), and create it if it doesn’t.
Second, it will connect to the production database and export everything, but excluding any tables specified in ignore
.
# Export database from production database server to temporary directory
# $1 database hostname
# $2 database username
# $3 database name
# $4 path to save mysql export
# $5 mysqldump ignore-table option(s)
function export_database {
host=$1
user=$2
db=$3
dst=$4
ignore=$5
if [ ! -d "$dst" ]; then
echo "> Creating temporary directory..."
echo " $dst"
mkdir -p "$dst"
fi
if [ -d "$dst" ]; then
echo "> Exporting production database to temporary directory..."
echo " Database : $db"
echo " Destination : $dst"
echo " Filename : $db.sql"
mysqldump --verbose --host=$host --user=$user --password $db $ignore > "$dst/$db.sql"
else
echo "> Temporary directory does not exist"
echo " $dst"
fi
}
7.3 Upload Files
The program will mirror the temporary directory with the equivalent directory of the local site.
# Copy files from temporary directory to local site
# $1 path to temporary files on local server
# $2 path to real files on local server
function upload_files {
src=$1
dst=$2
if [[ (-d "$src") && (-d "$dst") ]]; then
echo "> Synchronizing temporary files with local site directory..."
echo " Source : $src"
echo " Destination : $dst"
rsync --archive --delete --delete-excluded "$src" "$dst"
else
echo "> Temporary or local site directory does not exist"
echo " Source : $src"
echo " Destination : $dst"
fi
}
7.4 Import Database
The program will replace your local database with the exported database from production. This shouldn’t be an issue, but if you have some test data in there, remember it’ll all be overwritten.
# Import production database from temporary directory to local database
# $1 database hostname
# $2 database username
# $3 database password
# $4 database name
# $5 path to mysql export
function import_database {
host=$1
user=$2
pass=$3
db=$4
src=$5
if [ -f "$src/$db.sql" ]; then
echo "> Importing temporary database export into local database..."
echo " Filename : $db.sql"
echo " Source : $src"
echo " Database : $db"
mysql --host=$host --user=$user --password=$pass $db < "$src/$db.sql"
else
echo "> Temporary database export does not exist"
echo " Filename : $db.sql"
echo " Source : $src"
fi
}
8. Run Functions
Since we have two things (synchronize and update) the program can do, we need at least two functions to take care of this.
- run_sync – Get everything from production and store it in the temporary directory.
- run_update – Copy everything from the temporary directory into the local site.
This is the place where you define the specifics for each local site, like where the files need to be copied to, what the database name is, etc.
In addition, there is one other function below those two, which I call the main component, and it either calls run_sync
or run_update
, depending on the arguments the user provided.
8.1 Run Synchronize
The program will mirror the production files and database with the temporary directory.
# Run sync component
function run_sync {
case $arg2 in
sitename)
display_output_start
copy_from="$SSH_PRD_USERNAME@$SSH_PRD_HOSTNAME:$PRD_SITES_PATH/domain.com/www/htdocs/wp-content/uploads/"
copy_to="$TEMP_SITES_PATH/$arg2/uploads/"
exclude='--exclude=temp --exclude=wpcf7_uploads'
download_files "$copy_from" "$copy_to" "$exclude"
display_newline
mysql_hostname=$MYSQL_PRD_HOSTNAME
mysql_username=$MYSQL_PRD_USERNAME
mysql_database='domain_com'
export_to="$TEMP_SITES_PATH/$arg2"
ignore="--ignore-table=$mysql_database.wp_w3tc_cdn_queue"
export_database $mysql_hostname $mysql_username $mysql_database "$export_to" "$ignore"
display_output_end
;;
*)
display_usage
;;
esac
}
Things to edit:
- Line 5: sitename
- Line 8: domain.com/www/htdocs/wp-content/uploads
- Line 9: uploads
- Line 10: --exclude=temp --exclude=wpcf7_uploads
- Line 17: domain_com
- Line 19: --ignore-table=$mysql_database.wp_w3tc_cdn_queue
8.2 Run Update
The program will mirror the temporary directory with the equivalent local site files and database.
Note that you’ll need to update the sitename
, the path to where it should be copied to, and the different database credentials for your environment.
# Run update component
function run_update {
case $arg2 in
sitename)
display_output_start
copy_from="$TEMP_SITES_PATH/$arg2/uploads/"
copy_to="$LOCAL_SITES_PATH/domain.com/www/htdocs/wp-content/uploads/"
upload_files "$copy_from" "$copy_to"
mysql_username=''
mysql_password=''
domain_name=''
case $ENV in
DEV)
mysql_username='username'
mysql_password='password'
domain_name='dev01.domain.com'
;;
STG)
mysql_username='username'
mysql_password='password'
domain_name='stg01.domain.com'
;;
esac
if [[ (-n $mysql_username) && (-n $mysql_password) && (-n $domain_name) ]]; then
mysql_hostname='localhost'
mysql_database='domain_com'
import_from="$TEMP_SITES_PATH/$arg2"
import_database $mysql_hostname $mysql_username $mysql_password $mysql_database "$import_from"
wp_mu_db_update $mysql_hostname $mysql_username $mysql_password $mysql_database $domain_name
else
echo "> Unable to obtain MySQL credentials for $ENV server"
fi
display_output_end
;;
*)
display_usage
;;
esac
}
Things to edit:
- Line 5: sitename
- Line 8: uploads
- Line 9: domain.com/www/htdocs/wp-content/uploads
- Line 17: username
- Line 18: password
- Line 19: dev01.domain.com
- Line 22: username
- Line 23: password
- Line 24: stg01.domain.com
- Line 28: localhost
- Line 29: domain_com
8.3 Run
This is responsible for routing the user’s request.
# Run main component
function run {
case $arg1 in
sync)
run_sync
display_newline
;;
update)
run_update
display_newline
;;
*)
display_usage
;;
esac
}
9. Run Program
Last, but not least, this is what starts the program.
### Run Program
display_header
run
exit 0
Summary
If you save all this code in a file called site
, copy it into the /usr/bin
directory on the target server, give it 750
permissions (chmod 750 /usr/bin/site
), then execute it:
[root@DEV01 ~]# site
------------------------------------------------------------
Site v1.0.0 on DEV01.DOMAIN.COM
10/22/2013 06:05:16 PM UTC by root
$ site
------------------------------------------------------------
Usage: site [sync|update] [sitename]
And then to synchronize:
[root@DEV01 ~]# site sync sitename
------------------------------------------------------------
Site v1.0.0 on DEV01.DOMAIN.COM
10/22/2013 06:07:46 PM UTC by root
$ site sync sitename
------------------------------------------------------------
> Starting...
> Synchronizing production files with temporary directory...
Source : username@hostname:/var/www/domains/domain.com/www/htdocs/wp-content/uploads/
Destination : /opt/sites/domain/uploads/
username@hostname's password:
receiving incremental file list
[...]
sent 770 bytes received 1484485 bytes 118820.40 bytes/sec
total size is 1267533727 speedup is 853.41
> Exporting production database to temporary directory...
Database : domain_com
Destination : /opt/sites/domain
Filename : domain_com.sql
Enter password:
-- Connecting to hostname...
[...]
-- Disconnecting from hostname...
> Done.
And lastly to update:
[root@DEV01 ~]# site update sitename
------------------------------------------------------------
Site v1.0.0 on DEV01.DOMAIN.COM
10/22/2013 06:11:30 PM UTC by root
$ site update sitename
------------------------------------------------------------
> Starting...
> Synchronizing temporary files with local site directory...
Source : /opt/sites/domain/uploads/
Destination : /var/www/domains/domain.com/www/htdocs/wp-content/uploads/
> Importing temporary database export into local database...
Filename : domain_com.sql
Source : /opt/sites/domain
Database : domain_com
> Setting 'domain' field in 'wp_blogs' table to 'dev01.domain.com'...
> Setting 'site_url' field in 'wp_options' table to 'http://dev01.domain.com/wordpress'...
> Setting 'home' field in 'wp_options' to 'http://dev01.domain.com'...
> Setting 'domain' field in 'wp_site' to 'dev01.domain.com'...
> Setting 'site_url' field in 'wp_sitemeta' table to 'http://dev01.domain.com/'...
> Done.
Hopefully this is useful to someone else. If you have any questions or comments, especially improvement suggestions, please leave them below.
Featured image by Andre Mouton.
Comments (2)
Previously posted in WordPress and transferred to Ghost.
mahender
January 22, 2014 at 5:30 am
Hi,
We have site for library.We will keep on updating the site.We are planing for mirror site for it.
I have understood with the help of rsync we can update the /var/www folder of mirror site.
How mysql database of mirror site will be in sync with production server.Could you please tell me how it can be achieved.
Thanks in advance,
Raja
Ryan Sechrest
January 22, 2014 at 8:55 am
The key commands arersync
(to synchronize files),mysqldump
to export a database to a file, andmysql
used to import a file into another database. With those three commands, all showcased in the above code, you can mirror files and databases between servers, or more specifically, keep one server (primary) synchronized with another (secondary).
If you look at my development kit page, toward the very bottom are links to the individual documentation of those commands.