uvdock
v0.1.0
Published
Dock your code into an AWS Lambda MicroVM. Bring an existing Lambda handler, attach it, and get a running, stateful VM — a tiny PID-1 runtime bridges HTTP to the Lambda handler contract (Function URL events + invoke mode + lifecycle hooks), so your handle
Downloads
19
Maintainers
Readme
uvdock
Dock your code into an AWS Lambda MicroVM. Bring a Lambda handler, attach it, get a running — and stateful — VM.
MicroVMs are powerful but raw — your app runs as PID 1 on ARM64, there's no invoke API, and you have to answer platform lifecycle hooks. uvdock makes them usable: bring your existing code, dock it, and you get a running VM you can invoke like a function. It ships with a Lambda adapter as the first way to bring code — a tiny, zero-dependency PID-1 runtime that lets an existing handler(event, context) run exactly as written, but now keeping its memory warm between calls. (The optional invoke client depends on uvlink for auth-token handling; the runtime that ships inside the VM stays dependency-free.)
Part of the
uv*MicroVM toolkit — sibling touvlink(reach a MicroVM from a browser).uvlinkgets you to a MicroVM;uvdockgets your code running inside one.
The Lambda adapter is the inverse of
aws-lambda-web-adapter: that runs a web server on Lambda; this runs a Lambda handler as a server on a MicroVM.
Hobby project. Built for fun and learning, not affiliated with or endorsed by AWS / Lambda / Amazon. MIT-licensed — free for anyone to use, fork, and modify. Provided as-is, with no warranty and no guarantee of future releases, maintenance, or support.
Why
Lambda MicroVMs are Firecracker-isolated VMs that suspend when idle and resume with memory and disk intact, each with its own HTTPS endpoint. But they're an alien target: your process runs as PID 1 on ARM64, there's no invoke API (you reach it over HTTP with a token), and you must answer the platform's lifecycle hooks. A raw Lambda zip can't run there.
This adapter is the missing glue. The payoff: a "slim, stateful Lambda" — the same handler you already have, but in-memory caches, loaded models, and open connections persist across invocations instead of being thrown away.
How it works
Lambda zip in S3 Running MicroVM
(code + handler, unchanged) ┌───────────────────────────────┐
│ │ bootstrap.mjs (PID 1) │
│ build.sh │ HTTP :8080 ─┐ │
▼ │ ▼ │
unzip → /app + bootstrap.mjs ───► │ translate → handler(event,ctx)
+ generated Dockerfile │ │ │
│ │ response ◄──┘ │
▼ │ lifecycle hooks (/run …) │
create-microvm-image └───────────────────────────────┘build.sh never runs your code — it repackages: unzip the Lambda deployment package into /app, drop in the adapter, generate a Dockerfile, and hand it to create-microvm-image. At runtime the adapter serves HTTP, turns each request into a Lambda Function URL v2.0 event, calls your handler, and turns the return value back into a response.
Quick start
Point it at a Lambda deployment package in your own S3 bucket — the same object Lambda self-managed code storage uses.
REGION=us-east-1 ACCOUNT=123456789012 BUCKET=my-bucket \
LAMBDA_S3_URI=s3://my-bucket/functions/my-fn.zip \
HANDLER=index.handler \
./build.sh
# → prints IMAGE_ARNThen run it and invoke it:
aws lambda-microvms run-microvm --region us-east-1 --image-identifier <IMAGE_ARN> \
--ingress-network-connectors "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:ALL_INGRESS" \
--egress-network-connectors "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
node invoke.mjs <microvmId> '{"numbers":[1,2,3,4,5]}'
# → { "sum": 15, "count": 5, "invocations": 1 }Try it locally (no AWS)
APP_DIR="$PWD/example" HANDLER=index.handler PORT=8099 node runtime/bootstrap.mjs &
curl -s "http://127.0.0.1:8099/hello?name=world" # HTTP mode
curl -s -XPOST "http://127.0.0.1:8099/2015-03-31/functions/function/invocations" -d '{"numbers":[1,2,3]}' # invoke modeThe stateful payoff
invoke.mjs is a lambda.invoke()-shaped client. The difference — and the reason to use this:
| | lambda.invoke() | invokeMicrovm() |
|---|---|---|
| Capacity | conjured by the service | a VM you already ran (you own its lifecycle) |
| State between calls | none | preserved — module state, warm caches, loaded models persist |
| First call after idle | cold start | auto-resume from suspend (once) |
| Isolation | shared Firecracker fleet | one dedicated Firecracker VM |
import { invokeMicrovm } from "uvdock";
const out = await invokeMicrovm("mvm-01234...", { numbers: [1, 2, 3] }, { region: "us-east-1" });Two invoke modes
| Mode | Path | event your handler sees | Response |
|---|---|---|---|
| Invoke (passthrough) | POST /2015-03-31/functions/function/invocations | your payload, verbatim | return value, verbatim JSON |
| HTTP (Function URL v2.0) | any other path/method | Function URL v2.0 request envelope | your {statusCode, headers, body, cookies} |
Invoke mode intentionally mirrors the Lambda Runtime Interface Emulator (RIE) contract, so existing tooling and curl recipes work unchanged.
Configuration
Set via env vars (baked into the image by build.sh, or exported for local runs).
build.sh
| Var | Required | Default | Meaning |
|---|---|---|---|
| REGION ACCOUNT BUCKET | ✓ | — | AWS target + bucket for build context |
| LAMBDA_S3_URI | ✓ | — | s3://…/fn.zip — your Lambda deployment package |
| HANDLER | | index.handler | file.export to load from /app |
| IMAGE_NAME | | uvdock | MicroVM image name |
| BUILD_ROLE | | MicrovmBuildRole | build role (needs s3:GetObject on BUCKET — see gotchas) |
| NODE_TAG | | node:22-bookworm-slim | container base image |
| ENV_VARS | | — | comma-separated K=V baked into the image |
Adapter runtime
| Var | Default | Meaning |
|---|---|---|
| HANDLER | index.handler | which export to invoke |
| PORT | 8080 | MicroVM default ingress port |
| ADAPTER_CONCURRENCY | 1 | max concurrent invocations. Default 1 matches Lambda's one-at-a-time model. Set N (or 0 = unbounded) only if your handler is reentrant |
| MAX_BODY_BYTES | 6291456 (6 MB) | request body cap (Lambda's sync payload limit) |
Optional lifecycle hooks
Export any of these from your handler module and the adapter calls them; omit them and it uses safe 200 defaults:
export async function onRun(payload) {} // { microvmId, runHookPayload } — mint per-VM identity HERE
export async function onSuspend() {} // flush before snapshot
export async function onResume() {} // refresh creds / reconnect
export async function onTerminate() {}Snapshot-safety: anything unique-per-VM (a session id, secret, tenant token) must be generated in
onRun, never at import/build time — every VM resumed from one snapshot shares build-time state byte-for-byte.
Compatibility & limits
| ✓ Works | ✗ Not supported |
|---|---|
| CommonJS (exports.handler) and ESM (export const handler) | Non-Node runtimes (Python, Go, …) — the pattern generalizes, this code doesn't |
| Request/response handlers (Function URL / API Gateway v2.0 payload) | Event-source handlers (SQS/S3/EventBridge) — no HTTP request to translate from |
| async handlers returning {statusCode,…} or a bare object/string | API Gateway v1.0 payloads (event.httpMethod), response streaming, layers, extensions |
| isBase64Encoded bodies, cookies[] responses, context.getRemainingTimeInMillis() | X-Ray, real CloudWatch log-stream identity, provisioned concurrency |
Similar projects & when not to use this
Be honest about the alternatives — reach for this only when its specific shape fits.
- Lambda Managed Instances — AWS's first-party "warm, no-cold-start, unchanged code" mode. If you want a fully managed, autoscaling, load-balanced warm Lambda and don't need per-tenant isolation, use this instead — it runs on EC2 Nitro containers with dynamic scaling. Reach for
uvdockwhen you specifically want one Firecracker-isolated VM per tenant/session, a dedicated HTTPS endpoint, and snapshot / suspend-when-idle billing — none of which Managed Instances offers. - aws-lambda-web-adapter — the inverse (a web server on Lambda). Use it if your goal is the opposite direction.
- RIE /
serverless-offline/lambda-local— local emulation/testing, not a production MicroVM target. - Fargate / ECS — if you want a persistently-running service with standard load balancing and no 8-hour VM lifetime, a container is simpler.
- A native MicroVM app — if you're greenfield on MicroVMs with no existing Lambda code, write directly to the platform; this adapter is dead weight.
Gotchas (learned from real runs)
- Build role needs S3 read on your bucket.
create-microvm-imagefetches the build context as theBUILD_ROLE; if that role lackss3:GetObjectonBUCKET, the build fails with "Access denied when fetching artifact from S3." - Auth tokens expire at 60 min; VMs live up to 8 h.
invoke.mjscaches and re-mints on 403 — a long-lived client that doesn't will start failing. - Concurrency differs from Lambda. Defaulted to 1 for safety (see
ADAPTER_CONCURRENCY). - ARM64 only. Native deps must ship aarch64 binaries.
Requirements
- Node.js ≥ 18 (uses global
fetch, ESM). - AWS CLI v2 (
lambda-microvmsis v2-only) forbuild.sh/invoke.mjscontrol-plane calls.
Development
npm install # pulls uvlink (used by the invoke client)
npm test # NO AWS requiredTests spin up the adapter as a child process against fixtures and assert the full contract: ESM + CJS loading, both invoke modes, state persistence, all five hooks, error shapes, body cap, concurrency serialization, and the uvlink token-provider composition.
Files
| File | Role |
|---|---|
| runtime/bootstrap.mjs | The adapter — PID 1, HTTP↔Lambda bridge, 2 modes, lifecycle hooks. Zero deps. |
| build.sh | S3 zip → wrapped build context → create-microvm-image. |
| invoke.mjs | lambda.invoke()-shaped client (ensure-running → token → POST → response). |
| example/index.mjs | A stateful sample handler; runs unchanged on Lambda or a MicroVM. |
| explainer.html | A visual walkthrough of the whole approach. |
License & disclaimer
MIT © PandeyLabs — free for anyone to use, copy, modify, and distribute.
This is a personal hobby project, not affiliated with or endorsed by AWS, Lambda, or Amazon. It is provided "as is", without warranty of any kind and with no guarantee of future releases, maintenance, bug fixes, or support. Use at your own risk; you are responsible for anything you run with it (including AWS charges).
