~/docs/essentials/local-development
╭─ § 02.02 ─╮

Local Development

└───────────╯

Corral manages dev server processes for your projects. It handles port allocation, ready detection, crash recovery, and graceful shutdown — so you can focus on writing code.

Starting and Stopping Projects

start, stop, restart and status all take the same selector — a name, a subdomain, kind:name, a uuid, or an unambiguous id prefix — and all four accept a project, a service or a stack. See the CLI reference for the full grammar.

corral start waits for the dev server and tells you what it actually did, exiting non-zero if the server never came up. Pass --no-wait to go back to returning the moment the process is spawned (projects only).

Start a single project:

corral start my-worker

Stop it:

corral stop my-worker

Restart it (picks up wrangler config changes):

corral restart my-worker

Recover a failed projectcorral start is also the retry path. It drives the project to running from whatever state it is in, so once you have fixed the underlying issue there is nothing else to run:

corral start my-worker

Use restart for a running project that needs to pick up config changes. For infrastructure, corral infra retry does the same job across the helper, DNS and proxy.

Start everything — infrastructure plus all projects flagged for auto-start:

corral up

Stop everything:

corral down

Check what’s running:

corral status

This prints one roster of every registered unit — projects, services, stacks and infrastructure — with NAME, KIND, STATE, HEALTH and LAST FAILURE (plus an ID column only when two rows share a name). Pass a selector to get a detail view of one unit instead; if the selector matches more than one, every match is reported rather than one being picked for you.

Network Access

By default, dev servers bind to 127.0.0.1 (localhost only). To make a project accessible from other devices on your local network — like a phone for mobile testing — use the --host flag:

corral start my-worker --host
# ok Started my-worker
#    network http://192.168.1.100:8787

This binds the dev server to 0.0.0.0 instead of 127.0.0.1, making it reachable at your machine’s LAN IP address. The .test domain and HTTPS continue working on your Mac as usual — they just won’t resolve on other devices.

Network access is ephemeral: it applies only to the current run and resets when the project stops. To restart a running project with network access:

corral restart my-worker --host

In the macOS app, use the ellipsis menu (three-dot button) next to the play/stop button in the toolbar. It offers “Serve on Network” to start or restart with network access, and “Disable Network Access” to switch back.

Sharing to the Internet

To make a project accessible from anywhere — not just your local network — use tunnel sharing:

corral share my-worker
# ok Sharing my-worker
#    public https://random-words.trycloudflare.com

This creates a public HTTPS URL via a Cloudflare Quick Tunnel. Anyone with the link can reach your dev server. Useful for:

  • Showing work to a teammate or client
  • Testing webhooks from external services (Stripe, Twilio, etc.)
  • Cross-device testing when the device isn’t on your network

The URL is ephemeral — it changes when the project restarts or you stop and re-share. To stop sharing:

corral share my-worker --stop

If the project isn’t running when you share it, Corral starts it first automatically.

In the macOS app, use the ellipsis menu (three-dot button) in the toolbar. “Share to Internet…” starts the tunnel; “Stop Sharing” tears it down. The shared URL appears in the project’s overview with a copy button.

First-time use: Corral downloads the cloudflared binary (~30 MB) on your first share. Subsequent shares start instantly.

No Cloudflare account is needed. Sharing is free and has no bandwidth limits for development use.

Port Allocation

Corral automatically assigns a port to each project from a configurable range. The defaults are:

  • Start port: 8787
  • Pool size: 100 ports (8787–8886)

Before allocating a port, Corral checks that it’s not already in use by another process. Ports are released when a project stops.

You can change the start port in Configuration:

corral settings set port-range-start 9000

Auto-Start

Flag projects that should start automatically when you run corral up:

corral project set my-worker auto-start true

When auto-start is enabled in app settings, corral up (or launching the macOS app) will start infrastructure and all flagged projects.

Process Lifecycle

All project processes are managed by a background daemon. This means both the CLI and the macOS app see the same running state — a project started via corral start is immediately visible in the app’s UI, and vice versa.

Projects, services, and infrastructure components (DNS, proxy, helper) are all managed as units — discrete managed entities with their own lifecycle and health state. A unit’s lifecycle can be:

  • Pending — registered but not yet started. Pending carries a reason, and one of them is not an error at all: a unit waiting on your approval (the system helper, most often) reports needs setup rather than a failure. It is never red and never cascades
  • Active — running (with phases: Starting, Initializing, Ready, Reloading, Stopping)
  • Idle — stopped intentionally
  • Failed — encountered an error (with structured failure details)
  • Abandoned — stopped retrying after repeated failures

When Corral starts a project, it transitions through these active phases:

  1. Starting — port allocation and dev command resolution
  2. Initializing — spawning the process with the correct Node.js version and port
  3. Ready detection — watching process output for ready signals and performing HTTP health checks
  4. Ready — the project is serving requests
  5. Stopping (when requested) — sends graceful termination signal, waits before forcing a stop

Units also track health independent of lifecycle. A project can be Active/Ready but Degraded (e.g., a linked service is down) or Critical (e.g., DNS infrastructure failed). Health signals surface in the UI as warning banners and can stack — multiple failed services each add their own signal.

Dev Command Resolution

