# The router

URL: https://mechcrate.dev/docs/framework/router/
Source: site/apps/site/src/content/docs/docs/framework/router.mdx

---

import EcosystemTopology from '@/components/diagrams/EcosystemTopology.astro';

The mx router is a single Traefik container per workstation. Every mx project
attaches to the same Docker network and declares a `Host()` rule in its compose
labels; Traefik reads those labels off the Docker socket and routes by hostname.
Application services publish no HTTP host port at all, so two ecosystems can both
run a web app on container port 3000 and never collide.

<EcosystemTopology />

## Install once, per machine

```bash
mx router install    # copies the Traefik config to ~/.mech-crate/router, creates devmesh-traefik
mx router up         # start (or re-apply config to) the router
mx router status     # installed / running / network / dashboard URL
```

`mx router status` prints something like:

```
Router Status
────────────────────────────────────────
  Installed: installed
  Status: running
  Network: devmesh-traefik
  Dashboard: http://localhost:7680
  Location: /Users/you/.mech-crate/router
```

The rest of the verbs: `down`, `restart`, `logs`, `inspect`, `network`,
`uninstall`. Aliases exist for most (`start`/`up`, `stop`/`down`, `ps`/`status`,
`info`/`inspect`, `remove`/`uninstall`).

## Connecting a service

`mx add --domain <host>` writes these labels for you. This is what they look
like, and what you would write by hand for a service mx did not scaffold:

```yaml
services:
  myapp:
    networks:
      - default              # internal: db, redis, siblings
      - devmesh-traefik      # external: how Traefik reaches you
    labels:
      - traefik.enable=true
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}-myapp.rule=Host(`${MYAPP_ROUTER_HOST:-myapp.localhost}`)
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}-myapp.entrypoints=web
      - traefik.http.services.${COMPOSE_PROJECT_NAME}-myapp.loadbalancer.server.port=3000
      - traefik.docker.network=devmesh-traefik

networks:
  devmesh-traefik:
    external: true
```

Four things matter and are easy to get wrong:

1. **Both networks.** `devmesh-traefik` is how the router reaches the container;
   `default` is how the container reaches its database. Only the service that
   receives external traffic needs the first one. Postgres, Redis and workers
   stay on `default` only.
2. **`traefik.docker.network`.** A container on two networks has two addresses.
   Without this label Traefik may pick the wrong one and you get a 502.
3. **`loadbalancer.server.port` is the *container's* port**, not a host port.
   There is no host port.
4. **The router, service and middleware *names* carry the compose project.**
   Traefik keeps one table of each per machine, so a bare `myapp` is a name every
   mx project on the workstation shares. `${COMPOSE_PROJECT_NAME}-myapp` is
   resolved by Compose from the project name it already knows, so it needs nothing
   exported and works under a hand-rolled `docker compose -f …` too.

The routing rule is a Traefik matcher, so the usual composition works:
`Host(...) || Host(...)`, `Host(...) && PathPrefix(...)`, `HostRegexp(...)`.

## Two ecosystems at once

This is the point of the whole arrangement. Project A's `api.localhost` and
project B's `admin.localhost` are two containers on one shared network behind one
Traefik. Switching between them is `cd`: no teardown, no port bookkeeping, no
lost container state. Start the subset you need:

```bash
make dev s=api        # in project A
make dev s=admin      # in project B — both reachable, simultaneously
make dev s="api web"  # or a named subset, quotes required
```

