If you're still deploying WordPress changes over FTP, you're one accidental overwrite away from a bad day. Manual file transfers don't track what changed, can't be rolled back, and fall apart the moment a second developer touches the project. The fix isn't complicated: put your WordPress theme and plugin code in Git, and let a deployment tool handle the rest.
This guide walks through setting up automated Git-based deployments for WordPress — from structuring your repository to pushing changes to production with zero manual intervention.
Why FTP Deployments Break Down
FTP served WordPress well in 2010. In 2026, it's a liability:
- No change history. You can't answer
what changed last Tuesday?
when something breaks. - No rollback. If a deployment introduces a bug, the only option is to re-upload the previous files — assuming you still have them.
- No collaboration. Two developers editing the same theme via FTP will overwrite each other's work.
- No automation. Every deployment requires a human clicking through FileZilla. That human will eventually drag the wrong folder.
- No staging environment. You're deploying directly to production and hoping for the best.
Git-based deployments solve all five problems. Your code lives in a repository, every change is tracked, and deployments happen automatically when you push.
What to Track in Git (And What to Ignore)
Not everything in a WordPress installation belongs in version control. The general rule: track code you write, ignore everything you install or generate.
Recommended .gitignore for WordPress
# WordPress core — don't track, install via wp-cli or composer
/wp-admin/
/wp-includes/
/wp-*.php
/index.php
/license.txt
/readme.html
/xmlrpc.php
# wp-content: track selectively
/wp-content/uploads/
/wp-content/upgrade/
/wp-content/cache/
/wp-content/advanced-cache.php
/wp-content/object-cache.php
/wp-content/db.php
# Environment and secrets
.env
wp-config-local.php
*.sql
*.sql.gz
# Dependencies
/vendor/
/node_modules/
# OS files
.DS_Store
Thumbs.db
What You Should Track
wp-content/
├── themes/
│ └── your-custom-theme/ ← Track this
├── plugins/
│ └── your-custom-plugin/ ← Track this
└── mu-plugins/ ← Track this (must-use plugins)
If you use third-party plugins, manage them with Composer and track only the composer.json and composer.lock files — not the plugin source code itself.
Setting Up Git for WordPress
Option A: Track Only Your Theme/Plugin
The simplest approach — your Git repo contains only the code you write:
cd /path/to/your-theme
git init
git add .
git commit -m "Initial commit of theme"
git remote add origin git@github.com:yourteam/your-wp-theme.git
git push -u origin main
DeployHQ then deploys this repo to /var/www/html/wp-content/themes/your-theme/ on your server.
Option B: Track the Entire wp-content Directory
For projects where you manage multiple themes, plugins, and mu-plugins:
cd /path/to/wordpress/wp-content
git init
# Add your .gitignore first
git add .gitignore
git add themes/your-theme/ plugins/your-plugin/ mu-plugins/
git commit -m "Initial wp-content structure"
Option C: Full WordPress via Composer (Recommended for Teams)
Use Roots Bedrock or a Composer-based WordPress setup for the most professional workflow:
composer create-project roots/bedrock your-project
cd your-project
git init && git add . && git commit -m "Initial Bedrock setup"
Bedrock gives you:
- WordPress core managed via Composer (not tracked in Git)
.envfile for environment-specific configuration- Better directory structure separating web root from application code
- Plugin management via
composer require wpackagist-plugin/plugin-name
Method 1: Automated Deployments with DeployHQ
DeployHQ connects your Git repository to your server and deploys automatically on every push. No scripts to write, no CI pipeline to maintain.
Step 1: Create a Project
In your DeployHQ dashboard, create a new project and connect your GitHub, GitLab, or Bitbucket repository.
Step 2: Add Your Server
Add your production server using SSH (SFTP also works but SSH is faster and more reliable):
- Protocol: SSH/SFTP
- Hostname: your server IP or domain
- Username: your deploy user (don't use root)
- Authentication: SSH key (DeployHQ generates one — add the public key to your server's
~/.ssh/authorized_keys) - Deployment path: the target directory on your server, e.g.:
- Theme only:
/var/www/html/wp-content/themes/your-theme - Full wp-content:
/var/www/html/wp-content - Bedrock:
/var/www/html/current
- Theme only:
Step 3: Configure Build Steps
Build steps run on DeployHQ's servers before files are transferred. Use them for:
Install dependencies:
composer install --no-dev --optimize-autoloader
npm ci && npm run build
Compile assets:
# If your theme uses Webpack, Vite, or similar
npm run production
Step 4: Add Post-Deployment SSH Commands
After files are transferred, run commands on your server:
# Clear WordPress object cache
wp cache flush --path=/var/www/html
# Clear OPcache (if using PHP-FPM)
sudo systemctl reload php8.3-fpm
# Run database migrations (if using a migration plugin)
wp core update-db --path=/var/www/html
# Clear CDN cache (example: Cloudflare)
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer CF_TOKEN" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
Step 5: Enable Automatic Deployments
In your project settings, enable the webhook. DeployHQ will deploy automatically every time you push to the configured branch. You can also set up different branches for different servers:
main→ Production serverstaging→ Staging serverdevelop→ Development server
Step 6: Use .deployignore
Create a .deployignore file in your repo root to exclude files that shouldn't be transferred to the server:
# Development files
node_modules/
tests/
.github/
.gitignore
README.md
package.json
package-lock.json
phpcs.xml
phpunit.xml
# Source files (only deploy compiled assets)
src/scss/
src/js/
webpack.config.js
vite.config.js
This keeps your deployment lean — only production-ready files reach the server.
Method 2: GitHub Actions + SSH
For teams that want everything in their CI pipeline without a separate deployment tool:
name: Deploy WordPress Theme
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build assets
run: |
npm ci
npm run build
- name: Deploy via rsync
uses: burnett01/rsync-deployments@7.0.1
with:
switches: -avz --delete --exclude='.git' --exclude='node_modules' --exclude='src/'
path: ./
remote_path: /var/www/html/wp-content/themes/your-theme/
remote_host: ${{ secrets.SSH_HOST }}
remote_user: ${{ secrets.SSH_USER }}
remote_key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Post-deployment commands
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
wp cache flush --path=/var/www/html
sudo systemctl reload php8.3-fpm
This works, but you're maintaining the pipeline yourself. When something breaks at 2am, you're debugging YAML instead of deploying a fix. DeployHQ handles this infrastructure for you.
Method 3: WP-CLI Deployment Scripts
For power users who want full control via command-line scripts:
#!/bin/bash
set -euo pipefail
SERVER="deploy@your-server.com"
DEPLOY_PATH="/var/www/html/wp-content/themes/your-theme"
BRANCH="main"
echo "=== Deploying WordPress theme ==="
# Pre-flight checks
ssh $SERVER "test -d $DEPLOY_PATH" || { echo "Deploy path missing"; exit 1; }
# Build locally
npm ci && npm run build
# Sync files (excluding dev files)
rsync -avz --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='src/' \
--exclude='tests/' \
./ $SERVER:$DEPLOY_PATH/
# Post-deploy
ssh $SERVER "wp cache flush --path=/var/www/html && sudo systemctl reload php8.3-fpm"
echo "=== Deployment complete ==="
Simple, transparent, and portable. But it runs on your machine, so it doesn't work when you're offline, and other team members can't deploy without access to this script.
Handling the Hard Part: Database Migrations
WordPress doesn't have a built-in migration system like Rails or Laravel. Deploying code changes that require database schema updates needs careful handling.
Strategy 1: WP-CLI for Core Updates
# After deploying new WordPress core files
wp core update-db --path=/var/www/html
Strategy 2: Migration Plugins
Plugins like WP Migrate handle database syncing between environments. Run after code deployment to push schema changes.
Strategy 3: Custom Migration Scripts
For custom plugins that modify the database, use WordPress's dbDelta() function in your plugin activation hook, or run WP-CLI commands post-deployment:
wp eval 'your_plugin_run_migrations();' --path=/var/www/html
What NOT to Do
Never copy the production database to staging and then deploy staging code back to production. Data flows down (production → staging), code flows up (staging → production).
Multi-Environment Setup
A professional WordPress workflow uses at least three environments:
flowchart LR
A[Local] -->|git push| B[Git Repository]
B -->|auto-deploy| C[Staging]
C -->|manual promote| D[Production]
In DeployHQ
Create two servers in the same project:
| Server | Branch | Deploy Mode |
|---|---|---|
| Staging | staging |
Automatic on push |
| Production | main |
Manual (one-click) or automatic |
This gives you a safety net: code deploys to staging first, you verify it works, then merge to main to push to production.
WordPress Configuration Per Environment
Use wp-config.php with environment detection, or better yet, use a .env file (native with Bedrock):
// wp-config.php approach
$env = getenv('WP_ENV') ?: 'production';
if ($env === 'staging') {
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('DISALLOW_FILE_MODS', true);
}
WordPress-Specific .deployignore Patterns
Beyond the basics, these WordPress-specific exclusions keep your deployments clean:
# WordPress dev/test files
tests/
phpunit.xml.dist
phpcs.xml.dist
.phpcs.xml
# Theme development
src/scss/
src/js/
webpack.config.js
vite.config.js
tailwind.config.js
postcss.config.js
babel.config.js
# Documentation
*.md
LICENSE
CHANGELOG
# CI/CD config (handled by DeployHQ, not needed on server)
.github/
.gitlab-ci.yml
bitbucket-pipelines.yml
Common Mistakes to Avoid
Tracking wp-config.php in Git. This file contains database passwords. Use .env files or environment-specific config files that are in .gitignore.
Deploying node_modules. Your build step should compile assets. Only deploy the compiled CSS/JS, never the 200MB of npm packages.
Using root for SSH deployments. Create a dedicated deploy user with limited permissions. It should own the web directory and nothing else.
Skipping staging. It works on my machine
is not a deployment strategy. Always verify on staging first.
Forgetting to flush cache. WordPress aggressively caches everything. A deployment without a cache clear means users see stale content.
When to Use Each Method
| Factor | DeployHQ | GitHub Actions | WP-CLI Script |
|---|---|---|---|
| Setup time | 10 minutes | 1-2 hours | 30 minutes |
| Maintenance | None (managed) | You maintain YAML | You maintain script |
| Rollback | One-click | Manual revert | Manual revert |
| Multi-server | Built-in | DIY per environment | DIY per server |
| Team access | Web dashboard | Repo access required | CLI access required |
| Build steps | Built-in | GitHub-hosted runners | Your machine |
| Cost | Free tier available | Free for public repos | Free |
| Best for | Teams, agencies, multiple sites | Dev teams already using GH Actions | Solo devs, simple setups |
Get Started
The fastest path from FTP to automated deployments:
- Put your theme/plugin code in Git (10 minutes)
- Create a free DeployHQ project (5 minutes)
- Connect your repo and add your server (5 minutes)
- Push a change and watch it deploy automatically
That's it. No YAML to debug, no scripts to maintain, no FTP clients to open. Your WordPress deployments now work like every other modern web project.
If you run into issues, reach out to our team at support@deployhq.com or find us on Twitter/X.