The problem with FTP
Most small-agency WordPress work is still deployed by editing files on the server, or by uploading over SFTP and hoping the local copy matches. It works until two people touch the same theme, or a change breaks production with no way back, or a client asks what changed in March and nobody knows. Git fixes all three, and the workflow does not need to be elaborate.
This is the one we use across a portfolio of client sites.
What goes in the repository
The repository holds the code you write and nothing you can reinstall.
In:
- The theme.
- Any site-specific plugins and must-use plugins.
- Configuration that is code:
composer.jsonif you manage plugins with Composer, a.env.example, deployment scripts. - A
wp-config.phpthat reads secrets from environment variables, committed without secrets.
Out:
- WordPress core. Install it per environment or via Composer.
- Third-party plugins from the directory or vendors. Manage them via Composer, or install them on each environment and record the list. Committing them bloats the repo and hides the version history.
wp-content/uploads. Media lives on the server or in object storage, never in Git..envwith real credentials, cache directories,node_modules.
A .gitignore for WordPress that whitelists your theme and plugins and ignores everything else is the cleanest way to enforce this.
Environments
Three, always: local, staging, production. Each has its own database and its own uploads. Code moves forward through them via Git; data moves backward from production when you need a realistic copy.
Local is where work happens, on a Docker-based setup or a local WordPress tool. Staging is a server that mirrors production, at a private URL, where the client reviews and where updates are tested. Production is live.
Configuration differs per environment only through environment variables: database credentials, site URL, debug flags, API keys. The code is identical everywhere.
The deployment
A deploy is: push to a branch, and something puts that branch's code on a server. The something can be simple.
Minimal: a Git hook or a CI job that SSHes into the server and runs git pull in the theme directory, then clears caches. Adequate for a single developer and small sites.
Better: a CI pipeline (GitHub Actions, GitLab CI) that on push to staging builds assets, runs any checks, and deploys to the staging server; on push to main does the same to production. Deployment tools like Deployer, or host-native Git deploys on WP Engine, Kinsta and similar, handle the file transfer atomically so a half-deployed state never serves.
Atomic releases matter: deploy into a new directory, then switch a symlink. If anything fails, the symlink still points at the last good release, and rollback is switching it back.
The database
Code goes forward. Content goes backward. Never push a local database to production; you will overwrite the orders and bookings that happened since you pulled.
When you need production data locally or on staging: export production, import to the target, search-and-replace the site URL (WP-CLI's search-replace handles serialised data correctly). Do this on demand, not automatically.
Schema changes, new post types, new fields, live in code (the plugin registers them) and need no database migration. Settings changes that live in the database, plugin options, menus, are the awkward case; document them, or manage the important ones as code via configuration plugins.
Uploads
Production uploads are the truth. Staging and local can proxy missing images from production with a small rewrite rule, so you never sync gigabytes of media just to test a layout. For sites with large media libraries, offloading uploads to object storage makes every environment read the same files.
Plugin updates in this workflow
Third-party plugins update on staging first, through Composer version bumps or through the admin on staging. Test. Then the same update on production, via the same mechanism. If plugins are Composer-managed, the update is a commit, which means it is in the history and it is reversible.
Rollback
Because releases are atomic and code is versioned, rollback is: switch the symlink to the previous release, or revert the commit and redeploy. Under a minute. Database rollback is the daily backup, which is why the backup and the deployment are separate concerns and both need to exist.
What it costs to set up
A day per site the first time, an hour per site once you have the template pipeline. The return is every subsequent change being reviewable, reversible and reproducible, and every client site being deployable by whoever is on shift rather than whoever remembers the SFTP password.
For an agency running several client sites, this is also the workflow that makes a care plan scalable: staged updates, tested, deployed the same way every month, with a history. It is how our own client work runs, and it is the first thing we set up when we take a site on for WordPress development.