SiteShift

Push a WordPress site over SSH without deleting anything: the add-only rule

· 4 min read · by Lennart de Ridder

Every deployment tool eventually asks the same question: what should happen to a file that exists on the server but not on your machine? Most answer “delete it”, because that’s what “sync” means. SiteShift answers “leave it alone”, and this post is about why that rule — pushes only add and overwrite, never delete — is worth more than the tidiness it costs.

What “delete what isn’t local” actually deletes

On a WordPress site the server accumulates files you don’t have locally, and most of them are supposed to be there:

  • wp-content/uploads/ — every image a client added through wp-admin since your last pull.
  • Cache directories — LiteSpeed, WP Rocket, object-cache drop-ins written by the host.
  • Files the host manages — hostinger-* mu-plugins, .well-known/ for certificates, .htaccess rules added by a security plugin.
  • Plugins the client installed themselves, and their settings files.
  • wp-config.php, which differs by design between local and live.

A mirror-style sync (rsync --delete, or a “make remote match local” button) removes all of that unless you’ve excluded each item correctly, and the exclusion list is never complete on the first try. The failure is silent: the deploy reports success, and the client reports missing images on Monday.

The cost of a mistake is asymmetric. A file you failed to overwrite is a visible bug you fix with another push. A file you deleted is data loss, on the server, on the client’s site.

The add-only rule

So SiteShift’s push has one rule: a push can create or overwrite a file on the server; it can never delete one. Deletion is always something you do yourself, deliberately, on the server, knowing what you’re removing.

Mechanically it’s simple. The selected files are packed with tar on your machine, the archive goes over SFTP to a temporary directory on the server, and tar xzf unpacks it into the webroot. Extracting an archive writes the files in the archive; it doesn’t touch anything else. There is no “delete” step to get wrong because there is no delete step.

tar czf site.tar.gz --no-xattrs --exclude='._*' --exclude='./wp-config.php' -C webroot .
# → SFTP upload →
tar xzf site.tar.gz -C /home/u123456789/domains/acme-bakery.nl/public_html

Two exclusions are permanent. ._* files are macOS resource forks that tar on a Mac happily packs and a Linux server has no use for (on Windows there are none to exclude). And wp-config.php never leaves your machine: the local one points at your local database, and the remote one is the host’s business.

A review before anything moves

Add-only removes the catastrophic failure. The review screen removes the surprising one. Before the transfer starts, SiteShift compares your webroot with the server and shows a tree with three kinds of entries:

  • New — on your machine, not on the server. Will be created.
  • Modified — different on both sides. Will be overwritten with your version.
  • Remote only — on the server, not on your machine. Listed as “untouched”, which is exactly what happens to it.

You tick what goes. A theme fix is three files; a push of three files is what you should see, not “4 312 files synced”. Then you type the production domain into a confirm field. It’s a small friction, and it’s there because the one time you’re about to push the staging copy to production is the one time you want to be asked.

The database is the exception, so it gets a backup

Files can be add-only. A database can’t: importing your local database replaces the server’s tables. That’s the one destructive step in a push, so it’s opt-in per push and it’s wrapped:

  1. The server’s database is dumped with mysqldump to ~/siteshift-backups/<domain>-<timestamp>.sql.gz on the server.
  2. The dump is downloaded to ~/.siteshift/backups/<site>/ on your machine.
  3. Its size is checked. A 0-byte backup aborts the push before any import.
  4. Your local database is exported with wp search-replace from the local URL to the production URL — serialized data included, for reasons that deserve their own post.
  5. Only then is it imported.

If something is wrong afterwards, the previous database is in two places, named by date. Restoring it is one mysql < command.

What the rule costs

Deleted files don’t get deleted. If you remove a plugin locally, the plugin’s folder stays on the server until you remove it there. That’s the trade, and it’s the right one: an extra folder on a server is inert; a missing uploads folder is a phone call.

The other cost is that a push is not a mirror, so “the server is exactly what’s on my machine” is not a promise SiteShift makes. Pull is the mirror — in the other direction, onto your machine, where overwriting is cheap because it’s your copy.

If you deploy some other way

The rule travels. With rsync, drop --delete and add the two exclusions above. With Git-based deploys, make sure the post-receive hook doesn’t git clean. With any tool that has a “sync” button, look for the word “delete” in its options before pressing it — and if you can’t find how it treats remote-only files, assume the worst and test on a site you can afford to break.

L
Lennart de Ridder

Builds SiteShift and runs DeveloKey, a WordPress agency in Leiden. Every guide here comes from running client sites through the same tool.

Local WordPress with bundled PHP and MySQL, pushed to any host over SSH.

Get SiteShift — €9/mo