Introduction
Most teams still manage WordPress through FTP uploads and manual file edits on the server—a workflow that scales poorly, invites mistakes, and leaves no audit trail. Deploying WordPress with Git: what developers should know means treating your WordPress codebase the same way you treat any modern application: version-controlled, automated, and reproducible across environments.
In this piece, we'll walk through a Git-based deployment strategy that removes FTP entirely, keeps your production code clean, and lets your team deploy with confidence. Whether you're running a small agency or managing WordPress for a SaaS platform, this approach pays dividends fast.
Track code only — Themes, plugins and config in Git
The first principle: your Git repository should contain only code, not content or uploads. This distinction matters because WordPress mixes application code with user-generated content in the same filesystem, and that's where things fall apart.
A well-structured WordPress repository looks like this:
wordpress/
├── web/
│ ├── wp-config.php (generated, not committed)
│ ├── wp-content/
│ │ ├── plugins/ (managed via composer)
│ │ ├── themes/
│ │ │ └── custom-theme/ (your code)
│ │ └── uploads/ (gitignored)
│ └── ...
├── composer.json (committed)
├── .gitignore (critical)
└── README.mdYour .gitignore must exclude:
wp-config.phpand.envfiles (database credentials)wp-content/uploads/(user media)wp-content/plugins/from third parties (managed by Composer instead)node_modules/andvendor/(installed at deployment time)
This means your custom themes and any custom plugins you've written go into Git. A typical custom theme structure:
wp-content/themes/custom-theme/
├── functions.php
├── style.css
├── template-parts/
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
├── package.json (for frontend tooling)
└── README.mdWhen a developer clones the repository and runs deployment steps (which we'll cover), they get the exact same code as production. No more "it works on my machine but not on staging"—your local environment and production become reproducible.
Composer — Manage plugins with WPackagist
Installing plugins via the WordPress admin UI or by uploading ZIP files is convenient but makes deployments fragile. WPackagist.org mirrors the WordPress.org plugin repository as Composer packages, letting you declare dependencies in composer.json and install them consistently across all environments.
Set up Composer in your WordPress project:
{
"name": "company/wordpress-project",
"require": {
"php": ">=8.1",
"wpackagist-plugin/advanced-custom-fields": "^6.1",
"wpackagist-plugin/wpml-core": "^4.4",
"wpackagist-theme/storefront": "^4.0"
},
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
}
],
"extra": {
"wordpress-install-dir": "web/wordpress",
"wordpress-content-dir": "web/wp-content"
}
}
When you run composer install, all plugins and themes listed install into the correct WordPress directories. When you commit composer.lock, your entire team and CI pipeline get identical versions. No surprises in production.
For premium plugins you've purchased (like GeneratePress Pro), you can use private Composer repositories. Satis or a private Packagist mirror lets you host them yourself, keeping authentication out of version control.
This approach also means plugin updates happen in your local environment first, tested thoroughly, then deployed through Git commits. It's a far cry from the one-click "Update" button that can break a live site.
CI deploys — GitHub Actions deploys to the server
Once your code is in Git and dependencies are managed through Composer, automation becomes straightforward. A CI/CD pipeline (like GitHub Actions) watches your repository, runs tests, and deploys on merge to main.
Here's a minimal GitHub Actions workflow that deploys WordPress to a production server:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Composer dependencies
run: composer install --no-dev --optimize-autoloader
- name: Deploy to server
uses: deploymentio/actions-sh@main
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_KEY }}
script: |
cd /var/www/wordpress
git pull origin main
composer install --no-dev --optimize-autoloader
wp db migrate
wp cache flushThe workflow:
- Developer pushes a commit to the main branch.
- GitHub Actions checks out the code.
- Composer installs dependencies (production-only, no dev packages).
- SSH into your server and pull the latest code.
- Run any database migrations (if you use a migration tool).
- Flush caches to serve fresh content.
This removes the manual FTP step entirely. Every deploy is logged, versioned, and reproducible. If something breaks, you roll back by reverting the commit and pushing again.
You can also add testing steps before deploy: PHPUnit tests, PHPCS linting, or theme validation. If tests fail, the deployment stops automatically.
Bedrock — A modern project structure for WordPress
Roots' Bedrock project is a modernized WordPress skeleton that bakes in best practices from day one. Instead of the default WordPress structure, Bedrock organizes files for version control, environment configuration, and deployment automation.
Key features:
- Environment-based configuration: Use
.envfiles for database credentials, API keys, and environment-specific settings instead of scatteredwp-config.phpconstants. - Separated web root: The
web/directory is your WordPress install; your project root stays clean for Git. - Composer-first dependency management: WordPress core itself is installed via Composer, not uploaded as a ZIP.
- WP-CLI integration: Built-in support for WP-CLI commands like database imports and cache clearing.
A Bedrock project structure:
project-root/
├── web/ (web server root)
│ ├── wp-content/
│ │ ├── plugins/
│ │ ├── themes/
│ │ └── uploads/ (gitignored)
│ ├── wp-config.php (generated from .env)
│ └── index.php
├── .env (gitignored, secrets here)
├── .env.example (committed, shows structure)
├── composer.json
├── composer.lock
└── config/
├── environments/
│ ├── development.php
│ └── production.php
└── application.phpEnvironment-specific settings live in PHP files, not wp-config.php:
// config/environments/production.php
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('SCRIPT_DEBUG', false);
define('WP_MEMORY_LIMIT', '256M');
define('WP_POST_REVISIONS', 10);
// config/environments/development.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('SCRIPT_DEBUG', true);On deployment, your server reads the production .env file, which isn't in Git. Each environment (local, staging, production) has its own .env, keeping secrets out of version control and configuration environment-aware.
Bedrock also includes sensible defaults for security: DISALLOW_FILE_EDIT is true by default (no theme editor in the admin), and database table prefixes are randomized.
To start a Bedrock project, use Composer:
composer create-project roots/bedrock my-wordpress-siteCommon mistakes
Committing wp-config.php: Database passwords, API keys, and salts should never be in Git. Use .env files or a secrets management tool instead.
Ignoring uploads/ but tracking plugins: Users' media shouldn't be in Git (it's huge and changes constantly), but third-party plugins should be managed via Composer, not committed directly.
Deploying without running composer install: If your server checks out code but skips dependency installation, you'll have missing files and broken sites. Always run Composer as part of your deployment script.
Manual database migrations: Running schema changes by hand on production is error-prone. Use a migration tool (like Phinx or a custom WP-CLI command) to automate them as part of your deploy.
Testing only in production: Use GitHub Actions or similar to catch mistakes before they hit live. At minimum, run PHPUnit tests and code standards checks before merging.
Key takeaways
- Keep Git repositories clean: commit code and configuration, gitignore secrets, uploads, and dependencies.
- Use Composer and WPackagist to manage plugins and themes as versioned dependencies, not manual uploads.
- Automate deployments with CI/CD (GitHub Actions, GitLab CI, etc.) to eliminate manual FTP steps and add safety checks.
- Bedrock provides a modern WordPress structure out of the box, handling environment config, dependency management, and deployment patterns.
- A Git-based workflow gives you audit trails, rollback capabilities, and reproducible environments across your entire team.






Comments
Loading comments…