← Selected projects
/ Case study 2026

Self-hosted Gitea on AWS Lightsail

A single-VPS Git server that hosts my code, builds my portfolio, and replicates itself nightly to GitLab — provisioned end-to-end in Terraform, fronted by Caddy with auto-HTTPS, for about ten dollars a month.

Year
2026
Cloud
AWS Lightsail
Stack
Terraform · Docker · Caddy · Gitea
Cost
~$10 / month
/ 01

Context

GitHub is free and works fine. Self-hosting Git is a deliberate choice — to own the substrate, control the runner, and learn the moving parts. The constraint is that self-hosted can't mean fragile. If the box dies, the work has to come back.

The artifact is a Terraform-managed Gitea instance on AWS Lightsail, sized for solo use, with a CI runner that deploys this portfolio site to S3 + CloudFront on every push to main.

/ 02

Constraints

Budget
Ten dollars a month, all-in. No managed Git, no managed CI, no per-build minutes meter ticking.
Reproducibility
The whole stack — VPS, IP, firewall, DNS, snapshots — defined in a Terraform module separate from the portfolio's S3 + CloudFront stack. State backend reused, scope isolated.
TLS without ceremony
Caddy provisions a Let's Encrypt certificate on first boot and renews silently. No manual cert lifecycle.
Backups off the host
Daily Lightsail snapshots cover the host. A push-mirror to GitLab covers the data — every push is on three machines within seconds.
SSH separation
OS administration on port 22, Git over SSH on port 2222. One container, one host SSH daemon, no overlap.
/ 03

Architecture

The whole thing is six Terraform resources on the AWS side and one docker compose stack on the box.

Compute
Lightsail small_2_0 instance — 2 vCPU, 2 GB RAM, 60 GB SSD, Ubuntu 22.04. cloud-init installs Docker on first boot.
Networking
Static public IP attached to the instance. Firewall opens 22 (admin SSH), 80 / 443 (Caddy + LE), and 2222 (Gitea SSH) — nothing else.
DNS
An A record for git.josephvu.me in the same Route 53 zone as the apex, looked up via a data source so the two stacks stay decoupled.
Reverse proxy
Caddy 2 in a container, terminating TLS on 443 and reverse-proxying to Gitea on the internal Docker network. Auto-HTTPS via Let's Encrypt.
Application
Gitea 1.22 with SQLite as the database — lighter than Postgres for solo use and cheaper to back up.
CI runner
Gitea Actions runner on the same host, registered to the Gitea instance. Builds Astro, syncs to S3, invalidates CloudFront — running under a least-privilege IAM user scoped to the site bucket and one distribution.
Backup
Daily Lightsail auto-snapshots at 04:00 UTC. Push mirror to a private GitLab repo via a deploy token, fired on every commit.
/ 04

Outcomes

$10
All-in monthly cost
6
Terraform resources
TLS
Auto via Let’s Encrypt
1d
Snapshot cadence
  • Whole stack reproducible from a clean AWS account with terraform apply against the infra/aws-gitea module.
  • Push to main on Gitea triggers an Actions job that builds the site and ships it to CloudFront — same ergonomics as GitHub Actions, on infrastructure I control.
  • Survives instance loss in two ways: Lightsail snapshot for the box, GitLab mirror for the data.
/ 05

Reflections

Running the runner on the same host as the Git server is cheap and convenient, and it's also the failure mode that bites first — a runaway build can saturate the box and lock me out of pushing the fix. A second instance for the runner, or migrating to ephemeral runners, is the next move when the bill can absorb it.