Container names no longer stand in the way of this either. No shipped compose
file pins `container_name`, so each project's containers are named
`<COMPOSE_PROJECT_NAME>-<service>-<index>` and two projects that both declare a
`db` service coexist. See
[container names](/docs/framework/upgrade/#migrating-container-names) if you have
a script that addressed one by a fixed name.

Two copies of the *same* shape coexist as well, and that took two separate
repairs. Dev overrides no longer pin host ports, so the second stack's Postgres
does not find 5432 taken. Traefik router, service and middleware names now carry
the compose project, so two stacks that each scaffold a service called `api`
write two entries into Traefik's one router table instead of colliding in it. See
[host ports](/docs/framework/upgrade/#migrating-host-ports) for the port classes
and their override names.

:::note[What is still on you: the hostname]
Both stacks run. A *hostname*, though, belongs to one of them: two routers whose
rules name the same host are both accepted, each keeps its own backend, and
Traefik serves exactly one. Changing the default domain to embed the project name
would fix that and break every URL, bookmark and OAuth callback in every existing
project, so the default is untouched and the rule reads through the environment:

```yaml
- traefik.http.routers.${COMPOSE_PROJECT_NAME}-api.rule=Host(`${API_ROUTER_HOST:-api.localhost}`)
```

A lone stack behaves exactly as before. The second stack moves itself with an
exported variable rather than an edit to a shipped file:

```bash
API_ROUTER_HOST=api-two.localhost make dev
```

The variable is `<SERVICE_UPPER>_ROUTER_HOST` and not `<SERVICE>_HOST` because
`DB_HOST`, `REDIS_HOST` and `API_HOST` are conventional service-address variables
you may already export, and quietly repointing a router at a database host is not
a failure anyone would enjoy debugging.
:::

:::caution[Breaking change: label names carry the compose project]
Traefik keeps **one** router table, **one** service table and **one** middleware
table per provider for the whole machine, and the recipes used to name their
entries after the compose service alone. Two mx projects that each scaffolded an
`api` service therefore wrote the same key into that one table. When the labels
differed in any way Traefik dropped the router outright (`ERR Router defined
multiple times with different configurations`) and both hostnames 404'd; when
they agreed it was quieter and worse, merging both containers into one
load-balancer pool so consecutive requests alternated between two projects' apps
with nothing logged at all.

The name now carries the project:

```yaml
# before
- traefik.http.routers.api.rule=Host(`api.localhost`)
- traefik.http.services.api.loadbalancer.server.port=3000

# now
- traefik.http.routers.${COMPOSE_PROJECT_NAME}-api.rule=Host(`${API_ROUTER_HOST:-api.localhost}`)
- traefik.http.services.${COMPOSE_PROJECT_NAME}-api.loadbalancer.server.port=3000
```

Compose resolves `COMPOSE_PROJECT_NAME` from the project name it already
resolved, so this works even for a hand-rolled `docker compose -f …` that never
sources the project's `.bashrc`. Hostnames are unchanged. 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 router is the deliberate exception. `templates/router/` configures
Traefik by **file**, not by label, and the middlewares in
`config/dynamic/` are `@file` singletons on purpose, because one machine has one
router.
:::

## Tuning

| Variable | Default | Effect |
|---|---|---|
| `MX_ROUTER_HOME` | `~/.mech-crate/router` | State directory |
| `MX_ROUTER_NETWORK` | `devmesh-traefik` | Docker network name |
| `MX_ROUTER_DASHBOARD_PORT` | auto | Pin the dashboard port |
| `MX_ROUTER_DASHBOARD_RANGE` | `7680-7799` | Range the dashboard is allocated from |

## When a hostname does not answer

Work down this list; it is ordered by how often each one is the cause.

1. `mx router status`. Is the router even running?
2. `docker ps`. Is the service running, and is it healthy?
3. Is the service on `devmesh-traefik`? A container that is not on the network is
   invisible to Traefik no matter how good its labels are.
4. Open the dashboard (`mx router status` prints the URL) and look at Routers.
   A missing router means the labels did not parse; a router with no service
   behind it usually means `traefik.docker.network` is wrong or absent.
5. A 502 rather than a 404 means Traefik found the container and could not reach
   the port. Check `loadbalancer.server.port` against what the app binds.

## Going deeper

The full guide covers HTTPS with mkcert and Let's Encrypt, the dynamic file
provider, middlewares, custom routes, routing to non-Docker host services, load
balancing, and the complete troubleshooting matrix. It is published in the corpus:

**→ [MechCrate Router (full guide)](/docs/corpus/framework-guides/router/)**

:::note[One stale command in that guide]
The corpus guide documents `mx router reload`. That subcommand does not exist in
the current build. `mx router reload` exits with `unrecognized subcommand`. Use
`mx router up`, which re-applies configuration to a running router, or
`mx router restart`.
:::
