Deploy WordPress from Local to Production with Git (No More FTP)

Devops & Infrastructure, Tutorials, and Wordpress

Deploy WordPress from Local to Production with Git (No More FTP)

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

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

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 wp-plugin/plugin-name (Bedrock now uses WP Packages instead of WPackagist)

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. If you're deploying from GitHub, our deploy from GitHub to your server 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 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 — 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 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: 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 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

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.

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

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.

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 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 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 DeployHQ 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 or find us on X/Twitter.