Skip to content

Build and Deploy

Vite Build Modes

Command import.meta.env.MODE import.meta.env.PROD API base WS base
pnpm dev development false /api (Vite proxy → :8080) /ws (Vite proxy → :8081)
pnpm build:staging staging true https://api.ftljeta.cloud/api wss://ws.ftljeta.cloud
pnpm build production true https://prd-api.ftljeta.cloud/api wss://prd-ws.ftljeta.cloud

All URLs are hardcoded constants in ftl-frontend/src/lib/api.ts and ftl-frontend/src/hooks/useWebSocket.ts. Dead branches are tree-shaken by Vite.

Required Build Argument

Arg Where used Effect if missing
VITE_GOOGLE_CLIENT_ID Docker ARG, passed as ENV to the Vite build GIS is disabled; a console warning is emitted; the sign-in button does not render

Pass it at docker build time:

docker build \
  --build-arg VITE_GOOGLE_CLIENT_ID=<your-client-id> \
  -t ftl-frontend:latest .

Dockerfile

Defined at ftl-frontend/Dockerfile. Two-stage build:

Stage 1 — builder (node:20-alpine)

  1. Enable pnpm@9 via corepack.
  2. Copy package.json and pnpm-lock.yaml; run pnpm install --frozen-lockfile (layer-cached separately from source).
  3. Accept ARG VITE_GOOGLE_CLIENT_ID and export as ENV so Vite can read it.
  4. Copy source; run pnpm build (production mode by default).

Stage 2 — runtime (caddy:2-alpine)

  1. Copy dist/ from builder to /usr/share/caddy.
  2. Copy Caddyfile to /etc/caddy/Caddyfile.
  3. Expose port 80. Caddy reads the Caddyfile on start.

Caddyfile

Defined at ftl-frontend/Caddyfile. Caddy listens on :80.

Path Destination Notes
/_frontend_health Inline 200 ok Wrapped in handle so the SPA fallback does not shadow it
/api/* {env.API_UPSTREAM}:443 TLS dialed; Host header set to upstream FQDN; Origin stripped
/ws/* {env.WS_UPSTREAM}:443 Same TLS setup; Caddy auto-upgrades Upgrade: websocket requests
Everything else /usr/share/caddy (static) try_files {path} /index.html for SPA client-side routing

Both reverse-proxy blocks add header_up X-Forwarded-Proto https. This reaches the Go api-server's ProxyHeader setting, which activates HSTS and correct cookie Secure flag behaviour behind TLS termination.

Decision: upstreams are supplied as plain hostnames (no scheme) in API_UPSTREAM / WS_UPSTREAM because Caddy's reverse_proxy does not parse URL schemes in env-var-substituted arguments. The transport http { tls } block tells Caddy to dial TLS on port 443.

Runtime Environment Variables

Set these in the Container App environment (not at build time):

Variable Example value Purpose
API_UPSTREAM api.ftljeta.cloud FQDN of the api-server Container App
WS_UPSTREAM ws.ftljeta.cloud FQDN of the ws-server Container App

Warning

Do not include https:// or a trailing slash in API_UPSTREAM / WS_UPSTREAM. Caddy appends :443 directly to the value.

Vite Dev Proxy

Configured in ftl-frontend/vite.config.ts:

/api  → http://localhost:8080
/ws   → ws://localhost:8081 (WebSocket upgrade)

The dev server also sets Cross-Origin-Opener-Policy: same-origin-allow-popups so Google OAuth's popup can call window.opener.postMessage back without being blocked by Chrome's cross-origin isolation policy.