Overcast is alpha — behaviour and APIs may change between releases. Pin your version and read the changelog before upgrading.

overcast

Getting Started

The inner loop — live code inside emulated compute

Edit a file and see it take effect without rebuilding an image or redeploying: cdk watch, Lambda hot reload, and ECS hot reload, with a full Laravel-on-Fargate walkthrough.

Running your application against Overcast gets you a realistic AWS. It does not, on its own, get you a fast edit-run cycle: a Lambda still wants a zip and an ECS task still wants an image, so a one-character fix costs a rebuild and a redeploy.

Which one to use

How it worksBest for
cdk watchRedeploys changed assets on every saveAny runtime, any bundler. Start here — nothing to configure
Lambda hot reloadBinds a host directory at /var/taskInterpreted runtimes, when redeploy latency still bites
ECS hot reloadBinds a host directory over a task volumeLong-running services — Laravel, Rails, Django, Express

cdk watch is genuinely the right answer more often than people expect. It needs no tags, no flags, and no Docker file-sharing configuration, and it works with compiled runtimes and bundlers that hot reload cannot help:

AWS_ENDPOINT_URL=http://localhost:4566 cdk watch

Reach for a bind mount when the redeploy cycle itself is the cost you want gone.

Breakpoints are a separate switch: Step debugging inside emulated compute attaches your editor to the same containers, and reads overcast:hot-reload-path as its source root so a hot-reloaded function needs no second tag.

Turning hot reload on

Both services are off by default — a server that binds arbitrary host paths on request is not something to enable silently. One flag covers everything:

OVERCAST_HOT_RELOAD=true overcast serve
VariableDefaultEffect
OVERCAST_HOT_RELOADfalseEvery compute service
OVERCAST_LAMBDA_HOT_RELOADinherits the umbrellaLambda only
OVERCAST_ECS_HOT_RELOADinherits the umbrellaECS only

The per-service variables override the umbrella in both directions, so OVERCAST_HOT_RELOAD=true OVERCAST_LAMBDA_HOT_RELOAD=false hot reloads ECS while leaving functions on their deployed zips.

Lambda

Tag the function with an absolute host path and the directory is bound at /var/task, read-only:

aws --endpoint-url http://localhost:4566 lambda create-function \
  --function-name demo \
  --runtime nodejs20.x \
  --handler index.handler \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --zip-file fileb://minimal.zip \
  --tags overcast:hot-reload-path=/absolute/path/to/src

Full detail, including CDK tagging and the TypeScript caveats, is in Lambda examples § Hot reload.

What counts as a change

Overcast fingerprints the mounted tree — every entry’s path, and every file’s size and modification time — before each invocation. When the fingerprint moves the warm container is retired and the next invocation starts a fresh one against the edited source, exactly as UpdateFunctionCode does, so editing an already-loaded file works on runtimes that cache modules.

The read is bounded so it stays cheap:

BoundEffect
Dependency and VCS directories are not looked insidenode_modules, .git, __pycache__, .venv, .mypy_cache, .pytest_cache are fingerprinted by name only
20,000 entries, 24 directory levelsA larger or deeper tree is covered up to the limit; edits past it are not noticed
A read costing more than 25 ms is rate-limitedRe-read at most once per 20× the previous read’s cost, and at least once every 2 seconds
Symbolic links are not followedA symlink is fingerprinted by name
SituationWhat you get
Below the 25 ms budgetThe tree is read before every invocation and an edit is live on the next one. A local disk takes 4–11 µs per entry, so a few thousand files stays under it
A Docker Desktop file shareAbout 2 ms per entry, so an edit can take up to 2 seconds rather than one invocation to be seen
Overcast itself in a containerThe bind mount is the daemon’s, but the fingerprint is read by Overcast from its own filesystem. Mount the source into Overcast at the same path too, or the first invocation runs the mounted source and every later one keeps that container
Filesystem timestamps at 1–2 second granularityTwo saves within one tick that leave the file the same size are one change; the second waits for something else to change
A very large tree, a slow file share, or dependencies you edit directlyUse cdk watch instead

ECS

A task definition declares an ordinary name-only scratch volume, mounts it where the source belongs, and a tag redirects that volume at your working tree:

overcast:hot-reload-path/<volume-name> = /absolute/host/path

Nothing about this changes what the task definition means to AWS. The volume is a plain scratch volume — legal on Fargate, deployable as-is — and the container path and readOnly come from the container’s own mountPoints, exactly as in production. Only the tag, which real AWS stores and ignores, redirects it locally. A tag that reaches a production deploy does nothing there.

