Cloud Run
On Google Cloud, Long Tail runs as two Cloud Run services from one image: a public API service and a private worker service, both connected to the same Cloud SQL for PostgreSQL instance. At the end of this page both services are deployed and the worker tier stays warm to process workflows.
Prerequisites
Section titled “Prerequisites”- A project with Cloud Run and Cloud SQL enabled, and
gcloudauthenticated against it - A Cloud SQL for PostgreSQL instance (
PROJECT:REGION:INSTANCE) and a database for Long Tail - An image in a registry Cloud Run can pull, built from a Dockerfile that compiles your code
- Separate
apiandworkerentry points, as in Run modes
Build one image
Section titled “Build one image”One Dockerfile serves both services; each service overrides the command:
FROM node:20-slim AS buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:20-slimWORKDIR /appCOPY --from=build /app/dist ./distCOPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/package.json ./
# Overridden per service:# API: ["node", "dist/api.js"]# Worker: ["node", "dist/worker.js"]CMD ["node", "dist/api.js"]Adjust dist/ to your compiler’s output directory.
Deploy
Section titled “Deploy”-
Deploy the API service. It serves the dashboard, REST API and
/mcpon port 3000:Terminal window gcloud run deploy long-tail-api \--image gcr.io/PROJECT/long-tail:latest \--command node,dist/api.js \--port 3000 \--allow-unauthenticated \--add-cloudsql-instances PROJECT:REGION:INSTANCE \--set-env-vars DATABASE_URL=...,JWT_SECRET=...--allow-unauthenticatedlets browsers reach the dashboard. Long Tail authenticates every API request itself. -
Deploy the worker service. It takes no traffic and must never scale to zero:
Terminal window gcloud run deploy long-tail-workers \--image gcr.io/PROJECT/long-tail:latest \--command node,dist/worker.js \--no-allow-unauthenticated \--min-instances 1 \--add-cloudsql-instances PROJECT:REGION:INSTANCE \--set-env-vars DATABASE_URL=...,HONEYCOMB_API_KEY=... -
Check the API:
Terminal window curl -s https://<api service URL>/health
Environment
Section titled “Environment”| Variable | API | Worker | Description |
|---|---|---|---|
DATABASE_URL |
yes | yes | Postgres connection string, passed to database.connectionString |
PORT |
yes | HTTP listen port (default 3000) | |
JWT_SECRET |
yes | Signs and verifies JWTs | |
NATS_URL |
yes | yes | NATS server for cross-container events |
HONEYCOMB_API_KEY |
yes | Honeycomb telemetry (optional) | |
HMSH_TELEMETRY |
yes | Span verbosity: info or debug |
|
OPENAI_API_KEY |
yes | For workflows that call OpenAI (optional) | |
NATS_WS_TICKET_TTL_SECONDS |
yes | Lifetime of the dashboard’s WebSocket proxy ticket (default 300) | |
NATS_RECONNECT_INITIAL_MS |
yes | Dashboard reconnect: first backoff delay (default 1000) | |
NATS_RECONNECT_MAX_MS |
yes | Dashboard reconnect: backoff cap (default 60000) | |
NATS_LIVE_NOTICE_AFTER_MS |
yes | How long live updates stay down before the dashboard shows a notice (default 30000) |
The API needs no telemetry or model keys: it executes no workflow code. Workers need no JWT_SECRET or PORT: they serve no HTTP. Prefer passing values to start() explicitly in your entry points; the config object is typed.
With the API and workers in separate services, give both an events.nats config pointing at a NATS server they share. The default Socket.IO transport delivers only within one process, so worker events would not reach the dashboard without it.
Database
Section titled “Database”Cloud SQL applies Postgres settings as database flags:
shared_buffers: 25% of instance RAM (auto-tuned by Cloud SQL)work_mem: 16MBmaintenance_work_mem: 256MBmax_wal_size: 2GBsynchronous_commit: onmax_connections: 200Migrations run when each container starts and are serialized by an advisory lock, so many instances can start at once. See Postgres for the reasoning behind each value.
For direct control over replica counts, run two Deployments in one cluster:
apiVersion: apps/v1kind: Deploymentmetadata: name: long-tail-apispec: replicas: 2 template: spec: containers: - name: api image: gcr.io/PROJECT/long-tail:latest command: ["node", "dist/api.js"] ports: - containerPort: 3000---apiVersion: apps/v1kind: Deploymentmetadata: name: long-tail-workersspec: replicas: 3 template: spec: containers: - name: worker image: gcr.io/PROJECT/long-tail:latest command: ["node", "dist/worker.js"]Put the API Deployment behind a Service and an Ingress. The worker Deployment has no Service: it only makes outbound connections.
