Lambda Custom Runtime for Shell Scripts
⚠️ Update (June 2026): This container image approach has been superseded by a layer-based architecture delivering sub-20ms cold starts and ~1KB function packages. See A Terraform Module for Shell Functions on Lambda for the current recommended approach.
This was our first production runtime for shell scripts on Lambda — packaging everything into Docker container images. It worked, taught us a lot, and led to something better.
The Container Image Approach
The idea was simple: what if Lambda spoke shell? Package a custom runtime into a Docker image, copy your handler in, deploy.
# handler.sh
main() {
local event="$1"
local name=$(echo "$event" | jq -r '.name // "World"')
echo '{"message": "Hello, '"$name"'!"}'
}
FROM ghcr.io/ql4b/lambda-shell-runtime:tiny
COPY handler.sh .
Deploy to Lambda as a container image.
Three Variants
We published three container image variants to GitHub Container Registry and AWS Public ECR, each built for both arm64 and x86_64:
tiny (132MB)
The minimal variant. Includes jq for JSON processing, curl for HTTP, and http-cli for structured API calls. Enough for most HTTP-driven functions — fetch an API, transform the response, return JSON.
docker pull ghcr.io/ql4b/lambda-shell-runtime:tiny
docker pull public.ecr.aws/ql4b/lambda-shell-runtime:tiny
micro (221MB)
Adds awscurl — a Python-based tool that signs requests with AWS SigV4, enabling direct calls to any AWS service without the full CLI.
docker pull ghcr.io/ql4b/lambda-shell-runtime:micro
docker pull public.ecr.aws/ql4b/lambda-shell-runtime:micro
Hindsight note: The
microvariant turned out to be unnecessary.curlin Amazon Linux 2023 ships with native--aws-sigv4support, which does exactly whatawscurldoes — without compiling and bundling a separate Python binary. A singlecurlflag replaces an entire tool:# awscurl approach (what micro added) awscurl --service s3 https://s3.eu-central-1.amazonaws.com/ # curl --aws-sigv4 (built into the OS, no extra binary) curl -sSf --aws-sigv4 "aws:amz:${AWS_REGION}:s3" \ --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \ -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ https://s3.${AWS_REGION}.amazonaws.com/This discovery was one of the reasons we moved away from bundling tools into images and toward the composable layer approach.
full (417MB)
The complete AWS CLI v2 pre-installed. For functions that need complex AWS operations — multi-step workflows, resource management, anything where raw curl calls would be unwieldy.
docker pull ghcr.io/ql4b/lambda-shell-runtime:full
docker pull public.ecr.aws/ql4b/lambda-shell-runtime:full
Image Size Comparison
| Variant | Size | Tools | Use Case |
|---|---|---|---|
tiny | 132MB | jq, curl, http-cli | HTTP APIs, JSON processing |
micro | 221MB | + awscurl | AWS service calls (superseded by curl —aws-sigv4) |
full | 417MB | + AWS CLI v2 | Complex AWS workflows |
| Python 3.12 | ~530MB | boto3, pip | (official runtime for comparison) |
All three variants were smaller than the standard Python runtime, while being production-ready and multi-platform.
Performance
- Cold start: 100-300ms
- Size: 75% smaller than Python runtime (tiny variant)
- Multi-platform: ARM64 and x86_64
- Local testing: Full Lambda RIE support
Source & Packages
The full implementation, Dockerfiles, and CI/CD pipeline are at github.com/ql4b/lambda-shell-runtime.
Pre-built images published to both registries:
# GitHub Container Registry
ghcr.io/ql4b/lambda-shell-runtime:tiny
ghcr.io/ql4b/lambda-shell-runtime:micro
ghcr.io/ql4b/lambda-shell-runtime:full
# AWS Public ECR
public.ecr.aws/ql4b/lambda-shell-runtime:tiny
public.ecr.aws/ql4b/lambda-shell-runtime:micro
public.ecr.aws/ql4b/lambda-shell-runtime:full
What We Learned
The container image approach proved shell scripts were viable on Lambda but had trade-offs:
- Large deployments — even
tinywas 132MB vs ~1KB with layers - Slow iteration — rebuild image for every handler change
- Coupled runtime — runtime and tools bundled together, no mix-and-match
- Higher cold starts — 100-300ms vs sub-20ms with the layer approach
These lessons led directly to the layer-based architecture where the runtime deploys once as a shared layer and functions are pure shell scripts.
What Replaced This
The runtime-as-a-layer approach solves all of the above:
- 2.3MB shared runtime layer (deployed once per region)
- ~1KB function packages (just your shell script)
- Sub-20ms cold starts on arm64
- Composable tool layers (jq, htmlq, uuid — add only what you need)
- Instant deploys — no image rebuild
If you’re starting fresh, go directly to terraform-aws-lambda-shell-runtime-layer. The latest work is published as an official module on the Terraform Registry:
module "lambda-shell-runtime-layer" {
source = "ql4b/lambda-shell-runtime-layer/aws"
version = "x.x.x"
}
This was the first step in our shell Lambda journey — from container images (100-300ms) to raw TCP layers (sub-20ms).