Deploying Drupal with Drush and DeployHQ

PHP, Tips & Tricks, and Tutorials

Deploying Drupal with Drush and DeployHQ

Deploying Drupal sites involves database updates, configuration imports, cache rebuilds, and custom hooks — steps that are easy to forget or get wrong when done manually. Drush automates all of this into a single command, and when paired with DeployHQ, your entire Drupal deployment becomes a push-to-deploy workflow.

This guide covers how drush deploy works, how to write custom deploy hooks, and how to set up fully automated Drupal deployments with DeployHQ.


What Drush Deploy Does

The drush deploy command runs a sequence of operations in the correct order every time:

  1. drush updatedb — runs pending database updates to match the deployed code
  2. drush config:import — imports configuration changes from your codebase into the database
  3. drush cache:rebuild — clears and rebuilds all caches
  4. drush deploy:hook — executes any custom deploy hooks you've defined

Without Drush, you'd run each of these manually (and risk forgetting one). With drush deploy, the entire post-deployment sequence is a single, repeatable command.


Essential Drush Commands for Deployments

Beyond drush deploy, here are the commands you'll use most when managing Drupal:

Command What It Does
drush deploy Runs the full deployment sequence (updatedb → config:import → cache:rebuild → deploy:hook)
drush updatedb Applies pending database schema updates
drush config:import Imports configuration YAML files into the active database
drush config:export Exports active configuration to YAML files
drush cache:rebuild Clears and rebuilds all Drupal caches
drush deploy:hook Runs custom deploy hook implementations
drush status Shows Drupal installation status and environment info
drush pm:list Lists all installed modules with their status
drush cron Runs Drupal's cron tasks
drush watchdog:show Displays recent log messages
drush sql:dump Exports the database as SQL
drush state:set system.maintenance_mode 1 Enables maintenance mode

Writing Custom Deploy Hooks

Deploy hooks run automatically as the last step of drush deploy. They're useful for one-time data migrations, content updates, or any task that needs to happen exactly once after a deployment.

Creating a Deploy Hook

Deploy hooks are defined in your module's .deploy.php file. Each hook is numbered — Drush tracks which hooks have already run and only executes new ones.

Create my_module.deploy.php in your module's root directory:

<?php

/**
 * @file
 * Deploy hooks for my_module.
 */

/**
 * Migrate legacy user roles to new permission structure.
 */
function my_module_deploy_10001(array &$sandbox): string {
  // First run — initialise the batch.
  if (!isset($sandbox['total'])) {
    $sandbox['total'] = \Drupal::entityQuery('user')
      ->condition('roles', 'legacy_editor')
      ->count()
      ->accessCheck(FALSE)
      ->execute();
    $sandbox['current'] = 0;
  }

  // Process 50 users per batch.
  $uids = \Drupal::entityQuery('user')
    ->condition('roles', 'legacy_editor')
    ->range(0, 50)
    ->accessCheck(FALSE)
    ->execute();

  foreach ($uids as $uid) {
    $user = \Drupal\user\Entity\User::load($uid);
    $user->removeRole('legacy_editor');
    $user->addRole('content_editor');
    $user->save();
    $sandbox['current']++;
  }

  // Tell Drush whether we're done.
  $sandbox['#finished'] = $sandbox['total'] > 0
    ? $sandbox['current'] / $sandbox['total']
    : 1;

  return "Migrated {$sandbox['current']}/{$sandbox['total']} users.";
}

/**
 * Set default value for new site configuration.
 */
function my_module_deploy_10002(): string {
  \Drupal::configFactory()
    ->getEditable('my_module.settings')
    ->set('enable_new_feature', TRUE)
    ->save();

  return 'Enabled new feature flag.';
}

How Deploy Hook Numbering Works

  • Hooks are named {module}_deploy_{number}
  • Drush stores which numbers have been executed in a key-value store
  • Only hooks with numbers higher than the last executed number will run
  • Use a numbering scheme like 10001, 10002, etc. to leave room for ordering

When to Use Deploy Hooks vs. Update Hooks

Use Deploy Hooks (deploy.php) Use Update Hooks (install)
Content or data migrations Database schema changes
One-time configuration changes Module install/uninstall logic
Post-deployment cleanup tasks Changes that need to run before config import

The key difference: deploy hooks run after config:import, while update hooks run before it (during updatedb).


Manual Deployment Script

If you're deploying Drupal to a server with SSH access, the basic workflow is:

# Pull latest code
cd /var/www/drupal
git pull origin main

# Install/update dependencies
composer install --no-dev --no-progress --optimize-autoloader

# Run the full deploy sequence
drush deploy

# Verify the site is healthy
drush status
drush watchdog:show --count=5

This works, but you're running it manually every time. Let's automate it with DeployHQ.


Automating Drupal Deployments with DeployHQ

DeployHQ connects your Git repository to your server and handles the full deployment lifecycle — file transfer, build steps, and post-deploy commands — every time you push code.

Step 1: Create a Project

Sign up at deployhq.com and create a new project. Connect your GitHub, GitLab, or Bitbucket repository where your Drupal codebase lives.

Step 2: Add Your Server

Go to ServersNew Server and configure:

  • Name: Production (or Staging, etc.)
  • Protocol: SSH/SFTP
  • Hostname: Your server's IP or domain
  • Username: deployhq (a dedicated deploy user)
  • Deployment Path: /var/www/drupal
  • Zero-downtime deployments: Enabled

