Skip to content
updated by skunxicat

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 micro variant turned out to be unnecessary. curl in Amazon Linux 2023 ships with native --aws-sigv4 support, which does exactly what awscurl does — without compiling and bundling a separate Python binary. A single curl flag 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

VariantSizeToolsUse Case
tiny132MBjq, curl, http-cliHTTP APIs, JSON processing
micro221MB+ awscurlAWS service calls (superseded by curl —aws-sigv4)
full417MB+ AWS CLI v2Complex AWS workflows
Python 3.12~530MBboto3, 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 tiny was 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).