How to Automate WordPress Deployments with Git (No More FTP)

Devops & Infrastructure, Tutorials, and Wordpress

How to Automate WordPress Deployments with Git (No More FTP)

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.

# 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"

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)
  • .env file 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

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 server
  • staging → Staging server
  • develop → 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:

  1. Put your theme/plugin code in Git (10 minutes)
  2. Create a free DeployHQ project (5 minutes)
  3. Connect your repo and add your server (5 minutes)
  4. 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.