CI/CD for WordPress: automating your deployment in 2026

CI/CD for WordPress: automating your deployment in 2026

Last verified: September 22, 2026
8 min read
Guide
Full-stack developer

Dragging themes over FTP in 2026 usually ends the same way: one file from inc/ is missing, composer.lock came from another laptop, and production is down because someone overwrote wp-config.php. Continuous Integration and Continuous Deployment are not enterprise theatre - they are how every Git change runs the same build, the same tests, and the same path onto the server.

This guide is for WordPress with a theme or plugin in the repository (Composer, npm, optionally Docker), not a site that only lives in the admin UI. If custom code still lives only in the database and media library, move themes and custom code into Git first - without that, CI/CD has nothing to build.

Runner docs: GitHub Actions. Keep WordPress server hardening in parallel: Hardening WordPress. For post-deploy commands, use WP-CLI.

#Why FTP and “just upload it” fail audits

Three problems keep showing up in UK and US WooCommerce reviews and agency handovers:

  1. Environment drift - local PHP 8.2 and Composer 2.7, VPS on PHP 8.1 without intl. Local composer install “works”; production dies on class not found.
  2. No audit trail - after an incident you cannot say which commit was live. The host panel shows a file date, not a Git SHA.
  3. Secrets on disk - SFTP passwords saved in FileZilla, or a .env committed “just for a minute”, remain a classic leak path.

CI/CD fixes this mechanically: build on a known image (shivammathur/setup-php or your own Docker), version the artifact by SHA, deploy over SSH with a key stored in Secrets.

#Minimal pipeline: push, build, test, deploy

A typical workflow for a custom theme plus Composer plugins:

  1. Trigger - push to main or merge a reviewed pull request. develop often deploys only to staging.
  2. Build - composer install --no-dev --optimize-autoloader, npm ci, npm run build. Pin PHP and Node in the workflow (php-version: '8.2', node-version: '20').
  3. Test - PHPUnit for plugins, optionally Playwright on the checkout path. Fail stops the job; no deploy.
  4. Artifact - pack a release without .git, without node_modules, with vendor/ and built assets/dist/.
  5. Deploy - rsync or SCP into releases/<run_id>/, then flip the symlink atomically.

Short GitHub Actions sketch:

name: deploy-production
on:
  push:
    branches: [main]
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - run: composer install --no-dev --optimize-autoloader
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
      - run: npm ci && npm run build
      - name: rsync release
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          # write key, rsync to releases/$GITHUB_SHA, ln -sfn

Keep the full symlink script in-repo as bin/release.sh and call it from the workflow. The same script still works from a laptop when Actions is down.

#Atomic deploys instead of overwriting files

Overwriting files in place under public_html means some requests briefly see an old functions.php with a new template. On WooCommerce that can corrupt cart sessions during a Black Friday push.

Atomic layout:

  • base: /var/www/site/
  • releases: /var/www/site/releases/20260920-142211/
  • shared: uploads, wp-config.php, object cache - outside the release
  • current → symlink to the active release

After a successful rsync:

ln -sfn /var/www/site/releases/20260920-142211 /var/www/site/current

Nginx root points at .../current/public (or your equivalent). The switch is immediate. Prune old releases after N days, or keep the last three for rollback.

WordPress uploads must live outside the release - otherwise every deploy deletes media or copies gigabytes. Classic pattern: shared/uploads linked into wp-content/uploads in each release.

#Staging, previews and a strategy matrix

In 2026, feature branch → PR → staging → main/production is not optional for shops and lead sites. Staging should share:

  • the same PHP version and extensions as production
  • a database copy (anonymised where personal data is in scope under GDPR/UK GDPR)
  • payment secrets in sandbox mode only

Per-PR preview URLs help redesigns and CSS reviews more than schema migrations - those still need staging with a real dump.

StrategyRiskUpkeep costWhen it fits
Manual FTPVery highCheap start, expensive outagesSandbox / learning only
Git hook on the serverMediumLowSmall portfolio, one developer
CI + atomic rsyncLowMediumBusiness sites, WooCommerce, agencies
Blue/green or two VPSMinimalHighHigh traffic, SLA, payments

Blue/green (two full environments, load-balancer flip) rarely pays below hundreds of concurrent sessions. Most mid-size UK shops do fine with an atomic symlink plus directory rollback.

#Secrets, WP-CLI and the database

Never commit:

  • SSH keys
  • database passwords
  • AUTH_KEY / SECURE_AUTH_KEY from wp-config.php
  • payment gateway API tokens

In GitHub: Settings → Secrets and variables → Actions. Inject them as env in the deploy step. On the server, wp-config.php lives under shared/ and is not part of the CI artifact.

WP-CLI after the symlink flip:

  • wp cache flush when object cache is on
  • wp rewrite flush after CPT changes
  • wp plugin list as a smoke line in the log

