Skip to content

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.

  • A project with Cloud Run and Cloud SQL enabled, and gcloud authenticated 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 api and worker entry points, as in Run modes

One Dockerfile serves both services; each service overrides the command:

Dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --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.

  1. Deploy the API service. It serves the dashboard, REST API and /mcp on 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-unauthenticated lets browsers reach the dashboard. Long Tail authenticates every API request itself.

  2. 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=...
  3. Check the API:

    Terminal window
    curl -s https://<api service URL>/health
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.

Cloud SQL applies Postgres settings as database flags:

shared_buffers: 25% of instance RAM (auto-tuned by Cloud SQL)
work_mem: 16MB
maintenance_work_mem: 256MB
max_wal_size: 2GB
synchronous_commit: on
max_connections: 200

Migrations 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:

long-tail.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: long-tail-api
spec:
replicas: 2
template:
spec:
containers:
- name: api
image: gcr.io/PROJECT/long-tail:latest
command: ["node", "dist/api.js"]
ports:
- containerPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: long-tail-workers
spec:
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.