You have fifteen services. Today you're touching two.
If your stack is more than a couple of services, you know this moment. You want to run web and api locally to work on a feature - but api talks to auth, billing, a queue, a search cluster, and three other internal services. So what are your options?
-
Run the whole thing in Docker Compose. Now you're maintaining a
docker-compose.ymlthat mirrors production (and drifts from it), booting thirteen containers you don't care about, feeding a daemon and disk, and waiting on rebuilds - all to change two services. -
Point your two local services at staging by hand. Edit env files, swap URLs to
localhostfor the one peer you're running, remember to change them back. Every time.
Both are chores. I wanted a third option, so I built crew.
Every service is a switch
The whole idea fits in one line: on = local, off = remote.
- Flip a service on → crew runs it natively and rewrites every peer that talks to it to point at your
localhost. - Leave it off → it stays on its real deployed environment (qa / staging / prod), and its callers keep hitting the real host.
The "slice" you run locally is just which switches are on - one service, or the whole stack, or anything in between. And flipping a switch flips the wiring for you, read straight from the .env files you already ship.
crew start env=staging web [x] on → http://localhost:3000
api [x] on → http://localhost:4000 (web now calls this)
auth [ ] off → https://auth.staging.acme.dev
… [ ] off → deployed staging
Here's crew start in practice - pick a connected slice from the dependency graph, run it, watch the labelled logs:
How the wiring works
crew doesn't ask you to declare a topology. It derives the dependency graph from your env files: each project gets a match (the host(s) it's deployed under, per env) and a local (its localhost URL). When a URL in one project's env matches another project's match host, that's an edge.
At crew start, for each service you switched on, crew reads its env file, rewrites the URLs of any other on-services to their local value, writes the result to a throwaway file, and hands that path to your start command via {envfile}. Services you left off keep their real URLs. Nothing in your repo is touched - the wired copy lives in ~/.config/crew/tmp/ and is deleted on teardown.
Because the env each service resolves to is derived from the graph (not a static setting), the same shared config serves multiple teams correctly - the frontend team and the SDK team each get the right remote env for the services they didn't run.
You don't hand-write the config
crew config is a visual editor: pick a project's folder and crew auto-fills the mechanical parts it can read from your package.json, lockfiles and .envs - type, runner, start command, env-file path. You fill in the URLs (local port + deployed host per env). Out comes one readable, committable config.json with no secrets in it.
{
"projects": {
"web": {
"path": "web",
"type": "frontend",
"tasks": { "start": "dotenv -e {envfile} -- npm run dev" },
"env": ".envs/{env}.env",
"match": { "staging": "web.staging.acme.dev", "prod": "web.acme.dev" },
"local": "http://localhost:3000"
},
"api": {
"path": "api",
"tasks": { "start": "uvicorn app:main --reload --env-file {envfile}" },
"env": ".envs/{env}.env",
"match": { "staging": "api.staging.acme.dev", "prod": "api.acme.dev" },
"local": "http://localhost:4000"
}
}
}
The same slice, three ways
The picked set is remembered, so the other two commands open exactly what you started:
| Command | What it opens |
|---|---|
crew start env=staging |
the dev servers (streamed, per-project-colored logs; Ctrl-C tears the whole group down) |
crew workspace |
one multi-root VS Code window with every picked repo side by side |
crew claude |
one Claude Code session over the set, history kept per group of repos |
Why not just Docker Compose?
crew runs your slice natively and borrows the rest from their deployed envs - a different category from the usual tools:
| crew | Compose | Tilt | mirrord | overmind | |
|---|---|---|---|---|---|
| Runs your slice natively | ✅ | ❌ | ❌ | ✅ | ✅ |
| Rest of stack = real remote | ✅ | ❌ | ❌ | ✅ | ❌ |
| Wiring from |
.env files |
compose file | k8s manifests | cluster intercept | - |
| Infra required | none | daemon | k8s | k8s | none |
crew sits between plain process-runners (overmind, foreman, mprocs) and remote-wiring tools (mirrord, Telepresence): the native-slice-plus-real-remote of the latter, without the cluster, daemon, or containers - just URL swaps in the env files you already have.
When Docker is the right call: you need full-stack isolation, byte-for-byte prod/CI parity, or your services can't run natively. crew doesn't replace that. It replaces the daily loop of flipping a couple of services on and letting everything else stay remote.
The honest caveats
- You need shared remote environments to borrow from. If you don't have a staging/qa the "off" services can run on, a full-stack tool fits better.
- crew doesn't proxy. If staging is down, your off-services are unreachable - same as calling staging directly. Turn them on to run locally.
- POSIX only (macOS + Linux) - teardown relies on process groups so reparented dev-server children actually die.
- No task graph, no ordering, no caching. crew fans out one task at a time; that's make/turbo/nx territory, on purpose.
Zero dependencies, by design
crew is one file of Node built-ins - no runtime dependencies, no build step, no bundler. The source is what runs, and its own parallel process runner handles the fan-out and teardown. So it installs instantly:
npx @pinkynrg/crew # or: npm i -g @pinkynrg/crew
Try it
The site has a live version of that on/off switchboard: flip services on and off and watch the URLs and wiring rewire as you go. Source, docs, and the full config reference are on GitHub.
I built crew for my own stack. If you've solved the run-a-slice-locally problem a different way, I'm curious which trade-offs you landed on.


Top comments (0)