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](https://www.deployhq.com), 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](https://www.deployhq.com).

* * *

## What Drush Deploy Does

The [`drush deploy`](https://www.drush.org/13.x/deploycommand/) 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com).

* * *

## Automating Drupal Deployments with DeployHQ

[DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com/signup) and create a new project. Connect your GitHub, GitLab, or Bitbucket repository where your Drupal codebase lives.

### Step 2: Add Your Server

Go to **Servers** → **New 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](https://deployhq.com/features/zero-downtime-deployments) enabled, [DeployHQ](https://www.deployhq.com) 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 Pipeline** → **New Command**. DeployHQ's [build pipeline](https://deployhq.com/blog/build-pipelines-in-deployhq-streamline-your-deployment-workflow) 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 Configuration** → **Cached 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](https://www.deployhq.com) 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](https://deployhq.com/blog/how-to-set-up-git-pull-deployments-with-deployhq). 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](https://www.deployhq.com) 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](https://deployhq.com/support/projects/configuration-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](https://www.deployhq.com) 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](https://www.deployhq.com) makes rollback straightforward:

1. **One-click rollback** : In [DeployHQ](https://www.deployhq.com), go to your deployment history and click **Rollback** on any previous deployment. [DeployHQ](https://www.deployhq.com) re-deploys the exact state of that revision.
2. **Zero-downtime rollback** : If you're using zero-downtime deployments, [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com/blog/how-to-implement-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](https://www.deployhq.com) 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](https://deployhq.com/features/zero-downtime-deployments). [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) trial — no credit card required.

