A standalone, stateful local emulator of the Fly.io Machines API (flaps). Like LocalStack, but for Fly Machines.
mudflaps is the local target for chant's Fly lexicon — its flyApply applier deploys against mudflaps for offline, accountless runs. See the chant docs and the Fly deploy tutorial it powers.
Testing a Machines-API client, such as an infrastructure-as-code applier, requires testing against state: a machine that moves through creating to started, a lease held by another caller, an update that mints a new instance_id and retires the prior version. A schema mock (Prism or WireMock pointed at the OpenAPI spec) validates request and response shapes but holds no state, so it cannot model these behaviors. mudflaps maintains apps, machines, leases, and version history in memory and advances machines through their lifecycle over short, deterministic delays, providing a stateful target for client test suites.
superfly/fly-go is the wire-shape reference; JSON field parity with it is the fidelity contract.
- Stateful in-memory store of apps and machines with full CRUD.
- Asynchronous lifecycle state machine on an injected clock: transient states (
creating,starting,stopping,restarting,suspending,replacing,destroying) settle into resting states. - Suspend/resume, cordon/uncordon, and machine metadata endpoints.
- Machine leases with nonce, TTL, owner, and expiry; a mutation by a non-holder returns
409, and a conflicting acquire returns the holder's lease envelope (without the nonce). - Synchronous version churn: an update mints a new
instance_id, marks the prior versionreplaced, and returns the new version in the response. - Deploy-failure injection: a machine whose
config.imageis the sentinelmudflaps/unpullablesettles intofailedinstead ofstarted, modeling a boot-time image-pull failure ("unable to pull image, canceling deploy"). A client that waits forstartedobserves a deploy that never comes up — its failure path, tested offline. /waitlong-poll that blocks until a target state or returns408; the deadline is measured on the injected clock, so timeouts are deterministic in tests.- A
/_mudflaps/healthendpoint that reports the implemented paths; the roadmap is currently empty — every documented endpoint mudflaps targets is built. - Single static binary and distroless container image; no runtime dependencies.
Run the container:
docker run --rm -p 4280:4280 ghcr.io/intentius/mudflaps:latestOr install with Go:
go install github.com/intentius/mudflaps/cmd/mudflaps@latest
mudflaps # listens on :4280 by defaultPoint a Machines-API client at it with the same environment variable the real client uses:
export FLY_FLAPS_BASE_URL=http://localhost:4280The listen address is configurable with -addr or MUDFLAPS_ADDR (default
:4280, matching Fly's internal _api.internal:4280).
BASE=http://localhost:4280
# Create an app.
curl -s -X POST "$BASE/v1/apps" \
-d '{"app_name":"demo","org_slug":"personal"}'
# Create a machine.
MID=$(curl -s -X POST "$BASE/v1/apps/demo/machines" \
-d '{"region":"local","config":{"image":"nginx"}}' | jq -r .id)
# Block until it reaches "started".
curl -s "$BASE/v1/apps/demo/machines/$MID/wait?state=started"
# => {"ok":true}
# Take a lease, then observe a mutation without the nonce being rejected.
curl -s -X POST "$BASE/v1/apps/demo/machines/$MID/lease" -d '{"ttl":30}'
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST "$BASE/v1/apps/demo/machines/$MID/stop"
# => 409| Capability | mudflaps | Prism / WireMock | Real Fly |
|---|---|---|---|
| State machine (create → started, stop, suspend, destroy) | Yes | No | Yes |
| Leases and nonce conflicts | Yes | No | Yes |
Version churn (new instance_id, old replaced) |
Yes | No | Yes |
| Machine metadata (ownership markers) | Yes | No | Yes |
| Runs fully offline | Yes | Yes | No |
| Cost | Free | Free | Billed |
| Real microVMs, images, networking | No | No | Yes |
Implemented: the apps and machines endpoints (start, stop, restart, suspend,
cordon/uncordon, metadata, signal, exec, ps), /wait, the lease endpoints, and
volumes, secrets (apply-only), IP assignments, and certificates. The roadmap is
empty — every documented endpoint mudflaps targets is built, so /_mudflaps/health
reports an empty unimplemented list. The full table is in the
API coverage docs.
mudflaps is the local target for the chant fly lexicon's flyApply applier.
Tracking epic: chant #736. The
Machines surface an IaC applier exercises is complete; new endpoints are staged
here as fly-go / the OpenAPI grows.
The primary task runner is just; a Makefile
mirrors the same targets.
just build # compile
just test # go test ./...
just race # go test -race ./...
just lint # golangci-lint
just cover # coverage profile
just docs-serve # preview the doc siteContributions are welcome. See CONTRIBUTING.md and the Code of Conduct.
Licensed under the MIT License. Copyright (c) 2026 Intentius.