RSS Amplifier

Matthew Tift - Kripalu Yoga Teacher in the Twin Cities · Jun 10, 2026

From Lando to DDEV: Replacing Custom Shell Scripts with Drainpipe

0
Sign in to vote or save

matthewtift.com

That is the entire DDEV configuration. For most Drupal projects, it is enough to start.

Replacing Shell Scripts with Drainpipe

DDEV solves the container environment. It does not solve the workflow, such as the commands developers run to download a database, import it, apply updates, and validate code. That is where Drainpipe comes in.

Drainpipe is a Composer package from Lullabot that provides a Task-based workflow for Drupal projects. Task is a task runner similar to Make, but defined in YAML. Drainpipe ships task definitions for common operations such as drupal:updatedrupal:import-dbdrupal:composer:development, and others, which projects can include and extend. A project-level Taskfile.yml includes those bundles and adds project-specific tasks on top:

version: '3'
dotenv: ['.env', '.env.defaults']
includes:
  deploy: ./vendor/lullabot/drainpipe/tasks/deploy.yml
  drupal: ./vendor/lullabot/drainpipe/tasks/drupal.yml
  snapshot: ./vendor/lullabot/drainpipe/tasks/snapshot.yml
  test:
    taskfile: ./vendor/lullabot/drainpipe-dev/tasks/test.yml
    optional: true
tasks:
  sync:
    desc: "Sync a database from production and import it"
    cmds:
      # Replace this with a command to fetch your database.
      - ./vendor/bin/drush site:install -y
      - echo "🧹 Sanitising database"
      - ./vendor/bin/drush sql:sanitize --yes
  # Drush aliases will be passed through e.g. task update site=@staging
  update:
    desc: "Runs the Drupal update process"
    cmds:
      - task: drupal:update
  validate:
    desc: "Run PHP linting and Drupal coding standards checks on custom code"
    cmds:
      - ./vendor/bin/parallel-lint -e php,module,inc,install docroot/modules/custom
      - ./vendor/bin/phpcs --standard=Drupal docroot/modules/custom

Every task has a desc field. Tasks with more complexity can add a summary block for extended documentation. That makes the workflow discoverable without reading source files:

ddev task --list
ddev task --summary sync
ddev task --summary setup:post-import

A developer who has never touched the project can run ddev task --list and see what exists. The documentation lives in the same file as the implementation.

Documenting Site-specific Quirks

Most Drupal projects that have been running in production for a few years have site-specific quirks that must be addressed during local setup. A particular post-update hook might crash unless a table exists first. Config import might need to run twice because of a dependency ordering issue. A specific module might need to be uninstalled before the configuration can be imported cleanly.

In shell scripts, these are comments, if they are documented at all. In a Taskfile, they become named tasks:

drupal:update:
    deps: [setup:pre-update]
    desc: Run Drupal update tasks after deploying new code
    cmds:
      - ./vendor/bin/drush {{.site}} --yes cache:rebuild
      - ./vendor/bin/drush {{.site}} --yes updatedb --no-cache-clear
      - ./vendor/bin/drush {{.site}} --yes config:import || true
      - ./vendor/bin/drush {{.site}} --yes config:import
      - ./vendor/bin/drush {{.site}} --yes deploy:hook
setup:pre-update:
  desc: "Ensure DB prerequisites are met before running drupal:update"
  summary: |
    Creates the xmlsitemap table if it does not exist. The remote database
    backup may not include it, which causes the xmlsitemap post-update hook
    to crash with "table doesn't exist". Also marks the problematic
    xmlsitemap_engines hook as already run to avoid a null-haystack error.
  cmds:
    - |
      ./vendor/bin/drush sql:cli <<'SQL'
      CREATE TABLE IF NOT EXISTS xmlsitemap ( ... );
      SQL
    - ./vendor/bin/drush php-eval "..."

The knowledge that previously lived in someone’s head, or in a comment inside a 300-line script, is now a named, described, searchable task. The next person who hits the same issue can read why the workaround exists.

How to Approach the Migration

Before introducing new tools, verify the existing Lando setup actually works. If Lando has not been used in a few months, there may be broken dependencies or outdated scripts. Fix those first so you have a clean baseline: the ability to download the database, import it, run a config import, and log in.

It is also worth checking production logs for errors that are failing silently. The migration to DDEV itself can surface latent issues that were never triggered in your existing setup. For example, on our recent project, a missing xmlsitemap database table had gone unnoticed under Lando, but caused drupal:update to crash once we ran it against a fresh database import during the switch to DDEV.

Once the baseline is stable, get DDEV running alongside Lando rather than replacing it immediately. The goal at this stage is simply for DDEV to start and for the site to load. You are not yet migrating anything. If your Lando setup is complex, a rough DDEV equivalent of your existing setup script is a reasonable intermediate step. It does not have to be clean, because you will not keep it.

With DDEV stable, the remaining work is translating shell scripts into Taskfile tasks. The translation itself is usually straightforward. The more interesting part is deciding which site-specific behaviors are worth documenting explicitly and which were workarounds for problems that no longer exist.

Tradeoffs to Consider

Lando’s multi-service setup is more flexible without additional configuration. If your project runs multiple services (a separate Node container for a frontend build, a Redis container, a custom image), Lando’s service definitions are intuitive and well-documented. DDEV supports additional services via Docker Compose overrides, but the pattern requires more configuration.

Drainpipe is opinionated. It uses Task, and the task definitions it ships are written for a particular style of Drupal project. Teams with existing workflows built around make or npm scripts will need to decide how much of that to preserve and how much to replace.

Neither of these is a reason not to migrate. They are reasons to go in with accurate expectations about where the work actually is.

The Daily Workflow After Migration

Once DDEV is working, the daily workflow might look like this:

ddev start
ddev auth ssh
ddev task sync

To update an existing local without re-importing the database:

ddev task update

To lint and run code standards checks:

ddev task validate

The value is not that any individual command is better than the one it replaced. It is that the commands are consistent, documented, and work the same way for every developer on the team. When something breaks, the failure is in a named task with a description, not in a chain of sourced shell files that nobody fully remembers.

Read the original on matthewtift.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.