Introduction
A few months back, I wanted to reserve a squash court, as usual, on a Wednesday evening. Unfortunately, everything was booked in my favorite club, so I started looking for other places. I was on my phone, which wasn’t very convenient, and every website was different — so I quickly got frustrated.
This situation happened a few times, and then I realized I could make this task easier by creating a small app to check court availability for me. It seemed like a good small project. I have two small kids, so I don’t have much free time, but I decided to spend 15–30 minutes on it whenever I could — and here we are!
What started as a quick idea to save a few clicks, turned into a mini project — a Python + FastAPI app hosted on AWS Lightsail, with full GitHub Actions automation.
In this post, I will share how I built it, automated deployment with CI/CD, and what I learned along the way.
Architecture
During my IT career, I spent over 10 years as an Infrastructure Engineer and the next 5 as a DevOps Engineer, so I’m not really a programmer. That’s why my technical choices were mostly about simplicity rather than software design perfection.
As I mentioned, time wasn’t on my side, so I decided on a simple architecture. Still, I wanted to learn something new, so I promised myself to use at least one AWS service I hadn’t worked with before.
The choice fell on AWS Lightsail.
Amazon Lightsail offers easy-to-use virtual private servers (VPS), containers, storage, and databases. It’s really intuitive and fits perfectly for my small app.
I’m quite comfortable with Python, so the backend is built with FastAPI. For the frontend, I used some simple HTML and CSS - generated mostly by ChatGPT, since I don’t have strong frontend experience.
To parse the web pages, I used the BeautifulSoup module, which provides methods and Pythonic idioms to navigate, search, and modify the parsed HTML tree.
Tests are based on pytest and mock, with simple assertions for specific use cases.
The code is hosted on GitHub, and I use GitHub Actions to build, test, and deploy automatically to AWS Lightsail after every push to the main branch.
Here’s how the overall architecture looks:
Initial app design
The main goal was to create a lightweight, intuitive web page to check squash court availability in Wroclaw.
The user can choose a date from the calendar (up to one week ahead) and select an hour between 06:00 and 23:00 (full hours only, since most facilities don’t support half-hour bookings).
After selecting the date and hour, the user clicks “Sprawdz” (means Search), and all sports facilities are listed with availability information. For the biggest club, Hasta La Vista, the app also lists individual courts — since there are 32 of them and players often prefer specific ones.
How to pull the data?!
Once I knew what I wanted to achieve, I started figuring out how to do it.
First, I gathered all sports facilities in Wroclaw that offer squash. Then, one by one, I tried to fetch the data I needed. Tools like curl and browser dev tools helped a lot.
After a few experiments, I wrote my first Python file — hasta.py. Using requests and BeautifulSoup, I fetched each website’s HTML and parsed the structure based on date and time. It looked like follow:
def check_availability(date_str: str, time_str: str):
url = f"https://(...)"
datetime_variants = [
f"{date_str} {time_str}:00",
f"{date_str}T{time_str}:00"
]
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
if "html" not in data:
return "❌ Failed to fetch calendar data."
soup = BeautifulSoup(data["html"], "html.parser")
all_elements = soup.select("[data-begin]")
for el in all_elements:
data_begin = el.get("data-begin")
if data_begin in datetime_variants:
text = el.get_text(strip=True)
if "Book" in text:
return (
f"✅ Court is available {date_str} {time_str}"
)
elif "Notify me" in text:
return f"❌ Court is not available ({time_str}) {date_str}"
After a few evenings, I had an early version of a crawler that could check court availability for all clubs in the city.
In the meantime, I also started working on a simple frontend to connect the crawler with a web interface — again, as I have already mentioned, mostly with ChatGPT’s help.
The frontend consists of three main files: style.css, index.html, and result.html.
Everything was working well locally, so I decided to deploy it as a container on AWS Lightsail.
Let's Automate A Few Things
Before deploying anything manually to AWS, I wanted to automate the process as much as possible.
Since the code was already on GitHub, GitHub Actions was a natural choice for CI/CD. I decided to build a simple pipeline that would:
Build the Docker image
Run tests using pytest
Deploy automatically to AWS Lightsail
I also decided to use OIDC (OpenID Connect) for authentication between GitHub and AWS, so I didn’t need to store long-lived access keys (you can check on OIDC Stack in my previous blog post regarding AWS Serverless). This approach is more secure and follows AWS best practices for CI/CD integration.
Here’s a simplified version of the GitHub Actions workflow:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/github-actions-role
aws-region: eu-west-1
- name: Build Docker image
run: docker build -t court-checker .
- name: Install lightsailctl
run: |
curl "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" -o "/usr/local/bin/lightsailctl"
chmod +x /usr/local/bin/lightsailctl
/usr/local/bin/lightsailctl --version
- name: Create Lightsail container service if not exists
run: |
set -e
if ! aws lightsail get-container-services --service-name court-checker >/dev/null 2>&1; then
echo "Creating Lightsail container service..."
aws lightsail create-container-service \
--service-name court-checker \
--power medium \
--scale 1
else
echo "Lightsail container service already exists."
fi
- name: Push image to Lightsail registry
id: push
run: |
set -euo pipefail
OUTPUT=$(aws lightsail push-container-image \
--service-name court-checker \
--label web \
--image court-checker:latest)
echo "$OUTPUT"
# Extract registryPath from human-readable output
REGISTRY_PATH=$(echo "$OUTPUT" | grep -oE ':court-checker\.web\.[0-9]+' | head -n 1)
if [ -z "$REGISTRY_PATH" ]; then
echo "ERROR: No registryPath found in push output."
exit 1
fi
echo "registry_path=$REGISTRY_PATH" >> $GITHUB_OUTPUT
echo "Using image: $REGISTRY_PATH"
- name: Create container config
run: |
set -euo pipefail
cat > container.json <<EOF
{
"web": {
"image": "${{ steps.push.outputs.registry_path }}",
"ports": {
"8000": "HTTP"
}
}
}
EOF
cat container.json
- name: Create endpoint config
run: |
echo '{
"containerName": "web",
"containerPort": 8000,
"healthCheck": {
"path": "/health",
"successCodes": "200-499",
"timeoutSeconds": 5,
"intervalSeconds": 10,
"healthyThreshold": 2,
"unhealthyThreshold": 2
}
}' > endpoint.json
- name: Deploy to Lightsail
run: |
aws lightsail create-container-service-deployment \
--service-name court-checker \
--containers file://container.json \
--public-endpoint file://endpoint.json
Release And Share With Others
Once all tests were done and I had used the app myself for a couple of days, I decided to share it with a few of my friends.
They tried it for about a week, gave me some feedback, I fixed a few things, and then decided to share it with a wider group.
Squash isn’t super popular, but I posted about the app in a local Facebook group with around 3,000 squash enthusiasts from Wroclaw so they could give it a try.
Now I can see there are around 20–50 visits per week — which makes me really happy that at least some people are finding it useful!
Costs
Obviously, I wanted to keep costs as low as possible. Fortunately, I’m an AWS Community Builder, so I have some AWS credits, which gave me flexibility in initial testing and I did not need to bother too much about that.
Here’s the rough cost breakdown:
Domain registration: around $45 for three years + $10 tax (one-time cost)
AWS Lightsail container (micro tier: 0.25 vCPU, 1GB RAM): $10/month
Each container service includes 500 GB/month data transfer. Extra data costs start at $0.09/GB depending on the region.GitHub Actions: 2,000 free minutes per month (my project used only 103 minutes in August)
Thoughts about AWS Lightsail?
It’s definitely a great and easy way to deploy small web apps with minimal effort.
You get a public DNS by default:
https://<app-name>.<random_digits>.<region>.cs.amazonlightsail.com
Containers are perfect for small/medium projects or proof-of-concepts. I didn’t use instances, but they seem suitable for larger workloads.
With GitHub Actions automation, I don’t need to worry about deployment — it happens automatically after every push.
The only downsides I’ve noticed are:
Lack of container-level alarms and metrics (available only for instances)
Occasional delays during deployment
Overall, it’s a neat and simple setup for quick and cost-effective deployments.
Conclusion And Key Takeaways
Looking back, this small side project turned out to be much more than I expected.
What started as a simple idea to save time booking a squash court became a fun way to learn, automate, and explore new AWS services. I truly recommend that everyone try something like this and not be afraid to experiment.
I also realized how much can be achieved by dedicating just a little time each day — consistency really does beat intensity.
Here are also a few takeaways I’d like to share as a summary:
Start small, iterate often: Even with just 15–30 minutes a day, you can deliver a working project over time.
AWS Lightsail is underrated: It’s perfect for small, containerized apps — simple setup, predictable cost, and built-in DNS.
Automation saves time: GitHub Actions makes it easy to build, test, and deploy without manual steps or AWS Console clicks.
Security by design: Using OIDC between GitHub and AWS avoids storing long-lived credentials.
Don’t fear imperfect tools: FastAPI, BeautifulSoup, and a bit of ChatGPT-generated frontend were enough to get a solid MVP online.
And if you’d like to take a look at the app yourself, check it out here! It is in Polish however should be intuitive for everyone.

Top comments (0)