Dynamic HTML with Python, AWS Lambda, and Containers

Adam Novotny
3 min readMar 27, 2021


This article is an extension of my previous article describing a similar deployment process using native AWS Lambda tools. However, Amazon since started supporting container images and updated it’s pricing policy to 1ms granularity. Both are major developments improving tooling and making small deployments cost effective.

Deploying AWS Lambda using a container

My previous article focused on the logic of the code and didn’t address how to actually deploy the function because that was well covered by AWS in its many tutorials. Here I explore the new the container deployment options while keeping all business logic untouched.

Please review the AWS tutorial on deploying a generic Python Lambda code using containers which I leveraged. In this article I am not going to describe how to push a container to AWS ECR or select your ECR container for your Lambda function in AWS Console. These are well covered in this AWS toturial. Below, I am showing how to serve html pages as opposed to the standard json API responses Lambdas are typically used for.


FROM public.ecr.aws/lambda/python:3.8
RUN mkdir -p /mnt/app
ADD app.py /mnt/app
ADD index.html /mnt/app
WORKDIR /mnt/app
RUN pip install --upgrade pip
RUN pip install Jinja2==2.11.*
CMD ["/mnt/app/app.handler"]

I am using the AWS base image because it is packaged with a very nice mini server that simulates function responses when developing locally. This is extremely useful because we can call the function with 100s of arguments and verify that it behaves as expected before deployed.

App code

From the Dockerfile, we can see that all application code is contained in two files:

1) app.py:

import os
from jinja2 import Environment, FileSystemLoader
def handler(event, context):
env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "."), encoding="utf8"))
my_name_from_query = False
if event["queryStringParameters"] and "my_name" in event["queryStringParameters"]:
my_name_from_query = event["queryStringParameters"]["my_name"]
template = env.get_template("index.html")
html = template.render(
return {
"statusCode": 200,
"body": html,
"headers": {
"Content-Type": "text/html",

2) index.html:


app.py simply parses one argument named “my_name” from the Lambda query string and passes it to the html template as variable named “my_name”. Jinja2 then parses the variable and returns the final template.

Calling and testing the app locally

Testing the app locally is very simple thanks to the new container packaging. Simply run

docker-compose -f docker-compose.yml up

where docker-compose.yml file is defined as:

version: '3'
container_name: cont_name
image: cont_name_img
context: .
dockerfile: Dockerfile
- .:/mnt/app
- "9000:8080"
stdin_open: true
tty: true
restart: always

This stands up the function locally on a simple AWS-provided server. We can send requests and monitor responses using Python code such as:

import requests
r = requests.get(
data=open("event.json", "rb")

where “event.json” is any .json file we wish to send to the lambda function as arguments. In the example case above, we would send something like:

"queryStringParameters": {
"my_name": "Adam"


The simple AWS base server returns responses such as the one below. This is where we can see the significant impact of the new 1ms pricing update. The cost of running this example code is about 9ms which is very small considering that we are returning a full html template to browsers. However, previously AWS would charge for the full 100ms because that is the minimum charge allowed. Now, this function could cost nearly 90% less!

Lambda duration

This article was originally published on my personal website adamnovotny.com