agentleFS
JMAILLYJMAILLY/claude-migration-skills/skills/lando-to-docker/SKILL.md

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

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
  1. Distinctive:

    If the user has no shared Traefik, say so and point them at the reference before generating the stack (the stack assumes it exists).

    line 53
  2. Very specific:

    Read .lando.yml (and .lando.local.yml if present.

    line 58
  3. Distinctive:

    Before writing any file, resolve every image's tag with

    line 75
  4. Distinctive:

    Write the resolved tags into the files below, and keep the report table for the MR.

    line 81
  5. Very specific:

    Create the image/config files verbatim from references/docker-images.md: docker/php/{Dockerfile,entrypoint.sh,php.ini,www.conf,xdebug.ini}, …

    line 85
  6. Very specific:

    Add the SITES loop + cr-all for multisite.

    line 112
  7. Very specific:

    Write .env.example (see references/drupal-glue.md), then:

    line 130

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

DecisionDetectEffect
Single-site vs multisitemultiple proxy hostnames / per-site DB services / web/sites/sites.phpmultisite adds sites.php, per-site init SQL + DB network aliases, per-site env prefix, SITES loop → references/drupal-glue.md
Frontend buildgulpfile.js vs vite.config.* vs Tailwind (@tailwindcss/*)node image toolchain + HMR wiring → references/frontend-build.md
Extra serviceselasticsearch, otherusually 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 in settings.php is silently empty. Which of $_ENV / getenv() actually holds the values also depends on the project's .env loader — see "Which source holds the .env values" 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.php is often git-ignored (a local file) — edit wherever the project actually reads DB config (a tracked base.settings.php, a settings.local.php, or the developer's local settings.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 cr

Step 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 xdebug

Manual checks:

  • https://<project>.dev.localhost loads (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=1 logs in
  • Edits to files on the host are picked up (bind mount + UID/GID remap OK)
  • make npm-dev HMR 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

SymptomCauseFix
An image is a year behindtags copied from the referencesResolve them (image-versions.md)
make up fails / nothing routestraefik-public network or Traefik instance missingCreate the network + run Traefik (Prerequisites)
Cert warning on *.dev.localhostmkcert root not installed / cert not loaded into Traefikmkcert -install; feed *.dev.localhost cert to Traefik
Site can't connect to DB, $_ENV emptyclear_env = no missing from www.confAdd it, make rebuild
getenv('APP_ENV') is FALSE while $_ENV['APP_ENV'] is setvlucas/phpdotenv populates only the superglobalsRead $_ENV, or move load.environment.php to symfony/dotenv + usePutenv(TRUE) (drupal-glue.md)
Dev-only settings apply on prod/CIan $_ENV['APP_ENV'] ?? 'local' fallback — fail-openStrict === 'local', no fallback (drupal-glue.md)
Redis ERR AUTH ... without any passwordempty-string password sent as a real password!empty() guard → NULL (see drupal-glue.md)
Redirect loops / wrong-scheme URLsreverse_proxy not set; Traefik TLS not trustedAdd the reverse_proxy block to settings
Root-owned files on host / permission errorsUSER_ID/GROUP_ID not set to host valuesSet from id -u/id -g, make rebuild
npm install fails on Apple Siliconimagemin-optipng native buildToolchain node Dockerfile form (docker-images.md)
Vite HMR dead / mixed-contentwss/clientPort/CSP/library scheme mismatchHMR checklist in frontend-build.md
php -m has no xdebug whatever XDEBUG_MODE saysxdebug.ini mounted over docker-php-ext-xdebug.iniMount as zz-xdebug.ini (docker-images.md)
Xdebug never connects on Linuxhost.docker.internal unresolvedextra_hosts: host-gateway on php
Local webform emails real peopletransport lives in the imported DBOverride every transport in local settings (drupal-glue.md)
make db-import wiped the DB then failedguard tested -n instead of -ftest -f "$(FILE)" before sql-drop
Multisite init SQL didn't runinit SQL only runs on an empty volumemake destroy then make init
updb/cr order errors after a later core bumpunrelated — that's the d11 skillsee d11

Reference files

  • references/image-versions.md — resolving each image's tag + the report table
  • references/docker-images.md — verbatim docker/ Dockerfiles + config
  • references/compose-traefik.md — docker-compose.yml, Traefik labels, mkcert
  • references/makefile.md — full Makefile template + argument passing
  • references/drupal-glue.md — settings/sites/drush/.env, single vs multisite
  • references/frontend-build.md — node image toolchain + Gulp/Vite/Tailwind HMR

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.