Skip to Content
InfraGCP migration analysis (2026)

GCP migration analysis — near-zero cost alternatives

Decision document. Evaluates leaving GCP entirely to close the gap to $0/mo for Oter + Negodex together. This supersedes the “stay on GCP; no platform migration” stance in gcp-cost-cuts.md — that stance was correct for May 2026 waste-cutting; this doc reopens the question for a different goal (near-zero cost, ops burden accepted).

This is analysis, not a queued migration. Execution would be a separate plan.

Context

The May 2026 cost cuts (see gcp-billing.md) brought monthly GCP spend to ~$22–38 (Scenario A: April total was the whole billing account) or ~$57–67 (Scenario B: Negodex SQL was extra). Cloud SQL now dominates at ~$46–52/mo; every other SKU is effectively at free tier.

The question this doc answers: is it worth leaving GCP entirely for a near-$0 target? Chosen profile:

  • Cost target: $0–5/mo (aggressive)
  • Ops tolerance: willing to own a VPS
  • Scope: Oter + Negodex together (they share oter-db — splitting them recreates the ~$50/mo second-instance problem, so they migrate as one unit)
  • Firebase FCM: stays on the existing oter-8c4e4 project regardless — free tier, host-agnostic

Current state (verified 2026-07-07)

ComponentToday on GCPMonthly
Ktor server (oter)Cloud Run, 1 vCPU / 512 MiB, scale-to-zero~$3–5
Next.js web (oter-web)Cloud Run, scale-to-zero~$2–3
Internal docs (oter-docs-internal)Cloud Run + Cloud IAP~$1
Nest API (Negodex api)Cloud Run, scale-to-zero (project negodex)~$1–2
Postgres (oter-db)Cloud SQL db-custom-1-3840, 10 GB, shared by both apps~$46–52
AttachmentsGCS bucket oter-attachments-prod (STANDARD → NEARLINE @ 90d → COLDLINE @ 365d)~$0–2
SecretsSecret Manager, mounted at deploy~$0
CI/CD authWorkload Identity Federation~$0
Logging1-day retention + severity exclusions~$0 (free tier)
Total (Scenario A / B)~$22–38 / ~$57–67

Cloud SQL is ~80% of the bill. Every other lever is already pulled.

Recommendation

Move both apps to a single Hetzner ARM VPS (CAX11, ~$4/mo) in Ashburn US-East, running Docker Compose, with Cloudflare R2 for attachments and Cloudflare Access for the internal docs site.

Why this over the alternatives (details in Alternatives considered):

  • Only option that plausibly lands under ~$6/mo total for both apps.
  • Ashburn keeps LATAM latency in the same region class as today’s us-east1 (delta <20ms).
  • Zero reclamation risk (unlike Oracle Always Free).
  • Runs on infrastructure we already understand: Docker Compose, Caddy, Postgres.

Target topology

