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.
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.
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.
Architecture
The whole thing is six Terraform resources on the AWS side and
one docker compose stack on the box.
- Compute
-
Lightsail
small_2_0instance — 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.mein 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.
Outcomes
-
Whole stack reproducible from a clean AWS account with
terraform applyagainst theinfra/aws-giteamodule. -
Push to
mainon 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.
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.