Notes

Running Next.js on Cloud Run from Nairobi

Three services, one load balancer, no service-account keys, and the region choice that mattered more than any of it. What we changed after the first month of bills.

Rentisha is three repositories deployed as three Cloud Run services in one Google Cloud project, in africa-south1. An API, a landlord dashboard, and a tenant portal, each on its own subdomain.

None of that is unusual. The parts worth writing down are the ones we got wrong first.

Region is a latency decision and a billing decision

Running in africa-south1 rather than a European region is the single change with the clearest user-visible effect. The users are in Nairobi, the payment rails are in Nairobi, and the tax gateway is in Nairobi. Every round trip that leaves the continent and comes back is spent for nothing.

The billing lesson was less obvious and more expensive. Early deployments left duplicate services running in a European region, not serving traffic, not in any runbook, and quietly billing every month alongside an orphaned VM and a handful of reserved IP addresses nobody had released. Removing that, plus setting a cleanup policy on Artifact Registry so build images stop accumulating forever, took roughly a hundred and fifty dollars a month off the bill.

For a bootstrapped company that is a meaningful number, and the general shape of the problem is worth naming: cloud spend rarely grows because the thing you are running is expensive. It grows because of things you stopped running but never deleted. A quarterly pass over every region in the project, not just the one you think you use, is worth more than most optimisation work.

One load balancer, not three domain mappings

The three custom domains are fronted by a single global external Application Load Balancer sharing one static IP, not by individual Cloud Run domain mappings, and not by three separate load balancers.

It is a small architectural choice that pays back constantly. One place where TLS certificates live, one place to reason about routing, one IP to point DNS at, and one thing to check when a domain misbehaves. The alternative spreads the same configuration across three mechanisms that fail in three different ways and are verified by three different commands.

The associated cost is that the load balancer becomes a shared dependency for all three services, so it deserves the documentation it gets: the deployment runbook records the exact commands to re-verify the certificate chain and per-domain status, because that is precisely the knowledge nobody has at hand during an outage.

No service-account keys anywhere

Deploys are automatic: push to main, GitHub Actions builds the image with Cloud Build and runs gcloud run deploy. No manual deploys.

Authentication uses Workload Identity Federation, which means there are no service-account key files in the repositories, in the CI secrets, or on anybody's laptop. GitHub issues a short-lived OIDC token on each run; Google Cloud is configured to trust that token, only from repositories under our organisation, and exchanges it for temporary credentials that impersonate a deploy service account.

The setup cost is real, and it is front-loaded in an unforgiving way: until the trust relationship is configured, every workflow run fails at the authentication step. That is documented as expected rather than as a bug, because the first person to hit it will otherwise assume the pipeline is broken.

What you get is the elimination of an entire class of incident. A long-lived JSON key is a credential that can be committed, copied into a chat, or left in a container image, and rotating it is a task somebody has to remember. A token that expires in minutes is not worth stealing.

Build machine size is a cheap lever

The Cloud Build config asks for an eight-vCPU worker with a one-line justification: monorepo installs. Dependency installation in a pnpm workspace parallelises well, and build minutes on a bigger machine cost about the same as more minutes on a smaller one, while the developer waiting on the deploy costs considerably more.

It is the least clever entry in the config file and one of the highest-return.

Cold starts and where they actually hurt

Cloud Run scales to zero, which is exactly what a bootstrapped company wants and exactly what makes cold starts a live concern. For the web services this is mostly a non-issue: the first request after idle is slow, and every subsequent one is not.

Where it bit us was somewhere less obvious: background workers on an otherwise-idle instance, where the database connection pool has gone cold at the same time as everything else. That produced a class of timeout failure that had nothing to do with the query being slow, and it is worth its own discussion, and it is covered in the piece on choosing dull infrastructure.

Keep reading