Let’s be honest: nothing in the cloud is truly free. But with a bit of effort and automation, you can get surprisingly close. This post covers how I run my personal infrastructure including a FoundryVTT game server, a portfolio site, and DNS for basically nothing.

If you’re able to set this up, you’re probably doing it for the fun of it, not because you can’t afford the cost. It’s not technically free, but it’s close enough for me. If your AWS account is still within the 12-month free tier, this setup would easily qualify.


Goals

I want to run:

  • A FoundryVTT server for weekly D&D sessions
  • A static portfolio site
  • Everything for less than one dollar a month
  • misc small workloads

If my AWS account were still under the 12-month free tier, this setup would qualify. Even without that, the actual bill is close to zero.


Requirements

  • A domain (mine is jck.sh) not free, but optional
  • Cloudflare account (free tier)
  • An AWS account with basic services enabled
  • Enough comfort with scripting to automate infra

If you don’t want to pay for a domain, you can get a free one from is-a.dev


Infrastructure Overview

Here’s what I’m running:

  • Domain: jck.sh (paid)
  • Cloudflare: handles DNS, DDoS protection, and caching
  • S3: static site hosting for the portfolio
  • EC2 Spot Instance: runs FoundryVTT temporarily
  • Lambda + EventBridge: controls when the EC2 instance is online
  • Cloudflare Worker: handles FoundryVTT downtime routing

Infra Overview Diagram

This setup lets me serve a static portfolio 24/7 while only running the heavier stuff (like FoundryVTT) when I actually need it.


Security And Cost Mitigation

I locked down my S3 bucket using a bucket policy that denies all non-Cloudflare IPs, making Cloudflare the only way to access the content.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyNonCloudflareIPs",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::jck.sh/*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": [
                        "173.245.48.0/20",
                        "103.21.244.0/22",
                        "103.22.200.0/22",
                        "103.31.4.0/22",
                        "141.101.64.0/18",
                        "108.162.192.0/18",
                        "190.93.240.0/20",
                        "188.114.96.0/20",
                        "197.234.240.0/22",
                        "198.41.128.0/17",
                        "162.158.0.0/15",
                        "104.16.0.0/13",
                        "104.24.0.0/14",
                        "172.64.0.0/13",
                        "131.0.72.0/22"
                    ]
                }
            }
        }
    ]
}

These IP ranges are publicly updated by Cloudflare.

I don’t want any S3 traffic coming in unless it’s from Cloudflare’s proxies. That way, objects get cached and I avoid unexpected bills. You don’t want to accidentally expose a big file and have someone download it repeatedly until you breach a dollar.

My Lambdas are throttled to a concurrency of 1. Even if I screw something up, nothing will get hammered. The longest-running Lambda is the EC2 infrastructure spin-up, and it runs for about 15 seconds.

I’m using EC2 Spot Instances with an EBS volume attached at runtime. This drops the already minuscule cost of a t3.micro by almost 75%.

Foundry is interruptible, so losing the instance isn’t a big deal. All persistent data lives on the attached volume.

I also back up the EBS regularly. Everything fits in a 5 GB volume (for now… until my players decide they want massive assets, of course).

My spot instance Lambda can run more than once without queuing up another spot instance (this is crucial).


Automation

The most important trick here is to keep Foundry offline when not in use.

I use:

  • One Lambda to start the EC2 spot instance
  • Another Lambda to stop it
  • A third Lambda to create budget reports daily
  • A fourth Lambda triggered by budget alerts, sending notifications to Discord
  • EventBridge to trigger both based on a game-night schedule
  • Cloudflare Worker to reroute my foundry page when it’s down
  • Budget Alerts to find cost anomalies

What I’m Monitoring for Budgets

  • Daily cost summary: A Lambda job pulls from Cost Explorer and drops a JSON snapshot into S3. This gives me a daily pulse on where money’s going, broken down by service.

  • Budget threshold alerts: I’m using CloudWatch’s Cost Anomaly Detection to alert me if AWS detects something unusual (like a sudden jump in VPC or EC2 charges). This triggers an SNS notification that hits my Discord via Lambda.

  • CloudWatch usage alarms: These are for tracking specific usage metrics. I’m not actively using this yet, but I’ll likely set up alarms for Lambda invocations and duration in a future project where scaling risks exist.

Running Costs

Is this setup actually free? No. But it’s so close that it feels like it. And honestly, that’s good enough.

The real cost comes almost entirely from keeping a public IPv4 address attached. Storage is negligible, and the spot instance usually stays under 25 cents per month with my current usage.

Here are my current running costs (including the inflated total from keeping the public IP attached for about two days):

Running Cost Breakdown from S3-fed Lambda

Check out the live dashboard on my front page


This post is part of an ongoing blog series where I try to keep real infrastructure under $1/month. Stay tuned for more breakdowns and creative ways to stretch a cloud budget.