Upgrade
The premise of the folder contract is that mx owns the verb layer, so
improvements to make dev can reach a project scaffolded a year ago. mx upgrade is the command that carries them.
mx upgrade --dry-run # show what would be done, change nothingmx upgrade --diff # show a diff for each changed filemx upgrade # walk the changes interactivelymx upgrade --yes # accept everything, non-interactiveWhat a run looks like
Section titled βWhat a run looks likeβDiscovery reads the layout that actually ships: templates/Makefile.template,
templates/make/*.mk, the top-level files of templates/scripts/ and
templates/docker/. A project scaffolded before the compose isolation change
below sees exactly the ten script files that changed:
$ mx upgrade --dry-run
π¦ MechCrate Project Upgrade
β Upgrading project at: /path/to/legacyapp
Checking directories...
Scanning templates...
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ π Tooling Updates Available βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The following tooling files have updates available:
β’ scripts/.bashrc β’ scripts/doctor.sh β’ scripts/down.sh β’ scripts/exec.sh β’ scripts/logs.sh β’ scripts/ps.sh β’ scripts/run.sh β’ scripts/sh.sh β’ scripts/stop.sh β’ scripts/test.sh
[DRY RUN] Would prompt to update 10 file(s)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ π Upgrade Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[DRY RUN] No changes were made
π¦ Crate Raccoon says: Your tooling is fresh!Exit code 0. mx upgrade --yes then applies that plan and keeps a .bak
beside every file it replaces, and the run after it reports β Project is up to date!. The command is safe to repeat: it compares content, so a project already
current is a no-op rather than a second round of prompts.
The categories
Section titled βThe categoriesβWhich files an upgrade may touch follows the same rule that governs who owns what in the folder contract. Every template file is categorised:
| Category | Paths | Behaviour |
|---|---|---|
| Tooling | Makefile, make/*.mk, every top-level file of scripts/ (including the extensionless scripts/.bashrc) | Offered for update (these are mxβs) |
| Config | docker/compose/, docker/config/, docker/dockerfiles/, docker/system/ | Added if missing, never overwritten (these are yours) |
| Conditional | make/cloudflare.mk, scripts/cf-*.sh, infra/cloudflare/ | Only touched if the project has infra/cloudflare/ |
| Skip | recipes/, router/, nested script bundles such as scripts/md2pdf/ | Not part of a project upgrade |
The line that matters: your compose files and dockerfiles are never replaced.
An upgrade can improve how make dev composes them; it cannot rewrite what they
say. That is what makes accepting an upgrade a low-stakes decision rather than a
merge conflict.
Conditional files are keyed on evidence rather than a config flag. The
Cloudflare set is in scope exactly when infra/cloudflare/ exists on disk, so a
project that never opted in never sees those files offered.
Skip is a scope rule, not an oversight. mx new copies the top-level files of
templates/scripts/ and nothing deeper, so an upgrade that laid down
scripts/md2pdf/ would be handing an existing project a subtree a fresh project
never gets. Upgradeβs reach and mx newβs reach are the same set, and a test
holds them there.
Migrating: the compose project name
Section titled βMigrating: the compose project nameβOne upgrade in this set changes behaviour you can feel, so it is worth reading before you accept it.
Every mx project now pins COMPOSE_PROJECT_NAME to its sanitised
project-directory name. scripts/.bashrc is the single source for that value,
and every script that shells out to compose passes -p "$COMPOSE_PROJECT_NAME".
Two mx projects on one machine therefore no longer share a namespace, and cannot
adopt or recreate each otherβs containers.
The migration cost is that a projectβs namespace changes. Containers started
before the upgrade ran under the default name compose derives from the compose
filesβ parent directory, which is the string compose for every mx project on
the machine. After upgrading, make down and make ps look in the new namespace
and no longer see them. They are orphans, still running, invisible to the
projectβs own verbs.
make doctor is where you find them:
βΉ Checking compose project name...β Compose project name: legacyappβ No orphans left under the old 'compose' project nameβΉ Another stack is using the shared default project name 'compose': - other-db (service 'db', from /path/to/another-project/docker/compose)βΉ Left alone - pinning 'legacyapp' is what keeps this project out of it.When there is something of yours to clean up, the same check names each container
and tells you to remove them once with docker rm -f <name>, then run make dev
again. Ownership is settled by composeβs own project.working_dir label, which is
how a colleagueβs stack sitting in the shared namespace gets reported and left
alone rather than offered up for deletion.
Two limits are worth knowing. A docker compose -f docker/compose/api.yml ...
typed by hand, outside make and the scripts, still gets the default name, so
pass -p yourself if you work that way. And two project directories that happen
to share a directory name still derive the same project name, so either name them
distinctly or export COMPOSE_PROJECT_NAME in your own environment, which wins
over the default.
Migrating: container names
Section titled βMigrating: container namesβA second behavioural change rides in the same set, and it is the one most likely to break something you wrote.
The shipped compose files used to pin container_name: db, container_name: api
and so on. With project names pinned per project, that became a hard blocker
rather than a cosmetic choice: container names are a Docker-daemon-wide
namespace, so two projects whose compose files both declared db could not run
at the same time. The second one failed with
Conflict. The container name "/db" is already in use. Those pins are gone from
every template and every recipe, and a conformance test keeps them gone.
Compose now derives the name itself:
<COMPOSE_PROJECT_NAME>-<service>-<index> e.g. myproj-db-1Service names did not change. depends_on, the include: graph, the Traefik
labels and every s=<service> on the make line address services, so all of
that behaves exactly as before. Upgrading does not change a single compose file
of yours either, because compose files are yours and an upgrade never rewrites
them. What changes is what you get after you adopt a newer template or apply a
newer recipe, and what breaks meanwhile is anything that addressed a container
by a fixed name:
# beforedocker exec db psql -U postgresdocker logs -f api
# aftermake exec s=db c=psqlmake logs s=apidocker compose -p "$COMPOSE_PROJECT_NAME" exec db psql -U postgresdocker compose -p "$COMPOSE_PROJECT_NAME" logs -f apiThe projectβs own verbs are the stable interface, and they were always the
intended one. Reach for the explicit docker compose -p form when you need
something the verbs do not cover.
One exception: the global router container is still mx-router. It is installed
once per machine rather than per project, so it has nothing to collide with, and
mx router addresses it by that name.
Migrating: host ports
Section titled βMigrating: host portsβEvery dev override used to publish a fixed host port, which is why a second stack
died on Bind for 0.0.0.0:5432 failed: port is already allocated with nothing in
the project to edit short of hand-patching a shipped template. The repair asked
one question per port: who dials it?
Ports that tooling dials on demand now publish on host port 0, which Docker
allocates from the free range, so they can never collide:
| Container port | Was | Pin it with |
|---|---|---|
| Postgres 5432 | 5432:5432 | DB_HOST_PORT |
| Redis 6379 | 6379:6379 | REDIS_HOST_PORT |
| Node inspector 9229 | 9229:9229 | NODE_DEBUG_HOST_PORT |
| Worker metrics 9090 | 9090:9090 | METRICS_HOST_PORT |
| Vite 5173 (laravel) | 5173:5173 | VITE_HOST_PORT |
| Inertia SSR 13714 | 13714:13714 | INERTIA_SSR_HOST_PORT |
Discover the allocated one instead of guessing:
docker compose -p "$COMPOSE_PROJECT_NAME" port db 5432Or pin one for a session, leaving its siblings ephemeral:
DB_HOST_PORT=5432 make devPorts a browser dials from a URL it already holds keep todayβs number as a
default and gain an override, so an overriding second stack moves instead of
failing to boot: LEPTOS_RELOAD_HOST_PORT (3001), ZOLA_LIVERELOAD_PORT (1024),
NGINX_HTTP_PORT / NGINX_HTTPS_PORT, TRAEFIK_HTTP_PORT /
TRAEFIK_HTTPS_PORT / TRAEFIK_DASHBOARD_PORT, and the global routerβs own
MX_ROUTER_HTTP_PORT / MX_ROUTER_HTTPS_PORT.
What breaks is a script or GUI connection profile pointed at localhost:5432,
localhost:6379, localhost:9229, localhost:9090, localhost:5173,
localhost:13714 or localhost:24678 of an mx dev stack. Discover the port or
pin it.
Migrating: Traefik label names
Section titled βMigrating: Traefik label namesβtraefik.http.routers.<service>.* is now
traefik.http.routers.${COMPOSE_PROJECT_NAME}-<service>.*, and the same for
.services. and .middlewares..
Traefik keeps one router table, one service table and one middleware table per
provider for the whole machine, so the old unqualified names meant two projects
that each scaffolded an api service wrote the same key. Mismatched labels made
Traefik drop the router outright and 404 both hostnames; matching labels were
quieter and worse, merging both containers into one load-balancer pool so
consecutive requests alternated between two projectsβ apps with nothing logged.
Compose resolves COMPOSE_PROJECT_NAME from the project name it already
resolved, so nothing has to be exported and the qualification holds under a
hand-rolled docker compose -f β¦. Hostnames are unchanged, and the routing
rule gained a per-service override in the default position:
- traefik.http.routers.${COMPOSE_PROJECT_NAME}-api.rule=Host(`${API_ROUTER_HOST:-api.localhost}`)What breaks is a script or dashboard bookmark that addressed a router or service
entry by its bare service name. api@docker is now myproj-api@docker. The
global mx-router container is unaffected: it configures Traefik by file rather
than by label, and one machine has one router.
Recipes are upgraded separately
Section titled βRecipes are upgraded separatelyβmx upgrade covers the project skeleton. Service-level scaffolding comes from
recipes and has its own path: mx recipes apply <name> re-applies a recipe to
the current project, and mx recipes versions <name> lists what is available.
See Recipes.