DEV Community

Cover image for I Cut My AWS Hosting Bill from ~$45/mo to ~$8 by Consolidating Three Apps on One Lightsail Box
David Shibley
David Shibley

Posted on

I Cut My AWS Hosting Bill from ~$45/mo to ~$8 by Consolidating Three Apps on One Lightsail Box

How I moved portfolio, krogerCart, and chat-bot off separate EC2 instances onto a single $5 Lightsail instance with Docker and Caddy and what broke along the way.


The problem: paying for servers, not traffic

I had three small web apps running on three separate EC2 instances:

App What it does Rough monthly cost
portfolio React SPA + Node API (Featherless chat) ~$15
krogerCart Express API + auth (Cognito, Stripe, Kroger) ~$15
chat-bot Express + DynamoDB chat UI ~$15

Total: ~$45/month for sites that get almost no traffic.

EC2 pricing is mostly uptime, not requests. Three always-on boxes for hobby projects is like renting three parking spots when you only ever use one car at a time.

A fourth site, organic-bass, was already on S3 + CloudFront for pennies. That was the hint: static sites shouldn't sit on EC2, and dynamic apps don't each need their own box at this scale.


The goal: one box, three domains

Instead of three EC2 instances, I wanted:

shibleyrecords.com          ─┐
www.grocerygoblin.com       ─┼──►  One Lightsail instance
chat.grocerygoblin.com      ─┘     Docker + Caddy
Enter fullscreen mode Exit fullscreen mode

Target cost: ~$5–8/month (Lightsail + Route 53 + a bit of bandwidth).

What stays separate: organic-bass on S3/CloudFront (~$1–3/mo). No reason to touch that.


Architecture

Each app stays in its own repo with its own Dockerfile. A small hosting/ repo ties them together:

hosting/
├── docker-compose.yml   # builds all three apps + Caddy
├── Caddyfile            # TLS + routing by hostname
└── env/
    ├── portfolio.env
    ├── krogercart.env
    └── chatbot.env
Enter fullscreen mode Exit fullscreen mode

Sibling directory layout on the server:

~/projects/
  portfolio/
  krogerCart/
  chat-bot/
  hosting/
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

Three app services expose ports only inside Docker (8080, 8000, 4000). Caddy is the only service that publishes 80 and 443 to the internet:

services:
  portfolio:
    build: ../portfolio
    expose: ["8080"]
    env_file: ./env/portfolio.env
    environment:
      TRUST_PROXY: "1"

  krogercart:
    build: ../krogerCart
    expose: ["8000"]
    env_file: ./env/krogercart.env
    environment:
      TRUST_PROXY: "1"

  chatbot:
    build: ../chat-bot
    expose: ["4000"]
    env_file: ./env/chatbot.env

  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
Enter fullscreen mode Exit fullscreen mode

Caddyfile

