If you're still deploying WordPress changes over FTP, you're one accidental overwrite away from a bad day. Dragging files from your local machine straight onto the live site — no history, no staging, no undo — is how a Tuesday-afternoon tweak takes down a client's homepage. Manual file transfers don't track what changed, can't be rolled back, and fall apart the moment a second developer touches the project. 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. But the real fix is simpler than it sounds: put your WordPress theme and plugin code in Git, and let a deployment tool promote it from **local → staging → production** automatically.

This guide walks through the full local-to-production workflow for WordPress — from structuring your repository, to previewing on staging, to pushing changes live with zero manual file transfers. The same setup handles the two questions every WordPress developer eventually hits: _how do I move my site from local to live without breaking it,_ and _how do I promote from staging to production safely?_

## 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. Prefer a screenshot-driven walkthrough? Our [guide to deploying a WordPress theme with Git](https://www.deployhq.com/guides/how-to-deploy-your-wordpress-theme-with-git) covers the same connect-repo-to-server flow step by step.

## 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://www.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://www.deployhq.com), create a new project and connect your GitHub, GitLab, or Bitbucket repository. If you're deploying from GitHub, our [deploy from GitHub to your server](https://www.deployhq.com/deploy-from-github) walkthrough covers the repo connection in detail.

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

Don't have a production server yet? Provision one first — our [beginner's guide to installing WordPress on a VPS](https://www.deployhq.com/blog/deploying-a-wordpress-application-on-a-vps-a-beginner-s-guide) walks through Nginx, PHP 8.3, and a deploy user before you point Git at it.

### Step 3: Configure Build Steps

Build steps run on DeployHQ's servers before files are transferred. This is DeployHQ's [Build Pipeline](https://www.deployhq.com/features/build-pipelines) — use it 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. Because each release is transferred as a complete, self-contained set of files and only switched live once it's fully in place, you get [atomic deployments](https://www.deployhq.com/features/atomic-deployments): visitors never hit a half-uploaded site mid-deploy.

## 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://www.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).

## The Local → Staging → Production Workflow

Automating the _code_ deploy is only half of moving WordPress from local to live. The other half is the environment path itself. A professional WordPress workflow uses at least three environments, and the golden rule never changes: **code flows up (local → staging → production), data flows down (production → staging → local).**

```
flowchart LR
    A[Local] -->|git push| B[Git Repository]
    B -->|auto-deploy| C[Staging]
    C -->|manual promote| D[Production]
```

For a deeper look at keeping the three environments aligned so nothing drifts between them, see [keeping development, staging, and production in sync](https://www.deployhq.com/blog/keeping-development-staging-and-production-in-sync-with-deployhq).

### The #1 Local-to-Live Gotcha: URLs Baked Into the Database

The single most common way a move WordPress from local to production goes wrong isn't code — it's URLs. WordPress hard-codes your site URL (`http://localhost:8080`, `mysite.local`, etc.) throughout `wp_options`, post content, and widget data, and much of it is stored in **PHP-serialized arrays** where a naive find-and-replace corrupts the string-length prefixes and breaks the row.

Use WP-CLI's `search-replace`, which understands serialized data, instead of a raw SQL `UPDATE`:

```
# Rewrite local URLs to production URLs, serialization-safe.
# --dry-run first to preview the count of replacements:
wp search-replace 'http://mysite.local' 'https://mysite.com' \
  --all-tables --precise --recover --skip-columns=guid --dry-run

# Drop --dry-run to apply once the count looks right.
```

`--skip-columns=guid` keeps feed/GUID identifiers stable (changing them makes RSS readers re-import every post), and `--precise` forces PHP-level unserialization so nested arrays survive the rewrite.

### 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. If a production release does go wrong, [one-click rollback](https://www.deployhq.com/features/one-click-rollback) returns the site to the previous release without re-uploading anything by hand.

### Staging-to-Production Promotion Checklist

Before you promote a staging build to the live site, run through this every time:

1. **Assets built on staging?** Confirm the Build Pipeline compiled CSS/JS on the staging deploy, not just locally.
2. **Database changes accounted for?** Run `wp core update-db` and any plugin migrations as post-deploy commands.
3. **URLs correct for the target?** Never carry `localhost`/staging URLs into production — `search-replace` runs against the production DB only.
4. **Cache flush queued?** Object cache, OPcache, and CDN purge as post-deploy steps, or users see stale content.
5. **Rollback ready?** Confirm the previous release is retained so one-click rollback is available if the promote misbehaves.

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

**Forgetting the URL rewrite.** Pushing a local database to production without `wp search-replace` leaves `localhost` links scattered through the site — the classic images and links broken after going live symptom.

## 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://www.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 an automated local-to-production workflow:

1. Put your theme/plugin code in Git (10 minutes)
2. [Create a free](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) project (5 minutes)
3. Connect your repo and add your staging and production servers (5 minutes)
4. Push a change, verify on staging, promote to production

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 — local to live, tracked end to end.

* * *

If you run into issues, reach out to our team at [support@deployhq.com](mailto:support@deployhq.com) or find us on [X/Twitter](https://x.com/deployhq).

