AWS Lambda-compatible endpoint for local dev and tests (devcloud, Rust). Warm starts. Port 3001.
722
A lightweight, AWS Lambda-compatible endpoint for local development and deterministic tests. It is the Lambda service of devcloud, packaged as a standalone Rust binary — no orchestrator, dashboard, or other services.
It serves the Lambda REST API on port 3001 (the port sam local start-lambda uses) and runs Python and Node.js handlers as child processes inside the container, keeping them warm between invocations like Lambda does. It is not intended for production use or full AWS parity.
linux/amd64, linux/arm64latest, 0.3.2, … (see Changelog); with the Docker CLI for container image functions: latest-docker, 0.3.2-docker (see Container image functions)services/lambda)docker run --rm -p 3001:3001 -v devcloud-lambda-data:/data simota/devcloud-lambda
cat > app.py <<'EOF'
def handler(event, context):
return {"sum": event["a"] + event["b"]}
EOF
zip function.zip app.py
export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url http://localhost:3001 lambda create-function \
--function-name sum --runtime python3.12 --handler app.handler \
--role arn:aws:iam::000000000000:role/lambda --zip-file fileb://function.zip
aws --endpoint-url http://localhost:3001 lambda invoke \
--function-name sum --cli-binary-format raw-in-base64-out \
--payload '{"a":2,"b":3}' --log-type Tail out.json && cat out.json
In the default relaxed auth mode any credentials are accepted. The --role ARN is stored but not checked.
services:
lambda:
image: simota/devcloud-lambda:0.3.2
ports:
- "3001:3001"
volumes:
- lambda-data:/data
- ./layers:/opt:ro # optional: layer contents (see Layers)
environment:
# optional: credentials handed to every handler (see Credentials)
DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID: test
DEVCLOUD_LAMBDA_FUNCTION_SECRET_ACCESS_KEY: test
volumes:
lambda-data:
| Variable | Default | Description |
|---|---|---|
DEVCLOUD_LAMBDA_ADDR | 0.0.0.0:3001 | Listen address (host:port). |
DEVCLOUD_LAMBDA_STORAGE | /data | Storage root for function state and deployment packages. |
DEVCLOUD_LAMBDA_ENDPOINT | http://localhost:3001 | Public base URL used for GetFunction Code.Location download links and function URLs. Set it when clients reach the container under another host or port. |
DEVCLOUD_LAMBDA_REGION | us-east-1 | Region used in function ARNs and expected in SigV4 signatures. |
DEVCLOUD_LAMBDA_ACCOUNT_ID | 000000000000 | Account ID used in function ARNs. |
DEVCLOUD_LAMBDA_AUTH_MODE | relaxed | relaxed accepts any request; signed-relaxed requires a well-formed SigV4 Authorization header without verifying it; strict verifies AWS SigV4 signatures (service lambda). |
DEVCLOUD_LAMBDA_ACCESS_KEY_ID | — | Access key accepted in strict mode. |
DEVCLOUD_LAMBDA_SECRET_ACCESS_KEY | — | Secret key used to verify signatures in strict mode. |
DEVCLOUD_LAMBDA_S3_STORAGE | — | Storage root of a devcloud-s3 volume. Enables Code.S3Bucket / Code.S3Key deployment packages. |
DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS | 300 | How long an idle execution environment stays warm. 0 makes every invocation a cold start. |
DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID / DEVCLOUD_LAMBDA_FUNCTION_SECRET_ACCESS_KEY / DEVCLOUD_LAMBDA_FUNCTION_SESSION_TOKEN | — | Credentials passed to every handler as AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN, standing in for the execution role. Set the key id and the secret together (the session token is optional). |
DEVCLOUD_LAMBDA_OPT_DIR | /opt | Directory standing in for Lambda's /opt (layer contents). |
DEVCLOUD_LAMBDA_URL_AUTH_MODE | DEVCLOUD_LAMBDA_AUTH_MODE | Auth mode for AWS_IAM function URLs only. strict verifies URL signatures (against DEVCLOUD_LAMBDA_ACCESS_KEY_ID / ..._SECRET_ACCESS_KEY) while the API stays relaxed. |
DEVCLOUD_LAMBDA_LOG_INVOCATIONS | true | Print every invocation's log (INIT_START/START/END/REPORT and the handler's output) to stdout, each line tagged [<function>] — a stand-in for CloudWatch Logs in docker logs. |
DEVCLOUD_LAMBDA_DOCKER | false (true in -docker tags) | Run PackageType: Image functions with the Docker CLI. |
DEVCLOUD_LAMBDA_DOCKER_NETWORK | — | Docker network this container is on; function containers join it (see Container image functions). |
Strict mode example:
docker run --rm -p 3001:3001 \
-e DEVCLOUD_LAMBDA_AUTH_MODE=strict \
-e DEVCLOUD_LAMBDA_ACCESS_KEY_ID=mykey \
-e DEVCLOUD_LAMBDA_SECRET_ACCESS_KEY=mysecret \
simota/devcloud-lambda
Deploy from a bucket in devcloud-s3 by sharing its volume (read-only is enough):
services:
s3:
image: simota/devcloud-s3:0.3.0
ports: ["9000:9000"]
volumes: [s3-data:/data]
lambda:
image: simota/devcloud-lambda:0.3.2
ports: ["3001:3001"]
environment:
DEVCLOUD_LAMBDA_S3_STORAGE: /s3
volumes:
- lambda-data:/data
- s3-data:/s3:ro
volumes:
s3-data:
lambda-data:
The container runs as a non-root user (UID 10001) under tini. Data persists in the /data volume; mount a named volume or a host directory writable by that UID. It stops cleanly on SIGTERM (docker stop), stopping every handler process first.
Like Lambda, an execution environment loads the handler module once and then serves invocations one at a time, so module-level state and one-time initialization (loading a model, opening clients) carry over between invocations.
INIT_START line, the init-phase output, and Init Duration in the REPORT line. Init gets its own budget of one function Timeout.DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS without invocations, when the function's configuration or code changes or the function is deleted, after a timeout, crash, or init failure, and on shutdown.REPORT includes Max Memory Used: the interpreter process's peak resident memory during the invocation (init included on a cold start; processes it spawns are not counted). Above MemorySize, the log warns that Lambda would have stopped the invocation — memory is not limited here.DEVCLOUD_LAMBDA_LOG_INVOCATIONS):[sum] INIT_START Runtime Version: python3.12
[sum] START RequestId: 57b04d27-... Version: $LATEST
[sum] END RequestId: 57b04d27-...
[sum] REPORT RequestId: 57b04d27-... Duration: 0.36 ms Billed Duration: 1 ms Memory Size: 128 MB Max Memory Used: 34 MB Init Duration: 392.71 ms
Handlers run with the interpreters bundled in the image: Python 3.12 (with boto3, like Lambda's Python runtime) and Node.js 22. A function's Runtime selects the family; when the version differs (python3.13 on Python 3.12, nodejs20.x on Node.js 22), the invocation log gets a [WARNING] devcloud: runtime ... is running on ... line on a cold start, and the container log repeats it once per function and runtime.
python3.x: module.function handlers.nodejs*: CommonJS and ES modules (.mjs, or .js under a package.json with "type": "module"), async and callback style.java*, dotnet*, ruby*, provided*) can be created, but invoking them returns InvalidRuntimeException.boto3 is included (the version current when the image was built). The AWS SDK for JavaScript v3 is not: ship it in the zip or in /opt.Handlers get a cleared environment: PATH, Lambda's reserved variables (AWS_LAMBDA_FUNCTION_NAME, AWS_REGION, LAMBDA_TASK_ROOT, ...), the configured function credentials, and the function's Environment.Variables. Nothing else from the container's environment leaks in. Environment variable names follow Lambda's key pattern (letters, digits, _, starting with a letter).
The layer APIs are not supported; mount the layer contents at /opt instead. As on Lambda, the default PYTHONPATH is /opt/python/lib/python<version>/site-packages:/opt/python and the default NODE_PATH is /opt/nodejs/node<major>/node_modules:/opt/nodejs/node_modules, so packages placed there are importable without extra settings — from ES module handlers too. A function that sets its own PYTHONPATH / NODE_PATH replaces the default.
docker run --rm -p 3001:3001 -v "$PWD/deps:/opt/python:ro" simota/devcloud-lambda
Lambda passes the execution role's credentials to handlers; here, set DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID and DEVCLOUD_LAMBDA_FUNCTION_SECRET_ACCESS_KEY (and optionally ..._SESSION_TOKEN) and every handler receives them, so AWS SDK calls from a handler (for example to a local S3) can sign requests. As on Lambda, functions cannot set AWS_ACCESS_KEY_ID & co. themselves.
CreateFunctionUrlConfig / GetFunctionUrlConfig / UpdateFunctionUrlConfig / DeleteFunctionUrlConfig / ListFunctionUrlConfigs are supported. Requests become payload format 2.0 events and handler results map back to HTTP responses (statusCode, headers, cookies, body, isBase64Encoded; any other JSON value is returned as a 200 JSON body).
A URL is served on the same port, three ways:
FunctionUrl, http://<url-id>.lambda-url.<region>.localhost:3001/;http://localhost:3001/_url/<url-id>/;http://localhost:3001/urls/<function>/ — no id to look up and no *.localhost resolution needed.<url-id> is derived from the account, region, and function name, so a URL stays the same when it, the function, or the /data volume is recreated.
cat > web.py <<'EOF'
def handler(event, context):
name = (event.get("queryStringParameters") or {}).get("name", "world")
return {"statusCode": 200, "body": f"hello {name}"}
EOF
zip web.zip web.py
aws --endpoint-url http://localhost:3001 lambda create-function \
--function-name web --runtime python3.12 --handler web.handler \
--role arn:aws:iam::000000000000:role/lambda --zip-file fileb://web.zip
aws --endpoint-url http://localhost:3001 lambda create-function-url-config \
--function-name web --auth-type NONE
curl "http://localhost:3001/urls/web/?name=devcloud" # hello devcloud
AuthType: AWS_IAM refuses unsigned requests in every auth mode and verifies the SigV4 signature (service lambda) in strict mode; other modes check only that the signature is well-formed. DEVCLOUD_LAMBDA_URL_AUTH_MODE=strict verifies URL signatures while the API stays relaxed. AuthType: NONE URLs are open: resource policies (AddPermission) are not evaluated.Cors answers preflight requests and decorates responses. With AllowCredentials, wildcards are answered with the requested method and headers, since browsers read * literally there.InvokeMode: RESPONSE_STREAM is accepted but responses are buffered.PackageType: Image functions run with the Docker CLI in the -docker tags (latest-docker, 0.3.2-docker), sharing the host's Docker daemon. Each execution environment is a docker run of the function's image, invoked through the Runtime Interface Emulator that every AWS base image (public.ecr.aws/lambda/*) starts, and kept warm like zip functions. MemorySize becomes the container's memory limit.
Mount the Docker socket and name the network this container is on: function containers join it and are reached by name.
services:
lambda:
image: simota/devcloud-lambda:0.3.2-docker
ports: ["3001:3001"]
environment:
DEVCLOUD_LAMBDA_DOCKER_NETWORK: myapp_default # <compose project>_default
volumes:
- lambda-data:/data
- /var/run/docker.sock:/var/run/docker.sock
volumes:
lambda-data:
aws --endpoint-url http://localhost:3001 lambda create-function \
--function-name api --package-type Image --code ImageUri=my-function:latest \
--role arn:aws:iam::000000000000:role/lambda
ImageUri is any image the daemon has or can pull; the first start may pull it within a 120 s budget.ImageConfig (EntryPoint, Command, WorkingDirectory) overrides apply independently, as on Lambda.REPORT includes Max Memory Used: the container's cgroup memory peak since it started (page cache included, so it reads higher than a process's resident memory), with the same warning above MemorySize. It is read with docker exec <container> cat, so images without cat omit it. A cold start's log opens with INIT_START Image: <ImageUri>.docker.sock gives this container root on the Docker host, so the -docker tags run as root. Use them only where that is acceptable.SIGTERM (docker stop, docker compose down) this container removes the function containers it started. If it is killed instead (docker rm -f, a crash), it removes the leftovers the next time it starts on the same /data volume: containers are labelled devcloud.lambda.owner=<instance id> (kept in /data/instance-id), and only its own are touched, so several devcloud-lambda containers can share one Docker daemon.502 with an error saying so.| Feature | Status |
|---|---|
| CreateFunction, GetFunction, GetFunctionConfiguration, ListFunctions (Marker/MaxItems), DeleteFunction | Yes |
| UpdateFunctionConfiguration (RevisionId precondition), UpdateFunctionCode | Yes |
| Function identifiers: name, partial ARN, full ARN | Yes |
Invoke: RequestResponse, Event (async, 202), DryRun | Yes |
X-Amz-Function-Error: Unhandled on handler errors | Yes |
X-Amz-Log-Type: Tail (last 4 KB of the INIT_START/START/END/REPORT log) | Yes |
Warm execution environments, Init Duration | Yes |
Per-function Timeout | Yes — the environment's whole process group is stopped |
Function URLs (NONE, AWS_IAM, CORS; stable ids and /urls/<function>/) | Yes |
| Tags (list / tag / untag), GetAccountSettings | Yes |
| Deployment packages | Partial — zip only (ZipFile, or S3Bucket/S3Key with DEVCLOUD_LAMBDA_S3_STORAGE); no zip64. A package over 250 MB unzipped is rejected, as on Lambda (50 MB for a direct ZipFile upload) |
| Layers | Partial — mount the contents at /opt; no layer APIs |
Container images (PackageType: Image) | Yes in the -docker tags, through the Runtime Interface Emulator; not in the default tags |
MemorySize | Reported to handlers; REPORT adds Max Memory Used (zip and image functions); not enforced for zip functions (it is the container limit for image functions) |
| SigV4 auth | Yes — signatures are verified in strict mode |
| Versions, aliases, event source mappings, concurrency limits | No — only $LATEST exists |
/data (and, in the -docker tags, reach the Docker socket). Only deploy code you trust.GET API requests carrying a non-loopback Origin header are rejected with 403, so a web page cannot create or invoke functions. SDKs and CLIs are unaffected. Function URLs are meant to be called from browsers and are not subject to this check./data volume between multiple containers; locking is in-process only.Max Memory Used (the container's cgroup peak since it started) with the over-MemorySize warning.INIT_START Image: <ImageUri> instead of an empty Runtime Version:./data volume) left behind; containers are labelled devcloud.lambda.owner. Containers started by 0.3.0 are unlabelled and not cleaned up.-docker tags (Docker CLI included; DEVCLOUD_LAMBDA_DOCKER_NETWORK lets function containers join this container's network)./urls/<function>/ addresses a URL by name. URLs created by earlier versions keep their ids.DEVCLOUD_LAMBDA_URL_AUTH_MODE verifies AWS_IAM URL signatures independently of the API's auth mode.DEVCLOUD_LAMBDA_LOG_INVOCATIONS, on by default).Max Memory Used in REPORT, with a warning above MemorySize.docker logs), once per function and runtime.No such file or directory); DEVCLOUD_LAMBDA_DOCKER=true without docker on PATH is warned about at startup.DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS (default 300) idle. Module state now persists between invocations; set DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS=0 for the previous behavior. Cold starts report Init Duration./opt: /opt/python and /opt/nodejs/node_modules are on the default PYTHONPATH / NODE_PATH, including for ES module handlers (DEVCLOUD_LAMBDA_OPT_DIR relocates /opt).DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID / ..._SECRET_ACCESS_KEY / ..._SESSION_TOKEN are passed to handlers.NONE / AWS_IAM auth and CORS.[WARNING] log line when a runtime's version differs from the bundled interpreter..js handlers under "type": "module" load as ES modules; handlers get an empty stdin.3001 for linux/amd64 and linux/arm64, with Python 3.11, Node.js 18, and tini.Content type
Image
Digest
sha256:5d4505a67…
Size
109.9 MB
Last updated
about 9 hours ago
docker pull simota/devcloud-lambda