Plan database migrations apart from file deploys. Symlink rollback does not undo ALTER TABLE. For destructive migrations: backup first (wp db export on staging and production), migrate, then switch traffic to code that needs the schema - or gate it behind a PHP feature flag.

PHP dependency docs: Composer install - in CI always --no-dev for production and a locked composer.lock.

#What usually breaks the first WordPress pipeline

From VPS work (DigitalOcean, Linode, Hetzner) and managed hosts with SSH:

  • .gitignore eats vendor/ - production gets a theme without an autoloader. Either build vendor on the runner and ship it in the artifact, or run Composer on the server after rsync (then the server needs Composer and Packagist access).
  • Wrong paths - the script assumes /var/www/html while the host uses /home/user/domains/.... Pin paths in Secrets per environment.
  • Opcache keeps old code - after ln -sfn, reload PHP-FPM or call opcache_reset via WP-CLI/mu-plugin for the deploy user only.
  • Cron and queues - WooCommerce Action Scheduler can run jobs on old code for a few minutes; after a large release, watch failed jobs briefly.

Post-deploy smoke (curl from the runner or a separate job):

  • homepage HTTP 200
  • /wp-login.php 200
  • one critical product or form landing URL
  • optional HEAD to the CDN if assets are content-hashed

If smoke fails, automatic symlink rollback costs less than a client call at 23:00.

#Block themes, must-use plugins and Git exceptions

Not every WordPress surface fits the same pipeline. Content in the database (posts, ACF field groups if you do not export JSON, Elementor CSS) still lives outside the release. CI/CD protects code; it does not replace database backups.

Practical splits:

  • Classic or hybrid theme with Vite - full CI build, artifact with style.css + dist/.
  • Block theme (FSE) - keep theme.json and HTML templates in Git; Site Editor changes return via PR, or production and Git diverge after the first “Save”.
  • Must-use plugins - ship with the release; do not update them from the admin. Good place for deploy health checks and disabling the file editor.
  • wordpress.org plugins - pin versions via Composer (wpackagist) or update them in a reviewed job. Mixing “Update clicked in admin” with “CI overwrote plugins/” produces a random plugin tree.

Agency stacks often mix a custom theme, WooCommerce from wpackagist, and two premium zips installed via a Composer path repository. The pipeline must know all three sources or staging and production differ by one zip from six months ago.

#Checklist before the first Actions deploy

Before wiring main to production, walk this list once - it saves a weekend:

  1. Private repo, or confirm history has no secrets (git log -p does not show old .env files).
  2. Branch protection: required PR, required green test workflow.
  3. Separate Secrets: DEPLOY_HOST_STAGING, DEPLOY_HOST_PROD, separate SSH keys.
  4. Staging matches production PHP (not “we run 8.3, the client is on 8.1”).
  5. Database backup before the first atomic switch - even when code rollback is ready.
  6. Uptime monitoring (UptimeRobot, Better Stack, or similar) with SMS/Slack on the production URL.
  7. One-page runbook: how to ln -sfn to the previous release by hand, who has server access.

Only then enable auto-deploy from main. Until then, “build + test” on PRs plus manual workflow_dispatch to staging is enough.

#Summary

WordPress CI/CD is not “magic YAML”. It is a repeatable contract: same PHP, same lockfile, same artifact, a switch without a window of half-uploaded files. Start with Git and staging, add Actions for Composer/npm builds, finish with atomic current and a smoke test. FTP becomes an emergency tool, not the process.

If you need a WordPress developer who can keep that pipeline next to the theme code for a shop or lead site, WPPoland works from a real stack - not a slide deck.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Article FAQ

Frequently asked questions

Practical answers to apply the topic in real execution.

SEO-readyGEO-readyAEO-ready5 Q&A
Does CI/CD make sense for a solo freelancer?#
Yes. Solo developers lose the most from a forgotten PHP include or a composer.lock built on a different PHP version. The pipeline forces the same build on every commit and leaves an auditable log of which SHA went live.
What is zero-downtime deployment in WordPress?#
New code lands in releases/. Only after a successful rsync and optional wp cache flush do you flip the current symlink. PHP-FPM or Nginx serve through current, so the switch takes milliseconds and needs no maintenance mode.
Do I need a VPS for CI/CD?#
A VPS or containers give full control (SSH, symlinks, WP-CLI). On managed hosts you often get a deploy API or SFTP with a key in Secrets - still better than dragging files in FileZilla, even without true blue/green.
What if a deploy fails?#
Keep N previous release directories. If the smoke test returns 500 or vendor/ is missing, point the symlink back at the previous release. Plan database migrations separately - rolling back code does not undo ALTER TABLE.
Where should Vite or webpack themes be built?#
On the CI runner: checkout, setup Node, npm ci, npm run build, then pack dist/ into the artifact with PHP. Do not ship node_modules or TypeScript sources to production - only compiled CSS/JS.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles