Skip to content

ADR-0003 Deploy target

Company standard deploy target — Docker containers running Bun, self-hosted.

Decision
Docker, self-hosted, on BunShip & run
Status
Accepted
Date
Scope
Company-wide default for all new apps

Our apps are React + TanStack Start (ADR-0001, ADR-0002) running on Bun (ADR-0035) — full-stack: SSR, server functions, and the API routes (ADR-0036) ship from one codebase. The deploy target is decided before database, rate limiting, and auth because it constrains all of them:

  • Database — edge isolates (Cloudflare Workers) can open TCP sockets to Postgres but can’t keep a long-lived connection pool, so they need a pooling proxy (Hyperdrive); our own containers pool directly.
  • Rate limiting — Vercel/Cloudflare offer edge-native primitives; on our own containers we build it ourselves.
  • Auth — session storage and middleware behavior differ per platform.
  • Cost model — per-request pricing (serverless) vs flat monthly (containers) changes what “cheap” means at our scale.

TanStack Start deploys to all candidates (official adapters/presets exist for each), so the choice is about the platform, not framework compatibility.

Framework-agnostic serverless: Node and edge functions, per-app git previews, zero-ops.

Pros

  • Zero infrastructure work; deploy on push, preview per PR
  • Node runtime available → direct Postgres connections work fine
  • Good DX: logs, analytics, rollback in the dashboard
  • Scales to zero and up without thought

Cons

  • Most expensive option at sustained traffic; per-request and bandwidth pricing
  • Function execution limits (duration, memory) shape app design
  • Vendor lock-in pressure around platform features
  • Team-wide cost grows with seats

TanStack Start deploys to Cloudflare via the Vite plugin; V8 isolate edge runtime.

Pros

  • Cheapest at scale; generous free tier
  • Globally low latency by default
  • Strong platform primitives for later ADRs: KV, D1, R2, Durable Objects, Queues, native rate limiting

Cons

  • No long-lived connection pool — Workers can open TCP sockets (pg works with nodejs_compat), but isolates are short-lived, so Postgres needs Hyperdrive in front for pooling. Biggest ripple effect on the DB ADR.
  • V8 isolate constraints: limited Node API surface, no long-running connections, CPU time limits
  • Isolates are their own runtime with their own quirks — not the Bun runtime we standardized on (ADR-0035)
  • Debugging/observability is weaker than a plain server process

3. Docker container (self-hosted), Bun runtime

Section titled “3. Docker container (self-hosted), Bun runtime”

The TanStack Start server running on Bun, packaged as a Docker image, run on our own infra (VM, Fly.io/Railway/Render, or any container host). This is the decision below.

Pros

  • A full server runtime: any database driver, any npm package Bun supports, websockets, long requests
  • Same runtime in dev, CI, and prod — the Bun version pinned in package.json on laptops and CI, the matching image tag in the Dockerfile (ADR-0035)
  • Flat, predictable cost; cheapest at small, steady load
  • No platform constraints — every later decision (DB, rate limiting, queues) has maximum freedom
  • Deployable anywhere; least lock-in; same image runs locally, in CI, and in prod

Cons

  • We own ops: process management, TLS, scaling, deploys, health checks
  • No per-PR previews or global edge without extra work
  • Scaling beyond one box is a project, not a checkbox

Docker container (self-hosted) running the Bun runtime is the company-standard deploy target for all new apps. (Amended 2026-11-13: this ADR first named Node LTS; ADR-0035 replaced it with Bun.)

The TanStack Start server runs on Bun inside a Docker image, deployed wherever we choose. The deciding factors: full runtime freedom for the decisions that follow (direct Postgres, custom rate limiting, any npm package), one pinned runtime across dev/CI/prod, flat predictable cost, no vendor lock-in, and one artifact that runs identically on a laptop, in CI, and in production. We accept owning ops in exchange for maximum freedom and control.

Easier — what this unlocks for later ADRs:

  • Database: direct TCP Postgres (or any DB) from the app container — no proxy, no edge constraint
  • Rate limiting: we build it in-app as middleware — full control, no platform dependency
  • Auth: any session strategy works (cookies, DB sessions, JWT) — no runtime limits
  • Local dev = prod: docker compose up mirrors production exactly, Bun version pinned in package.json and the Dockerfile image tag (ADR-0035)

Harder — what we now own:

  • TLS termination, health checks, rolling deploys, and restart policy are our responsibility (compose / reverse proxy / orchestrator)
  • Horizontal scaling requires an explicit plan when a product outgrows one box
  • Per-PR preview environments need to be built if we want them
  • We own the Bun image and its upgrade cadence (ADR-0035); its tag must match packageManager in package.json, and Renovate bumps both in one pull request (ADR-0016)

Given up: Vercel’s zero-ops DX and Cloudflare’s free global edge. Accepted — control, runtime consistency, and cost predictability matter more to us than managed convenience at this stage.