lando-to-docker
Skill0 starsChanged 26 days agoMITView on GitHub
Migrate a Drupal project's local dev environment from Lando to Docker Compose + a Makefile (Traefik reverse proxy, *.dev.localhost, PHP-FPM + Apache, MariaDB, Redis, Adminer, Mailpit). Use when the user mentions replacing Lando, a ".lando.yml", "lando to docker", "lando to make", setting up the Makefile / Docker dev environment, or needs the Makefile that the d11 / gulp-to-vite / sass-migrator / php-docker-upgrade skills require. Produces the docker/ tree, docker-compose.yml, Makefile, and Drupal glue; handles single-site vs multisite and Gulp vs Vite/Tailwind frontends. IMPORTANT: after migration all commands MUST go through Makefile targets, never Lando or the host.
- Reads credentials
In this repository17 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- d11Skill
- drupal-build-foundationsSkill
- drupal-build-intakeSkill
- drupal-buildSkill
- drupal-build-unitSkill
- drupal-rgaaSkill
- drupal-scaffoldSkill
- gulp-to-viteSkill
- jquery-4-migrationSkill
- phpcbfSkill
- phpcsSkill
- php-deprecations-auditSkill
- php-docker-upgradeSkill
- php-standardsSkill
- phpstanSkill
- sass-migratorSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 3 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 53
If the user has no shared Traefik, say so and point them at the reference before generating the stack (the stack assumes it exists).
- Very specific: line 58
Read .lando.yml (and .lando.local.yml if present.
- Distinctive: line 75
Before writing any file, resolve every image's tag with
- Distinctive: line 81
Write the resolved tags into the files below, and keep the report table for the MR.
- Very specific: line 85
Create the image/config files verbatim from references/docker-images.md: docker/php/{Dockerfile,entrypoint.sh,php.ini,www.conf,xdebug.ini}, …
- Very specific: line 112
Add the SITES loop + cr-all for multisite.
- Very specific: line 130
Write .env.example (see references/drupal-glue.md), then:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Lando → Docker Compose + Makefile (Drupal local dev)
This is the upstream prerequisite skill: it produces the Makefile (and the
Dockerized environment behind it) that d11, gulp-to-vite, sass-migrator
and php-docker-upgrade all depend on. The target architecture is the one used
across the reference projects (Project B, Project D, Project E):
Traefik (shared, external) ──► apache (httpd:2.4-alpine, proxy_fcgi)
*.dev.localhost, TLS └─► php (php:8.4-fpm)
├─ db (mariadb:11.8)
├─ redis (redis:8-alpine)
├─ node (node:24-alpine, idle)
├─ adminer (adminer:6.1.2)
└─ mailpit (axllent/mailpit)The tags above are a baseline, not a target: Step 1 resolves the current ones
(references/image-versions.md).
Golden rule
The Makefile is the single developer entrypoint — after this migration,
every drush/composer/npm runs through a make target inside a container,
never via lando and never on the host. Building that Makefile faithfully is
the core deliverable.
Prerequisites (host, once per machine)
A shared external Traefik and a local wildcard cert — see
references/compose-traefik.md:
docker network create traefik-public
mkcert -install && mkcert "*.dev.localhost" # feed cert to the Traefik instance*.dev.localhost resolves to 127.0.0.1 automatically — no /etc/hosts edits.
If the user has no shared Traefik, say so and point them at the reference before
generating the stack (the stack assumes it exists).
Step 0 — Inventory the .lando.yml and decide the variants
Read .lando.yml (and .lando.local.yml if present; recover from git history
if already deleted). Extract: recipe, PHP version, web server, DB type+version,
node version, extra services (redis, search, cache), the tooling: commands,
and the proxy: hostnames. Then settle the three decisions that shape the
output:
| Decision | Detect | Effect |
| Single-site vs multisite | multiple proxy hostnames / per-site DB services / web/sites/sites.php | multisite adds sites.php, per-site init SQL + DB network aliases, per-site env prefix, SITES loop → references/drupal-glue.md |
| Frontend build | gulpfile.js vs vite.config.* vs Tailwind (@tailwindcss/*) | node image toolchain + HMR wiring → references/frontend-build.md |
| Extra services | elasticsearch, other | usually dropped (projects use search_api_db); only keep ES if a search_api_elasticsearch backend is really configured |
Capture the target
COMPOSE_PROJECT_NAME(the base hostname) and the theme folder now — they thread through every file.
Step 1 — Resolve the image versions, then create the docker/ tree
Before writing any file, resolve every image's tag with
references/image-versions.md:
- dev-only tools (Adminer, Mailpit, httpd, Redis) take their newest release;
- PHP, Node and the database take the newest LTS / supported version that production and the CI image also run.
Write the resolved tags into the files below, and keep the report table for the MR.
Create the image/config files verbatim from references/docker-images.md:
docker/php/{Dockerfile,entrypoint.sh,php.ini,www.conf,xdebug.ini},
docker/apache/{Dockerfile,vhost.conf}, and .dockerignore. The node
service uses the node:<LTS>-alpine image directly; write
docker/node/Dockerfile only if imagemin/optipng is a dependency
(toolchain form) — a FROM + WORKDIR-only Dockerfile is dead weight.
Do not skip
www.conf(clear_env = no) — it is load-bearing: without it the env-driven DB/Redis/SMTP config insettings.phpis silently empty. Which of$_ENV/getenv()actually holds the values also depends on the project's.envloader — see "Which source holds the.envvalues" in drupal-glue.md before writing any env-driven condition.
Step 2 — Write docker/compose/dev/docker-compose.yml
From references/compose-traefik.md: all services, the Traefik label
pattern (web + websecure routers, tls=true), the app-internal bridge and the
traefik-public external network, named volumes. For multisite, apply the
apache OR-rule and the per-site db network aliases.
Step 3 — Write the Makefile
From references/makefile.md: the header wiring, the dual argument-passing
convention (named c=/m=/uid= and positional with the catch-all %:
rule), and every target. Verify coverage: each lando <cmd> from the old
tooling: block must map to a target (drush→make drush, composer→
make composer, npm/node/yarn→make npm, gulp→make npm-dev/npm-build).
Add the SITES loop + cr-all for multisite.
Step 4 — Drupal glue
From references/drupal-glue.md: point the DB host at the compose service
(db) or network alias, read creds from $_ENV, set config_sync_directory,
trusted_host_patterns, the reverse_proxy block (Traefik terminates TLS),
and the empty-password Redis guard. Update drush aliases (/app →
/var/www/html, *.lndo.site → https://*.dev.localhost). For multisite, add
web/sites/sites.php, the per-site init SQL, and the per-site $_ENV prefix
logic.
web/sites/*/settings.phpis often git-ignored (a local file) — edit wherever the project actually reads DB config (a trackedbase.settings.php, asettings.local.php, or the developer's localsettings.php).
Step 5 — .env.example + bring it up
Write .env.example (see references/drupal-glue.md), then:
cp .env.example .env # or: make init (generates HASH_SALT, builds, boots)
# set USER_ID / GROUP_ID from `id -u` / `id -g`
make init # first run: .env, HASH_SALT, build, up
make db-import _dumps/<dump>.sql.gz
make cim
make crStep 6 — Verify
make up # prints the URLs
make drush c='status' # DB connected, bootstrap OK, shows PHP version
curl -ksI https://<project>.dev.localhost | head -1 # 200/301 via Traefik
make npm-build # frontend builds in the node container
# Failure paths — each one was a review finding once, test them, don't assume
make updv; echo $? # mistyped target → non-zero, not a silent 0
make db-import _dumps/nope.sql.gz # → "file not found", DB still intact
grep HASH_SALT .env; make init; grep HASH_SALT .env # identical: init is re-runnable
XDEBUG_MODE=debug docker compose … run --rm --no-deps --entrypoint "" php php -m | grep -i xdebugManual checks:
https://<project>.dev.localhostloads (TLS via Traefik, no cert warning)- Adminer at
https://<project>-adminer.dev.localhost, Mailpit at-mailpit - After importing a preprod dump, a real send (webform, password reset) lands in Mailpit — the transport override is in place (drupal-glue.md)
- No service publishes a host port that Traefik already routes
make uli uid=1logs in- Edits to files on the host are picked up (bind mount + UID/GID remap OK)
make npm-devHMR works (Vite/Gulp) if applicable- (multisite) each site resolves and reaches its own DB
- Every image is on the version the image-versions rule picks, and the report table lists each newer version held back, with the reason
Hand-off to the other skills
Once make drush c='status' is green, the Makefile contract the other skills
rely on exists. Natural next steps: php-docker-upgrade (bump PHP),
d11 (Drupal 10 → 11), gulp-to-vite, sass-migrator.
Git workflow
One focused branch + a single MR is enough (this is one coherent change). If the
gm:merge-request skill is available, delegate the branch/commit/MR to it;
otherwise do a plain branch → commit → push (open the MR with glab if
authenticated), or commit on the current branch — ask the user which. Keep the
old .lando.yml deletion in the same commit as the new docker/ + Makefile.
Common issues
| Symptom | Cause | Fix |
| An image is a year behind | tags copied from the references | Resolve them (image-versions.md) |
make up fails / nothing routes | traefik-public network or Traefik instance missing | Create the network + run Traefik (Prerequisites) |
Cert warning on *.dev.localhost | mkcert root not installed / cert not loaded into Traefik | mkcert -install; feed *.dev.localhost cert to Traefik |
Site can't connect to DB, $_ENV empty | clear_env = no missing from www.conf | Add it, make rebuild |
getenv('APP_ENV') is FALSE while $_ENV['APP_ENV'] is set | vlucas/phpdotenv populates only the superglobals | Read $_ENV, or move load.environment.php to symfony/dotenv + usePutenv(TRUE) (drupal-glue.md) |
| Dev-only settings apply on prod/CI | an $_ENV['APP_ENV'] ?? 'local' fallback — fail-open | Strict === 'local', no fallback (drupal-glue.md) |
Redis ERR AUTH ... without any password | empty-string password sent as a real password | !empty() guard → NULL (see drupal-glue.md) |
| Redirect loops / wrong-scheme URLs | reverse_proxy not set; Traefik TLS not trusted | Add the reverse_proxy block to settings |
| Root-owned files on host / permission errors | USER_ID/GROUP_ID not set to host values | Set from id -u/id -g, make rebuild |
npm install fails on Apple Silicon | imagemin-optipng native build | Toolchain node Dockerfile form (docker-images.md) |
| Vite HMR dead / mixed-content | wss/clientPort/CSP/library scheme mismatch | HMR checklist in frontend-build.md |
php -m has no xdebug whatever XDEBUG_MODE says | xdebug.ini mounted over docker-php-ext-xdebug.ini | Mount as zz-xdebug.ini (docker-images.md) |
| Xdebug never connects on Linux | host.docker.internal unresolved | extra_hosts: host-gateway on php |
| Local webform emails real people | transport lives in the imported DB | Override every transport in local settings (drupal-glue.md) |
make db-import wiped the DB then failed | guard tested -n instead of -f | test -f "$(FILE)" before sql-drop |
| Multisite init SQL didn't run | init SQL only runs on an empty volume | make destroy then make init |
updb/cr order errors after a later core bump | unrelated — that's the d11 skill | see d11 |
Reference files
references/image-versions.md— resolving each image's tag + the report tablereferences/docker-images.md— verbatimdocker/Dockerfiles + configreferences/compose-traefik.md—docker-compose.yml, Traefik labels, mkcertreferences/makefile.md— full Makefile template + argument passingreferences/drupal-glue.md— settings/sites/drush/.env, single vs multisitereferences/frontend-build.md— node image toolchain + Gulp/Vite/Tailwind HMR
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

