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:update, drupal:import-db, drupal: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/customEvery 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-importA 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 syncTo update an existing local without re-importing the database:
ddev task updateTo lint and run code standards checks:
ddev task validateThe 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.