Most Drupal shops still deploy with a combination of Drush commands, manual file transfers, and institutional memory. This works — until it doesn't. A misconfigured config import, a forgotten cache rebuild, a deployment that lands on production before it's been tested on staging: these are predictable failures from an unpredictable process.
This post walks through building a production-ready CI/CD pipeline for Drupal using GitHub Actions. By the end, every push to your main branch triggers an automated build, test, and deployment sequence that's repeatable, auditable, and reversible.
Why manual Drupal deployments are a liability
Manual deployments fail in specific, predictable ways:
- Someone forgets to run
drush updbafter deploying a module update - Config is exported from the wrong environment and overwrites production settings
- A deployment happens during business hours because 'it'll only take a minute'
- Nobody remembers which version is actually on staging because deployments weren't documented
- A rollback takes 45 minutes because there's no defined process
A CI/CD pipeline makes deployments boring and safe. The same sequence runs every time, in the same order, with the same checks.
What a Drupal CI/CD pipeline needs
Drupal deployments have specific requirements that generic pipeline templates don't handle well:
- Composer install — dependencies must be resolved and vendored correctly
- Config export/import — configuration changes must be tracked in version control and applied on deployment
- Database updates —
drush updbmust run after code deployment - Cache rebuild —
drush crafter updates - Config import —
drush cimto apply any configuration changes - Deployment hooks — environment-specific configuration via settings files
GitHub Actions: step-by-step setup
Create .github/workflows/deploy.yml in your repository. Here's a production-ready configuration:
name: Deploy Drupal
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: drupal
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, gd, pdo_mysql
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run PHPUnit tests
run: ./vendor/bin/phpunit --testsuite=unit
- name: Check coding standards
run: ./vendor/bin/phpcs --standard=Drupal web/modules/custom
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
uses: appleboy/[email protected]
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
cd /var/www/staging
git pull origin main
composer install --no-dev --optimize-autoloader
drush updb -y
drush cim -y
drush cr
drush deploy:hook
Handling config splits between environments
Production and staging often need different configuration — different base URLs, different logging levels, different API keys. The Config Split module handles this cleanly: you define a split per environment and store environment-specific config in a separate directory that doesn't get imported on other environments.
Store your deployment-specific settings in settings.php and settings.local.php (git-ignored), and use environment variables for secrets. Never commit credentials or environment-specific values to version control.
Automated testing: what to cover
A Drupal CI pipeline should run at minimum:
- PHPUnit unit tests — fast, test business logic in isolation. Should run on every PR.
- PHPUnit kernel tests — test integration with Drupal's kernel, including database. Slower but valuable for custom modules.
- PHP CodeSniffer — enforce Drupal coding standards automatically. Catches issues before code review.
- Config validation —
drush config:statusconfirms config in the repo matches what's in the database after import.
Deployment targets: Acquia, Pantheon, and self-managed
The pipeline above uses SSH deployment. Most managed Drupal hosts have native CI/CD integrations worth using instead:
- Acquia — use the Acquia Cloud API or Acquia Pipelines. Native config import and cache rebuild hooks are built into the deployment process.
- Pantheon — Terminus CLI integrates directly with GitHub Actions. Pantheon's multidev environments give you a full Drupal environment per branch.
- Self-managed on AWS/GCP — the SSH approach above works well, or use CodeDeploy for more sophisticated blue/green deployments.