Why this option exists
The default split stack (e.g. Vercel + Fly.io + Neon) optimises for speed and managed operations. A single-host layout optimises for one bill, one SSH target, predictable networking, and full control — appropriate for early traffic, staging, or teams comfortable running containers on a VM.
Goals and non-goals
- Goals — One public origin (or one domain + subdomains); reproducible deploy via Compose; secrets only on host or secret store; health checks; documented rollback; DB either managed Neon (simplest) or Postgres in Compose (full self-host).
- Non-goals (v1 of this topology) — Multi-region HA, Kubernetes, or auto-scaling beyond vertical resize of the VM. Those can follow if traffic demands.
Target topology
Internet → reverse proxy (Caddy or nginx) on ports 80/443
→ internal Docker network → web (Next start)
and api (Node + Hono, current apps/api).
Static HTML docs ship with the Next build under
/docs; no separate docs container required.
┌─────────────────────────────────────┐
HTTPS :443 │ Reverse proxy (Caddy / nginx) │
────────────────► │ · TLS (Let’s Encrypt) │
│ · / → next:3000 │
│ · /v1/* → api:3001 (path option) │
└──────────────┬──────────────────────┘
│ Docker bridge network
┌──────────────┴──────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ web (Next) │ │ api (Hono) │
│ :3000 │ │ :3001 │
└───────────────┘ └───────┬───────┘
│
┌─────────────────────────────┴────────────────────────────┐
▼ (optional) ▼
┌─────────────────┐ ┌─────────────────┐
│ postgres:5432 │ OR keep Neon only — API uses │ Neon (managed) │
│ (volume backup) │ DATABASE_URL to Neon host │ DATABASE_URL │
└─────────────────┘ └─────────────────┘
Routing: one origin vs API subdomain
| Pattern | Browser NEXT_PUBLIC_API_URL |
Notes |
|---|---|---|
| Path proxy (recommended for “one place”) | https://ardhkumbh2027.com (same as site) |
Proxy must forward /v1/* to the API container. Next
and API share one TLS cert. CORS becomes trivial for browser calls
to /v1.
|
| Subdomain | https://api.ardhkumbh2027.com |
Simpler proxy rules; configure CORS on Hono for the web origin. Mobile apps can use the same API URL. |
Compose services (planned)
| Service | Image / build | Role |
|---|---|---|
proxy |
Caddy official image or nginx | TLS termination, HTTP/2, routing to web and api. |
web |
Build from apps/web (multi-stage: install, build, run next start) |
Production Next.js; sync docs at build as today. |
api |
Build from apps/api (npm run build → node dist/index.js) |
Hono on Node 20; DATABASE_URL from env. |
db (optional) |
postgres:16 + named volume |
Only if leaving Neon; otherwise omit and point API at Neon. |
Environment variables (host / compose)
Never bake secrets into images. Use a root
.env on the server (mode 0600) or a secret manager
later.
| Variable | Service | Purpose |
|---|---|---|
DATABASE_URL |
api | Postgres (Neon URI or internal postgres://db:5432/...). |
NEXT_PUBLIC_API_URL |
web (build arg + runtime) |
Public API base the browser will call — same origin if using path
proxy, or https://api… if using subdomain.
|
NODE_ENV=production |
web, api | Standard production mode. |
PORT |
api | Listen port inside container (e.g. 3001); proxy maps to it. |
| Stripe / Resend / etc. | api (later) | As checkout and notifications land; same pattern as today’s API-only secrets. |
Phased rollout (plan)
- Phase 0 — Documentation (this page) — Agree topology, routing choice (path vs subdomain), and whether Postgres stays on Neon or moves in-Compose.
-
Phase 1 — Container definitions — Add
docker/Dockerfile.web,docker/Dockerfile.api, anddocker-compose.ymlat repo root; localdocker compose upsmoke test against Neon. - Phase 2 — VM provisioning — EC2 (e.g. Amazon Linux 2023 or Ubuntu LTS), security group (80/443 from world, 22 from admin IP only), install Docker Engine + Compose plugin, attach EBS if needed.
- Phase 3 — TLS and DNS — Point A/AAAA (or CNAME) to the VM; Let’s Encrypt via Caddy or certbot; renewals automated.
-
Phase 4 — Deploy pipeline — GitHub Actions (or manual):
build images, push to GHCR or ECR, SSH pull +
compose up -d, or use a tiny deploy script. Blue/green optional later. -
Phase 5 — Operations — Log rotation (
json-filedriver limits), unattended upgrades policy, off-site DB backups (critical if self-hosted Postgres), uptime check on/v1/health.
Cutover from split hosting
- Stand up single-host staging with the same
DATABASE_URLas production (read replica or copy — policy decision). - Run E2E: catalog, cart, checkout stub, docs at
/docs. - Lower DNS TTL; switch A/AAAA; monitor errors; keep old Vercel/Fly as instant rollback until stable.
Risks and mitigations
| Risk | Mitigation |
|---|---|
| Single point of failure | Accept for early stage; snapshot VM + volume; keep Neon backups if using managed DB. |
| Disk-full / log bloat | Compose log limits; monitor disk; rotate application logs if added. |
| Next + monorepo tracing | Build web image from monorepo context with correct outputFileTracingRoot equivalent in Docker build (documented when Dockerfiles land). |
Implementation note
The running API in this repo is Node 20 + Hono
(@hono/node-server), not Bun — Docker images should use an
official node:20-alpine (or slim) base unless the stack
changes.
Related documentation
- Architecture — monorepo and webhook ingress.
- Backend API — Hono routes and env.
- Database — Drizzle and Neon.
- Delivery & operations — sprints and CI/CD themes.