Skip to content

Your first project

Terminal window
mx router install && mx router up # once per machine
mx new my-app
cd my-app
mx add api --recipe rust-api --domain api.localhost
make doctor # check dependencies
make init # initialize environment
make dev # develop at http://api.localhost

That is the whole path. What follows is what each step actually does.

Creates the project skeleton (Makefile, make/, scripts/, apps/ and the docker/ tree) and copies the shared Docker config. Nothing is running yet; this is the folder contract on disk.

Two flags are worth knowing:

Terminal window
mx new my-app --no-prompt # skip the interactive prompts
mx new my-app --infra cloudflare # also install make/cloudflare.mk + infra/cloudflare/
mx new my-app --with api # add services during creation

--infra matters more than it looks: without it, the cf-* make targets simply are not in the project. See Cloudflare deploy.

mx add api --recipe rust-api --domain api.localhost

Section titled “mx add api --recipe rust-api --domain api.localhost”

Applies a recipe. mx recipes list shows what is installed and mx recipes info <name> shows a recipe’s options, features and the services it brings with it. mx add lands, for a typical recipe:

  • an app skeleton under apps/api/
  • docker/compose/api.yml and docker/compose/api.dev.yml
  • docker/dockerfiles/api/app and app.prod
  • docker/.config/.env.api
  • Traefik labels wired to the --domain you passed

--domain is what makes the service reachable at http://api.localhost rather than a port. Omit it and the recipe defaults to <service>.localhost. Extra recipe options go through --opt key=value. mx recipes info lists which ones a recipe accepts.

Recipes that also declare backing services (Postgres, Redis) drop their compose files in at the same time, so docker/compose/ grows db.yml and redis.yml on the first mx add that needs them.

Checks Docker, Compose and Make, then the project structure (Makefile, make/, scripts/, docker/compose/, docker/.config/, the secrets file and the Docker network) and lists the services it found. It exits non-zero if something is missing, so it is safe to put in front of a script.

Creates docker/.config/.env.secrets from .env.secrets.template if it does not exist yet, generates a real development value for every credential still empty or still a placeholder, resolves any ${VAR} references in the other env files into literals, and ensures the project network. Idempotent, so run it whenever you are not sure: a value that is already real, generated earlier or typed by you, is never overwritten.

That is what makes the quickstart above hold with no hand edits. mx new, mx add with a recipe that brings Postgres, then make dev, and the database comes up with credentials the application can actually authenticate with. make dev runs the same step on every start, so a service added later is covered too. There is nothing to fill in first.

REDIS_PASSWORD stays blank deliberately, because the shipped Redis runs without auth and a password there would break clients that build redis://:$REDIS_PASSWORD@redis:6379. make doctor knows that and does not flag it, while it does name any other key left without a value.

Starts the stack with the dev overrides merged in: source mounts, debug logging, relaxed health checks. Two shapes:

Terminal window
make dev # everything in docker/compose/
make dev s=api # just this service (and what it depends on)
make dev s="api site" # a named subset, quotes required

The quotes matter: make splits on whitespace, so s=api site would read site as a second goal. dev, up, down, stop, restart and logs accept a list. build, run, exec and sh act on one container or image and say so rather than quietly taking the first name.

make dev stops the existing services first, so it is also the “restart into a clean state” command. make up is the same thing without the dev overrides: the baseline compose files alone, which is what ships.

Then:

Terminal window
make logs s=api # tail it
make sh s=api # shell into it
make ps # what is running
make down # stop and remove

Full verb list: CLI reference.

api.localhost resolves to 127.0.0.1 in every current browser and in getaddrinfo on macOS and Linux. Port 80 is the router; the router reads the Host header, matches it against the Host() rule in your compose labels, and forwards to the container over the shared devmesh-traefik network. Your service publishes no HTTP host port at all, which is why two projects can both have a service listening on 3000 internally and never collide.