Only a volume that declares nothing can be redirected. An EFS, Docker, or host.sourcePath volume already names its own storage; pointing it somewhere else would override what the definition asked for rather than fill in what it deliberately left open.

With exactly one redirectable volume you can drop the suffix and use the bare overcast:hot-reload-path — the same key Lambda takes. See ECS examples § Hot reload.

Worked example: Laravel on Fargate

The shape most teams have: PHP-FPM behind nginx, deployed to Fargate, running locally against Overcast from the same CDK app.

Everything local sits behind one guard, so the production synth is untouched:

const taskDef = new ecs.FargateTaskDefinition(this, "AppTask", {
  cpu: 512,
  memoryLimitMiB: 1024,
});

const app = taskDef.addContainer("app", {
  image: ecs.ContainerImage.fromAsset("./docker/php-fpm"),
  logging: ecs.LogDrivers.awsLogs({ streamPrefix: "app" }),
  portMappings: [{ containerPort: 8000 }],
});

if (process.env.OVERCAST_LOCAL === "true") {
  // Plain scratch volumes — all Fargate-legal. Only app-src is redirected;
  // vendor and storage stay scratch so they shadow the host tree.
  taskDef.addVolume({ name: "app-src" });
  taskDef.addVolume({ name: "app-vendor" });
  taskDef.addVolume({ name: "app-storage" });

  app.addMountPoints(
    { sourceVolume: "app-src", containerPath: "/var/www/html", readOnly: false },
    { sourceVolume: "app-vendor", containerPath: "/var/www/html/vendor", readOnly: false },
    { sourceVolume: "app-storage", containerPath: "/var/www/html/storage", readOnly: false },
  );

  // Suffixed key: three redirectable volumes means the bare tag is ambiguous.
  cdk.Tags.of(taskDef).add(
    "overcast:hot-reload-path/app-src",
    path.resolve(__dirname, ".."),
  );
}
OVERCAST_HOT_RELOAD=true overcast serve
OVERCAST_LOCAL=true AWS_ENDPOINT_URL=http://localhost:4566 npx cdk deploy

The traps, in the order they bite

OPcache will ignore your edits. Production PHP images ship opcache.validate_timestamps=0, so PHP compiles each file once and never checks it again. Your saves land in the container and do nothing. This is by far the most common “the mount isn’t working” report when the mount is fine. Ship a dev-only ini:

opcache.validate_timestamps=1
opcache.revalidate_freq=0

Keep vendor/ in the image. A bind mount from a Windows or macOS host crosses a VM boundary, and Composer’s autoloader stats thousands of files per request. The app-vendor scratch volume above shadows the host’s vendor/, and Docker’s copy-up populates it from the image on first mount. Re-run composer install in the image, not on the host, when dependencies change. Same reasoning as Storage tuning § Data dir placement.

Overlay the writable paths. storage/ and bootstrap/cache get the same treatment: writes stay out of your working tree, and copy-up hands the directories to the image’s www-data ownership — which also dissolves the uid mismatch that breaks first write on native-Linux hosts. (Docker Desktop maps bind ownership permissively; Linux does not.) The trade-off: storage/logs then lives in the volume rather than your tree, so read logs through the task’s awslogs stream instead.

File watchers do not see host edits. inotify does not propagate across Docker Desktop’s host boundary, so anything watching needs polling. Worse, php artisan queue:work caches the whole application in memory and will not pick up code changes at all — use queue:listen locally.

Reaching the app. Overcast publishes a host port only when a port mapping carries an explicit hostPort, and Fargate mappings usually carry only containerPort. Front the service with ApplicationLoadBalancedFargateService and use the stack’s ServiceURL output, which resolves to Overcast and forwards to the task — see ECS examples § Load balancers — or add a hostPort under the same local guard as the volume.

When it does not work

Nothing here degrades silently: a task never starts with a mount you asked for and did not get.

SymptomCauseFix
A startup warning, and the task running on the plain scratch volumes it declaredThe tag cannot be honoured — the flag is off, the volume is unknown or not redirectable, the path is relative, or a bare key is ambiguousThe warning names exactly what to correct
CannotStartContainerError, with a stoppedReason naming the host pathsThe daemon refused the bind, usually a path outside Docker Desktop’s File Sharing setAdd the directory there and run the task again
Edits reach the container and the application ignores themThe mount is working and the problem is upstream — OPcache, a compiled bundle, or a worker holding the old code in memoryCheck the mount with a file the framework does not cache
docker exec $(docker ps -q -f name=overcast-ecs) ls -la /var/www/html