Caddy terminates TLS (Let's Encrypt, automatic renewals) and reverse-proxies by hostname:

shibleyrecords.com, www.shibleyrecords.com, portfolio.shibleyrecords.com {
    reverse_proxy portfolio:8080
}

www.grocerygoblin.com {
    reverse_proxy krogercart:8000
}

chat.grocerygoblin.com {
    reverse_proxy chatbot:4000
}
Enter fullscreen mode Exit fullscreen mode

No nginx config files. No Certbot cron jobs. One process handles HTTPS for everything.


Why Lightsail instead of EC2?

For low-traffic personal projects:

EC2 (×3) Lightsail ($5)
Billing Per instance + EBS + data transfer Flat ~$5/mo
Static IP Elastic IP (free if attached) Included
Firewall Security groups Simple rules in UI
Mental overhead Higher Lower

I didn't need ECS, Fargate, or an ALB (~$16/mo alone). Consolidation beat "serverless migration" on effort for this workload.


DNS: one IP, two zones, several hostnames

This tripped me up more than Docker.

All A records point to the same Lightsail static IP (e.g. 184.33.2.158). Caddy routes by domain name, not by IP.

Two Route 53 hosted zones (one per domain):

shibleyrecords.com

Name Type Value
@ A Lightsail IP
www A Lightsail IP
portfolio A Lightsail IP (optional)

grocerygoblin.com

Name Type Value
www A Lightsail IP
chat A Lightsail IP

Critical: the four nameservers at your domain registrar must exactly match the NS record inside each hosted zone. I had three correct and one wrong (ns-375... vs ns-176...); DNS partially broke until I fixed it.

Don't delete hosted zones when you only mean to change A records. I learned that the hard way. Deleting the zone while the registrar still points at Route 53 nameservers leaves the domain in limbo (SERVFAIL / empty answers).

Remove old CloudFront CNAMEs. www.shibleyrecords.com still pointed at CloudFront, so Let's Encrypt's HTTP challenge hit CloudFront and returned 404. Replace with a plain A record to Lightsail.


Migration steps (the happy path)

  1. Provision Lightsail $5 plan, attach a static IP, open ports 80 and 443.
  2. Install Docker + Compose on the instance (Amazon Linux didn't ship docker compose out of the box for me).
  3. Clone repos and copy .env files into hosting/env/.
  4. Fix Route 53 A records → Lightsail IP; nameservers match the hosted zone.
  5. Build and start: docker compose up -d --build (see below, this takes a while).
  6. Smoke test on the server (not your laptop):
   curl -H "Host: www.grocerygoblin.com" http://127.0.0.1/api/health
Enter fullscreen mode Exit fullscreen mode
  1. Restart Caddy so it picks up TLS once DNS propagates:
   docker compose restart caddy
Enter fullscreen mode Exit fullscreen mode
  1. Terminate the three old EC2 instances once HTTPS works.

Cognito, Stripe, and Kroger redirect URIs didn't need changes; domains stayed the same.


Gotchas I hit (so you don't have to)

1. Testing from the wrong machine

curl ifconfig.me on my Mac returned my home IP. I thought DNS was wrong. On the Lightsail SSH session, it correctly returned 184.33.2.158.

Always run server diagnostics over SSH on the instance.

2. You can't curl :8080 on the host

Apps only expose ports inside Docker, they aren't published on 127.0.0.1:8080 on the host. Test through Caddy on port 80 with a Host header, or exec into the container.

3. First docker compose build takes forever

A $5 Lightsail box has 512 MB–1 GB RAM. Running npm ci and frontend builds for three Node apps can take 20–45 minutes on first build. It looks frozen; it's often just swapping.

Mitigations:

  • Add a 2 GB swap file before building.
  • Build one service at a time: docker compose build portfolio, then krogercart, then chatbot.
  • Later deploys: docker compose up -d --build krogercart for single-app updates.

4. Let's Encrypt fails if DNS is wrong

Caddy logs showed the real story:

  • NXDOMAIN hostname has no DNS record (portfolio.shibleyrecords.com).
  • Connection timeout to old EC2 IP A record still pointed at a dead instance.
  • 404 from CloudFront www still on a CDN CNAME, not Lightsail.

Fix DNS first; then restart Caddy.

5. docker compose vs docker-compose

On my Lightsail AMI, docker compose wasn't available until I installed the Compose plugin manually.


What we didn't do (on purpose)

  • ECS + Fargate + ALB fixed costs exceed savings at this traffic level.
  • Lambda rewrite high effort for modest gain vs consolidation.
  • One EC2 per app "because isolation" operational overhead without benefit here.

Results

Before After
3× EC2 ~$15 each 1× Lightsail ~$5
3× TLS/nginx setups 1× Caddy
~$45/mo ~$6–11/mo (incl. Route 53 + organic-bass S3)

Same apps, same domains, same OAuth callbacks. Less money, less to babysit.


When this pattern fits

Good fit:

  • Several small Node/Docker apps
  • Low traffic, always-on is fine
  • You control DNS and can point multiple domains at one IP

Bad fit:

  • One app needs heavy CPU/RAM isolation
  • Strict compliance requiring separate networks
  • Traffic spikes that need autoscaling

Takeaways

  1. Separate EC2 per hobby app is usually waste you're paying for 720 hours × N instances.
  2. Caddy + Docker Compose multi-host routing is a simple consolidation pattern.
  3. DNS is half the migration one IP, many names; nameservers must match the hosted zone; don't leave CloudFront CNAMEs behind.
  4. Test on the server with Host headers before blaming DNS.
  5. First build on a tiny VPS is slow plan for swap and patience.

The hosting/ docker-compose + Caddy setup is boring infrastructure in the best way: one $5 box, three domains, HTTPS that renews itself, and ~$35/month back in my pocket.


If you're doing something similar, start with DNS and a single curl through Caddy on localhost before you chase HTTPS smoke tests from your laptop. It'll save you an afternoon.`

Top comments (0)