Most Drupal developers know how to build features. Fewer have invested in the automation layer that makes those features deployable safely, testable reliably, and maintainable long-term. After running CI/CD pipelines for 30+ production Drupal sites over the past five years, here is the complete automation stack we use โ€” and the hard-won lessons behind each decision.

1. The Drupal Deployment Stack

A modern Drupal automation stack consists of:

  • Version control: Git with a defined branching strategy (GitFlow or trunk-based)
  • CI/CD pipeline: GitLab CI (our preference), GitHub Actions, or CircleCI
  • Local development: DDEV (standardised across all team members)
  • Testing: PHPUnit (unit/kernel/functional), Behat (browser tests), PHPCS (code standards)
  • Preview environments: Tugboat or DDEV for PR review
  • Hosting: Acquia, Pantheon, or custom AWS/GCP with Nginx + PHP-FPM
  • Deployment tool: Drush for database updates and cache management

2. GitLab CI/CD Pipeline

Our standard .gitlab-ci.yml pipeline for a Drupal project runs four stages:

stages:
  - validate
  - test
  - build
  - deploy

validate:phpcs:
  stage: validate
  script:
    - vendor/bin/phpcs --standard=Drupal,DrupalPractice
      web/modules/custom web/themes/custom

validate:phpstan:
  stage: validate
  script:
    - vendor/bin/phpstan analyse web/modules/custom

test:phpunit:
  stage: test
  script:
    - cp .env.ci .env
    - vendor/bin/phpunit --testsuite=unit,kernel
  coverage: '/^\s*Lines:\s*\d+.\d+\%/'

test:behat:
  stage: test
  script:
    - vendor/bin/behat --profile=ci --format=progress

deploy:production:
  stage: deploy
  only: [main]
  script:
    - ./scripts/deploy.sh production
  environment: { name: production, url: https://example.com }

3. Automated Testing Strategy

We enforce three levels of automated tests on every Drupal project:

Unit tests cover pure PHP logic โ€” service classes, data transformers, utility functions. Fast, no database, no Drupal bootstrap:

// tests/src/Unit/PriceFormatterTest.php
public function testFormatWithCurrencySymbol(): void {
  $formatter = new PriceFormatter('INR');
  $this->assertEquals('โ‚น1,234.50', $formatter->format(1234.50));
}

Kernel tests test Drupal services with a partial bootstrap โ€” database available, services available, no HTTP requests. Best for testing custom fields, event subscribers, and plugin logic.

Functional/Behat tests drive a real browser via WebDriver and test editorial workflows end to end:

Scenario: Editor can publish a draft article
  Given I am logged in as an editor
  When I create a draft article with title "Test Post"
  And I click "Submit for Review"
  Then I should see "Needs Review" in the moderation state
  When I log in as a reviewer
  And I publish the article
  Then the article should be visible at "/blog/test-post"

4. Database Updates & Config Management

Every deployment runs database updates and config import atomically:

#!/bin/bash
# scripts/deploy.sh
set -e

echo "Putting site into maintenance mode"
drush state:set system.maintenance_mode 1

echo "Pulling code"
git pull origin main

echo "Installing dependencies"
composer install --no-dev --optimize-autoloader

echo "Running database updates"
drush updatedb -y

echo "Importing configuration"
drush config:import -y

echo "Rebuilding caches"
drush cache:rebuild

echo "Taking site out of maintenance mode"
drush state:set system.maintenance_mode 0

echo "Deployment complete"

5. Tugboat PR Preview Environments

Every pull request automatically gets a preview environment via Tugboat. Our .tugboat/config.yml spins up a full Drupal environment with a sanitised copy of the production database within 4โ€“6 minutes of a PR being opened. This means code reviewers and QA can test changes in a real environment โ€” not just read code โ€” before anything merges.

The ROI on Tugboat is exceptional. A single caught regression in a preview environment is worth more than the entire monthly Tugboat bill.

6. Zero-Downtime Deployment

Zero-downtime deployment for Drupal requires handling the window between "old code reading new database" and "new code reading new database." Our approach:

  1. Deploy new code alongside old code (blue/green or rolling, depending on hosting)
  2. Run drush updatedb โ€” hook_update_N functions must be backwards-compatible
  3. Import configuration (drush config:import) โ€” staged-file-based to prevent partial states
  4. Switch traffic to new code
  5. Rebuild caches with new code active

On Acquia and Pantheon, blue/green deployments are built into the platform. On custom infrastructure, we use Nginx upstream switching or AWS ALB target group swapping.

7. Monitoring and Alerting

Automation doesn't end at deployment. Production monitoring for Drupal sites should include:

  • Application performance: New Relic or Datadog for PHP request traces and slow query detection
  • Uptime monitoring: Pingdom or UptimeRobot for endpoint health checks every 60 seconds
  • Error tracking: Sentry with the Drupal Raven module for PHP exception capture
  • Security scanning: Scheduled drush pm:security in CI + automated Dependabot PRs
  • Database backup verification: Daily restore test on a staging environment