Running a containerized web app on AWS Lambda
How md2docx went from a VPS container to serverless on AWS for ~$0 — without changing a line of app code, plus the three sharp edges nobody warns you about.
md2docx ran happily as a Docker container on a VPS. I moved it to serverless on AWS for two reasons: pay ~$0 while nobody’s using it, and learn the platform properly. The surprise was that the application code didn’t change at all. The friction was everywhere else.
The app doesn’t change — an adapter does
Lambda speaks events; Express speaks HTTP. The AWS Lambda Web Adapter bridges the two: drop its binary into the container image as a Lambda extension, and it translates each invoke into a plain HTTP request to your server on localhost. The Dockerfile gains one line; the server stays exactly as it runs on a laptop. I built the image for arm64 (Graviton — about 20% cheaper), pushed it to ECR, and let Lambda serve it behind a built-in Function URL.
Three sharp edges
Then the platform pushed back.
- The region didn’t support Function URLs. Creating one failed with
AccessDeniedException: Unable to determine service/operation name to be authorized— which screams “permissions bug” but actually means “feature not available in this region.” A newer region simply didn’t have it. Fix: use a tier-1 region. - New AWS accounts block public Function URLs by default. Even with a textbook-correct resource policy granting
lambda:InvokeFunctionUrlto everyone, every request returned403 Forbidden. The undocumented-feeling fix: also grantlambda:InvokeFunctionto*. - CloudFront in front, but OAC won’t sign POST bodies. To force traffic through the CDN I tried Origin Access Control with IAM auth — and
POSTbroke instantly, because OAC doesn’t include the request body in its SigV4 signature and Lambda rejects unsigned payloads. For an app whose whole job is POSTing Markdown, that’s fatal. I switched to a shared secret: CloudFront injects a header, and the app rejects anything without it.
What it costs
Lambda and CloudFront both have perpetual free tiers — not the six-month credits — of 1M requests + 400k GB-seconds and 1 TB of egress per month. A hobby converter lives inside them with room to spare, so the bill is effectively $0, and it scales to zero when idle. The price you pay is latency: the first request after a quiet spell waits a few seconds for a cold start. For a tool people hit occasionally, that’s a fine trade.
Serverless didn’t make the app simpler — it made it free at rest and someone else’s problem to keep running. The catch was never the code; it was the dozen small defaults around it.