If you're still deploying WordPress changes over FTP, you're one accidental overwrite away from a bad day. If you're not ready to abandon FTP entirely yet, you can still get version-controlled releases by [deploying to your FTP server from Git with DeployHQ](https://www.deployhq.com/blog/how-to-deploy-to-your-ftp-server-with-git-and-deployhq) — keep your existing host, add a Git pipeline on top. 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 via [WP Packages](https://wp-packages.org/) and track only the `composer.json` and `composer.lock` files — not the plugin source code itself. For guidance on building your own plugins with modern tooling and best practices, see our guide to [modern WordPress plugin development](https://deployhq.com/blog/modern-wordpress-plugin-development).

## 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](https://www.deployhq.com) 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](https://roots.io/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 wp-plugin/plugin-name` (Bedrock now uses [WP Packages](https://wp-packages.org/) instead of WPackagist)

## Method 1: Automated Deployments with DeployHQ

[DeployHQ](https://www.deployhq.com) 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](https://deployhq.com), 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://deployhq.com/blog/deploying-a-laravel-react-application-to-fortrabbit-using-deployhq). 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
```

For the full WP-CLI command reference — `search-replace` URL migrations, db export/import, plugin/theme management, multisite ops, cache flush sequence — see our [WP-CLI cheatsheet](https://www.deployhq.com/cheatsheets/wp-cli).

### Strategy 2: Migration Plugins

Plugins like [WP Migrate](https://deliciousbrains.com/wp-migrate-db-pro/) 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](https://deployhq.com/pricing) | 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](https://deployhq.com/signup)[DeployHQ](https://www.deployhq.com) 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](mailto:support@deployhq.com) or find us on [Twitter/X](https://x.com/deployhq).