Corral determines what command to run in this order:

  1. Custom dev command — if you’ve set one via corral project set <project> dev-command "...", it’s used
  2. package.json dev script — if the project has a "dev" script in package.json, Corral runs it via the project’s resolved package manager (e.g., npm run dev, pnpm dev, yarn dev)
  3. Framework default — if neither of the above applies, Corral uses the framework’s built-in command (e.g., wrangler dev, next dev, vite)

Where the tools come from

Corral does not inherit its own environment when it runs your dev command. It reads your login shell once (<shell> -i -l, taken from your account’s passwd entry rather than $SHELL) and composes each child’s environment from that.

This is why a pnpm installed by Homebrew, nvm, fnm or Volta is found whether the daemon was started by the login agent or by you in a terminal. Without that read the two disagree: launchd hands a process four system directories, while a terminal hands it your whole PATH. The macOS app shows exactly what was captured on its Shell environment page, and corral doctor reports it too.

For options 1 and 2, the PORT environment variable is set and framework-specific port flags (e.g., --port for Astro/Vite, -p for Next.js) are appended to the command automatically. This ensures the dev server listens on the Corral-allocated port.

# Set a custom dev command
corral project set my-app dev-command "npm run dev"

# Clear it (revert to auto-detection)
corral project set my-app dev-command none

Ready Detection

Corral knows a project is ready in two steps, not two competing methods:

  • Log pattern matching — watches stdout for a framework-specific “ready” message. On its own this only triggers the check below; it no longer means “ready”
  • HTTP health check — asks the server whether it is actually serving, on a one-second interval and immediately whenever a ready line appears. It tries the port Corral allocated and the address the server announced on stdout, so a custom dev command that binds somewhere else is still found

The order matters because a dev server can print “Ready” and then die. Wrangler does exactly that: it reports a fatal error, prints its ready line, and exits. When the line alone was enough, Corral flashed the project green and switched traffic to it moments before it failed. Now the line just prompts a request, and the port has the final say.

Once a ready line has appeared, any HTTP response counts, including a 500. At that point the question is whether something is serving, not whether your app is healthy — a dev server whose / throws while you work on /api is running, and failing it would time the project out and take away the logs you need. Before any ready line the check is stricter and wants a non-5xx response, because nothing has vouched for that server yet.

If a project announces itself but never answers, Corral believes the ready line at the deadline and says so in the logs rather than failing a project that works.

Failure Recovery

If a process crashes unexpectedly, Corral automatically attempts to restart it with exponential backoff. After repeated failures, the unit enters a Failed state with structured failure details — the kind of failure (port in use, readiness timeout, insufficient disk space, etc.), a diagnostic message, and suggested remediation actions.

Failures are persisted to a single ledger at ~/.corral/units/failures.jsonl, with each row tagged by the unit it belongs to (capped at 10 MB, with the most recent 1000 entries kept on daemon startup). A failure the unit caused itself records a snapshot of its logs at the time of failure, accessible via the Failure History tab in the macOS app, corral logs snapshots, or the corral status --json output. Failures decided before the unit’s own process ran — a port already in use, a missing binary, a denied authorization — have no logs to capture and record none.

When a unit fails due to an upstream dependency (e.g., a project fails because a linked service crashed), the failure is marked as a cascade failure. The macOS app renders a failure chain that links the downstream failure back to its root cause, and the downstream unit is given its own copy of the root cause’s snapshot under ~/.corral/logs/<id>/root_cause/ — a real copy, so the forensic survives the root being recovered, dismissed or deleted.

Orphan Cleanup

On startup, Corral scans for any orphaned processes left behind from a previous session (e.g., after a crash) and cleans them up.

Wrangler

If your project has Wrangler in its own node_modules, Corral runs that one. It is the version your dev script would have used, so what Corral reports and what your project runs are the same thing.

If it doesn’t, Corral picks one for you. Which version depends on the project: a Worker’s compatibility_date is only supported up to a point by any given Wrangler, because each Wrangler bundles one exact runtime, and that runtime’s cut-off is fixed on the day it was released. Corral picks the newest release that covers your project’s date and that your project’s Node.js version can run.

That means:

  • You don’t need to install Wrangler globally
  • Your global Wrangler installation, if you have one, is not affected
  • Nothing is downloaded unless Corral is the one supplying the dev command — a project that runs its own Wrangler starts fine even when npm is unreachable

Corral automatically configures Wrangler with the correct port, environment, protocol, and Node.js version for each project.

When a compatibility date outruns the toolchain

A compatibility_date in the future, or one newer than any Wrangler your project’s Node.js version can run, can’t work — and on older Wranglers it fails quietly, by running your Worker under an earlier date rather than telling you. Corral checks this ahead of time and shows the verdict on the project’s Compatibility card, so you find out before you start rather than after something behaves oddly.

Config keys Wrangler 4 removed

Wrangler 4 dropped node_compat, legacy_assets, and usage_model outright. Old compatibility_date values keep working forever, but these keys do not — a config carrying one cannot start on any Wrangler 4.

If Corral is the one choosing your Wrangler, it says so before installing anything, names the keys and their replacements, and stops. It doesn’t let the start fail deep inside Wrangler, about a version you never picked. A project running its own Wrangler is exempt: its config is its own business, and Corral doesn’t second-guess a toolchain it didn’t select.

The replacements are compatibility_flags = ["nodejs_compat"] for node_compat, assets for legacy_assets, and nothing for usage_model — it’s an account-level setting now, so remove it.

// Last updated 2026-09-13