make build produces a single Go binary with the React SPA embedded via go:embed. The binary serves both the API and the frontend on a single port — no nginx or separate frontend container required.
To disable the embedded frontend (e.g., when serving the SPA separately), call engine.SetFrontendFS(nil) in your extensions.
The server is stateless. Scale horizontally behind a load balancer. All state lives in PostgreSQL.
- PostgreSQL 16+ (shared across instances)
- Typst CLI binary (installed per container)
Never put secrets in app.yaml. Use env vars:
DOC_ENGINE_DATABASE_PASSWORD=xxx
DOC_ENGINE_AUTH_JWKS_URL=https://...
DOC_ENGINE_INTERNAL_API_API_KEY=xxx# Build (unified: frontend + backend in single image)
docker build -t pdf-forge .
# Run with external PG
docker-compose up --scale postgres=0
# Custom PG port
PG_PORT=5433 docker-compose upThe root Dockerfile is a multi-stage build: Node.js (frontend) → Go (backend with embedded SPA) → Alpine (runtime with Typst).
livenessProbe:
httpGet:
path: /health
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080typst.max_concurrentis per instance — total cluster capacity = instances x max_concurrentdatabase.max_pool_sizeis per instance — ensure PGmax_connections>= sum of all instances' pool sizes- Image cache is local to each instance. With persistent cache dir on shared volume, instances can share cache
- Template cache is in-memory per instance (LRU). No cross-instance sharing needed.
- CPU: Each concurrent render uses ~1 Typst CLI process. Set
typst.max_concurrent≤ available CPU cores. - Memory: Base usage is low. Memory scales with concurrent renders and template cache size.
- Disk: Only needed if
typst.image_cache_diris set for persistent image caching.
| Endpoint | Purpose |
|---|---|
GET /health |
Liveness — app is running |
GET /ready |
Readiness — app can serve requests (DB connected, Typst available) |
On startup, the engine runs preflight checks via make doctor or automatically:
- Typst CLI binary is accessible
- PostgreSQL connection is valid
- Database schema is up to date
- Auth configuration is valid (JWKS URL reachable or dummy mode)