ShipDoc, part 2: moving off AWS before the account expired

The hackathon's AWS account ended on 18 October. ShipDoc moved to my homelab behind Caddy and a Cloudflare Tunnel, and the old domain now explains where it went.

On this page
  1. Move first, switch later
  2. From Lambda back to plain Docker
  3. Caddy and the tunnel
  4. What happens to shipdoc.org
  5. Trade-offs
  6. What came next

In part 1, ShipDoc went live on AWS: Lambda for the API, DynamoDB for results, Amplify for the website. The catch was that the AWS account came with the hackathon, and it expired on 18 October. When it did, everything on it would go with it.

A hackathon project listed on a CV should still work when someone clicks the link months later. So I moved it to my homelab server.

Move first, switch later

I didn't shut AWS down and then rebuild. Instead:

  1. Run a second copy of ShipDoc on the homelab, on new subdomains: lab.shipdoc.org for the website and lab-api.shipdoc.org for the API.
  2. Test it there while shipdoc.org was still served from AWS.
  3. When the account expired, point shipdoc.org at the homelab.

For a few days both versions were live, and nothing a visitor saw depended on the new one working.

From Lambda back to plain Docker

The cloud version was shaped around Lambda: a Mangum wrapper and DynamoDB, because Lambda runs short-lived copies side by side. The homelab doesn't need any of that. The repo already had a docker-compose.yml with two services:

  • api: the same FastAPI app, keeping results in a JSON file on a mounted volume, so they survive restarts;
  • web: the React site, built into static files and served by nginx.

Both are published on 127.0.0.1 only, never on the LAN:

ports:
  - "127.0.0.1:8000:8000"   # api
  - "127.0.0.1:5173:80"     # web

One detail matters when moving a Vite site. Vite bakes the API address into the JavaScript at build time, so changing an environment variable on the running container does nothing. The address has to be passed as a build argument (VITE_API_URL=https://lab-api.shipdoc.org) and the image rebuilt. The Android app is different: it has a setting for the API address, so it could be repointed without a rebuild.

Caddy and the tunnel

Every public site on the homelab follows the same path:

visitor → Cloudflare → Cloudflare Tunnel → Caddy on :80 → app on 127.0.0.1

The tunnel connects outwards from the server, so there are no open ports on my home router. Cloudflare handles HTTPS, so Caddy only speaks plain HTTP on loopback, with one block per hostname:

{
	auto_https off
}
http://lab.shipdoc.org {
	reverse_proxy localhost:5173
}
http://lab-api.shipdoc.org {
	reverse_proxy localhost:8000
}

What happens to shipdoc.org

When the account expired, the Amplify site and the Lambda API went with it. Rather than letting shipdoc.org show an error, I pointed it at the homelab too and gave it a third Caddy block that serves a single static page:

http://shipdoc.org, http://www.shipdoc.org {
	root * /srv/notice
	file_server
}

The page says plainly that the AWS deployment was taken down because the account expired, and links to the new website, the new API and the docs (which never depended on AWS: they're on Cloudflare). Old links in the README, the slides and the demo video still lead somewhere useful.

Trade-offs

AWS Homelab
Cost Hackathon credits, with an end date Electricity I already pay for
Uptime AWS's problem Mine, including power cuts
Scaling Lambda scales out on its own One laptop
Learning The AWS console Everything, end to end

For a demo with light traffic, the homelab wins. For a product with real users, it wouldn't.

What came next

Real projects depending on one laptop made me look at that laptop more critically. Two days later I asked for a full audit of the homelab, which turned into getting it ready to run for three months without me.