Hetzner CAX11 (Ashburn) — 2 vCPU ARM, 4 GB RAM, 40 GB SSD, 20 TB egress └── Docker Compose ├── caddy — TLS + reverse proxy (auto Let's Encrypt) ├── postgres:18 — one instance, two databases (oter_prod + myecommercedb), volume-mounted ├── oter-server — Ktor container from GHCR ├── oter-web — Next.js container from GHCR ├── negodex-api — Nest container from GHCR ├── oter-docs — nginx serving static docs, behind Cloudflare Access (free) └── (optional) uptime-kuma for self-hosted monitoring Cloudflare R2 — attachments bucket (S3 API, $0 egress, 10 GB free) Cloudflare Pages — wasmJS + static docs (already free-tier friendly) GitHub Container Registry — image storage Firebase FCM — unchanged (still push notifications)

Cost projection

LineMonthly
Hetzner CAX11 ARM~$4.00
Hetzner Cloud backups (20% surcharge, optional)~$0.80
Cloudflare R2 storage (~5 GB attachments)~$0.00 (10 GB free)
Cloudflare Access / Pages / DNS~$0.00
GHCR (small private images)~$0.00
Domain (already owned)
Total~$4.80/mo

That’s a ~78–92% reduction vs current post-optimization spend, and ~95% vs the April 2026 baseline.

Alternatives considered

OptionCostWhy not
Oracle Cloud Always Free (Ampere A1: 4 vCPU, 24 GB RAM)$0/moHighest saving, but Oracle has a well-documented history of reclaiming idle Always-Free ARM instances with 24h notice. Fine for a hobby app; not acceptable when Negodex runs on it. Reasonable fallback if Hetzner ever raises prices meaningfully.
Contabo VPS S (4 vCPU, 8 GB)~$5/moCheaper per core than Hetzner but Germany-only (higher LATAM latency), history of shared-CPU noisy-neighbor issues, weaker network reputation.
Fly.io (Machines + Managed Postgres)~$8–12/moPreserves scale-to-zero and matches Cloud Run architecturally. Managed Postgres tier is still $19/mo for anything non-trivial, and reliability track record (2022–2023 outages) is weaker than a dedicated VPS. Saves less than Hetzner.
Neon / Supabase for Postgres only, keep Cloud Run~$10–25/moLowest-effort option (~1 day of work): pg_dump → new host, update JDBC URL, done. At $19/mo Neon Launch or $25/mo Supabase Pro it isn’t “near-zero” — it fits the “best cost/effort ratio” profile that was explicitly deprioritized here.
AWS Lightsail / DigitalOcean Droplet~$5–7/moComparable to Hetzner on price but worse per resource unit. No decisive advantage.

What actually breaks (change surface)

The migration doc for future-us should enumerate these; not implementation steps, just the surface area.

  • Postgres connectivity. oter-db connection string switches from the Cloud SQL socket factory to a standard JDBC URL. server/src/main/kotlin/com/esteban/ruano/utils/Config.kt already handles both — it has a /cloudsql/ socket-factory branch (line ~102) and a standard-JDBC branch. This is essentially an env-var change (DB_HOST / DB_URL). Same for Negodex Nest API.
  • Attachments. server/src/main/kotlin/com/esteban/ruano/service/GcsFileStorageService.kt becomes an S3-client implementation pointed at R2. R2 exposes an S3-compatible API, so software.amazon.awssdk:s3 with a custom endpoint works. Lifecycle rules (STANDARD → NEARLINE → COLDLINE) become R2 lifecycle rules (fewer tiers; at single-digit GB the difference is <$1/mo).
  • Secrets. Secret Manager mounts (--set-secrets in publish-server-release.yml) become .env files on the VM, owned by root, read by docker compose. Rotate via SSH + docker compose up -d. Trade-off: no audit trail, no per-service scoping.
  • CI/CD. GitHub Actions currently uses WIF → gcloud run deploy. New flow: build container → push to GHCR → SSH to the VM → docker compose pull && docker compose up -d. Requires an SSH deploy key stored as a repo secret. Three workflows to rewrite: publish-server-release.yml, publish-web-release.yml, publish-docs-release.yml.
  • Internal docs auth. Cloud IAP → Cloudflare Access (free tier: up to 50 users, Google/GitHub SSO). Nearly at-parity feature-wise.
  • Firebase / FCM. No code change; NotificationService.kt already reads credentials from a file path. Credentials JSON just moves from Secret Manager to a mounted file.
  • Backups. Cloud SQL’s 7-day retained backups + 7-day PITR become pg_dumpall cron → R2 (encrypted). Hetzner snapshots (~$0.40/mo) cover the VM. No PITR. Accept RPO of ~24h. A restore drill should be documented and run once before cutover.
  • Observability. Cloud Logging → docker compose logs + optional Loki Cloud free tier (already wired in logback.xml). Uptime monitoring via Uptime Kuma on the same VM or Better Stack free tier.
  • DNS + TLS. Move DNS to Cloudflare (or keep current), point A records at the VM, Caddy handles Let’s Encrypt automatically.
  • Dependency cleanup. server/build.gradle.kts can drop com.google.cloud.sql:postgres-socket-factory and com.google.cloud:google-cloud-storage. Keep firebase-admin.

Risk register

RiskMitigation
Single point of failure — VM reboot ≈ 1 min downtime for both appsAcceptable for personal life-OS + small ecommerce. Document a maintenance-window policy. Consider a $4/mo standby that pulls the same images if downtime becomes user-visible.
No managed PITR on PostgresNightly encrypted pg_dumpall to R2 + a weekly restore drill. RPO 24h, RTO ~15 min.
LATAM latency slightly worse from Ashburn vs us-east1Same region class (US East), delta typically <20ms. Nürnberg is a fallback with different tradeoffs.
Ops burden creep — OS patches, Postgres major upgradesBudget one Sunday per quarter for apt upgrade + docker compose pull. Pin Postgres major, upgrade deliberately. Caddy handles TLS renewal.
Losing WIF audit trail / IAM granularityAccept it. Rotate SSH keys yearly, restrict management surfaces behind Cloudflare Access.
Cloudflare R2 free-tier changesFalls back to $0.015/GB storage + $0 egress — still cheap.
Attachments bucket lifecycle (NEARLINE/COLDLINE savings) doesn’t map exactlyR2 has fewer tiers; at current scale <$1/mo difference.

Trigger conditions for revisiting

Migrate now if:

  • Current spend consistently exceeds ~$30/mo (Scenario B is live) and no further GCP-side cut is available.
  • We’re comfortable owning weekly ops for both apps.

Defer migration if:

  • Scenario A is confirmed by billing export and monthly spend stays under ~$25.
  • Traffic grows enough that a $4 VPS won’t fit — at that point the analysis changes (managed Postgres becomes worth its price).
  • We plan to add more apps to the shared Cloud SQL instance (amortizes the fixed cost further and pushes the crossover further out).

Revisit within a year regardless if:

  • Hetzner raises prices >20%.
  • Oracle Always Free ARM reclamation policy changes materially (would make it a genuine alternative).
  • We stop needing Negodex — the shared-SQL argument collapses and Neon Launch ($19) becomes the obvious answer for Oter alone.

← Infrastructure docs · ← Docs index