With zero-downtime deployments enabled, DeployHQ deploys to a new release directory and atomically switches a symlink when everything is ready. Your site stays live throughout the deploy — no downtime, no broken requests.

Note: With zero-downtime enabled, your files will be served from /var/www/drupal/current. Update your web server configuration to point the document root to this path.

Setting Up SSH Key Authentication

On your server, set up the deployhq user:

# Create the deploy user
sudo adduser --disabled-password deployhq

# Set up SSH directory
sudo -u deployhq mkdir -p ~/.ssh
sudo -u deployhq chmod 700 ~/.ssh

# Add DeployHQ's public key (shown in the DeployHQ server setup screen)
sudo -u deployhq nano ~/.ssh/authorized_keys
# Paste the key, save, and exit

sudo -u deployhq chmod 600 ~/.ssh/authorized_keys

Make sure the deployhq user has write access to the deployment path and can run drush and composer.

Step 3: Configure the Build Pipeline

Go to Build PipelineNew Command. DeployHQ's build pipeline runs commands on DeployHQ's servers before transferring files, so you don't need build tools installed on your production server.

Add a Composer build command:

composer install --no-dev --no-progress --optimize-autoloader

Enable Stop deployment if this command fails — if composer install fails, there's no point deploying broken code.

Speed up builds with caching: Go to Build ConfigurationCached Files and add:

vendor/**

This caches your vendor directory between deployments so Composer doesn't re-download every package on each deploy.

Step 4: Add Post-Deploy SSH Commands

Go to SSH Commands and add a command that runs after files are transferred:

cd /var/www/drupal/current && drush deploy

This runs the full Drush deployment sequence (database updates → config import → cache rebuild → deploy hooks) on your server after DeployHQ has deployed the new code.

You can also add a health check after the deploy:

cd /var/www/drupal/current && drush status --field=bootstrap | grep -q "Successful" && echo "Deploy OK" || echo "Deploy FAILED"

Step 5: Enable Automatic Deployments

In your project settings, enable automatic deployments. Now every push to your main branch triggers a deployment automatically:

git push → DeployHQ detects change → Runs composer install → Transfers files → Runs drush deploy → Done

No manual steps. No SSH-ing into the server. Push your code and DeployHQ handles the rest.

Step 6: Use Config Files for Per-Environment Settings

If you deploy to multiple environments (staging, production), use DeployHQ's config files to inject environment-specific settings. For example, inject a sites/default/settings.local.php with different database credentials per server — without committing secrets to your repository.


Customising the Deploy Process

Maintenance Mode During Deploys

For major updates that change database schemas, you may want to enable maintenance mode:

cd /var/www/drupal/current && \
  drush state:set system.maintenance_mode 1 && \
  drush deploy && \
  drush state:set system.maintenance_mode 0

Add this as your SSH command in DeployHQ instead of the plain drush deploy.

Customising Drush Subcommands

You can modify how drush deploy behaves by adjusting the configuration for its subcommands. For example, to skip certain config during import, add to your drush/drush.yml:

command:
  config:
    import:
      options:
        partial: true
  updatedb:
    options:
      cache-clear: false

Rolling Back a Failed Deployment

If something goes wrong, DeployHQ makes rollback straightforward:

  1. One-click rollback: In DeployHQ, go to your deployment history and click Rollback on any previous deployment. DeployHQ re-deploys the exact state of that revision.
  2. Zero-downtime rollback: If you're using zero-downtime deployments, DeployHQ keeps previous release directories. Rolling back switches the symlink back to the previous release — instant, with no downtime.
  3. Database rollback: If the deployment included database changes, you may need to restore from a database backup. Consider running a database snapshot before each deploy (see our guide to server backups with AWS S3).

Common Questions

Can I run Drush commands without SSH access?

No. Drush is a command-line tool that requires direct server access. DeployHQ runs Drush commands over SSH as part of the post-deploy step, so you need SSH access configured on your server.

Will my site go down during deployments?

Not with DeployHQ's zero-downtime deployments. DeployHQ deploys to a new release directory and only switches the symlink after all files are in place and post-deploy commands have succeeded. Your visitors see zero interruption.

How do I deploy to multiple environments?

Create separate servers in DeployHQ for each environment (staging, production). Map different Git branches to each server — for example, develop deploys to staging and main deploys to production. Use config files to inject environment-specific database credentials and settings.

What if drush deploy fails mid-way?

If drush deploy fails (for example, a deploy hook throws an error), DeployHQ reports the failure in its dashboard. With zero-downtime deployments, the symlink hasn't switched yet, so your production site is still running the previous release. Fix the issue, push again, and DeployHQ will retry.

What version of Drush do I need?

The drush deploy command is available in Drush 10.3+ and is fully supported in Drush 12 and 13. If you're on an older version, update via Composer:

composer require drush/drush:^13.0

The Bottom Line

Drush turns complex Drupal deployment steps into a single command, and DeployHQ turns that command into an automated, push-to-deploy workflow. Together, you get reliable, repeatable Drupal deployments with zero downtime — no manual SSH sessions, no forgotten steps.

Ready to automate your Drupal deployments? Start your free DeployHQ trial — no credit card required.