Documentation
Production
Deploy

Deploy

You can deploy your Ponder app to any cloud environment that supports Node.js.

Railway

Railway's general-purpose cloud platform is a great starting point for most Ponder apps.

Log in to Railway

Connect your GitHub account, and make sure that your Ponder app has been pushed to remote.

Create a Ponder app service

From the Railway console:

  1. Click New ProjectDeploy from GitHub repo and select your repo from the list
  2. Click Add variables, then add RPC URLs (e.g. PONDER_RPC_URL_1) and other environment variables
  3. Create a public domain. In SettingsNetworking, click Generate Domain
  4. Set the healthcheck path and timeout. In SettingsDeploy, set the Healthcheck Path to /ready and the Healthcheck Timeout to 86400 seconds (1 day)
⚠️

Monorepo users: Configure the Root Directory and Start Command such that ponder start runs at the Ponder project root. For example, set the root directory to packages/ponder or set the start command to cd packages/ponder && pnpm start.

Create a Postgres database

From the new project dashboard:

  1. Click CreateDatabaseAdd PostgreSQL
  2. Open the Variables tab for the Ponder app service, click New VariableAdd Reference → select DATABASE_URL and click Add

After a moment, the Ponder app service should redeploy successfully. Check the Build Logs and Deploy Logs tabs to debug any issues.

Self hosting

In general, hosting a Ponder app is similar to hosting a normal Node.js HTTP server. Rather than offer a step-by-step guide, this section describes the key Ponder-specific quirks to consider when self-hosting.

Database connection

⚠️

You app will have performance issues if the roundtrip database latency exceeds ~20 milliseconds. This is common when using a database in different private network or region.

In production, Ponder works best with a Postgres database in the same private network. Set the DATABASE_URL environment variable to the connection string of your Postgres database, or manually override the database.connectionString option in ponder.config.ts.

ponder.config.ts
import { createConfig } from "@ponder/core";
 
export default createConfig({
  database: {
    kind: "postgres",
    connectionString: "postgres://user:password@mycloud.internal:5432/database",
  },
  // ... more config
});

Database schema

When a Ponder app starts up, it attempts to create user tables (defined in ponder.schema.ts) in the target schema. If another Ponder app is currently using that schema, the app will crash shortly after startup with the error Schema 'public' is locked by a different Ponder app. This locking mechanism avoids issues caused by two Ponder apps writing to the same tables simultaneously.

The default schema is public. This works fine during development, but causes problems in production when multiple deployments share the same database. During a zero-downtime redeployment, the new deployment will fail because the public schema is locked by the old deployment.

To avoid this problem, set the schema option in ponder.config.ts to a deployment-specific value. The best choice for schema depends on your environment - on Railway, Ponder automatically detects and uses the RAILWAY_DEPLOYMENT_ID environment variable. In a Kubernetes cluster, the pod name is often a great choice:

ponder.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ponder-deployment
spec:
  selector:
    matchLabels:
      app: ponder
  template:
    metadata:
      labels:
        app: ponder
    spec:
      containers:
      - name: ponder
        # ...
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
ponder.config.ts
import { createConfig } from "@ponder/core";
 
export default createConfig({
  database: {
    kind: "postgres",
    connectionString: process.env.DATABASE_URL,
    schema: process.env.POD_NAME,
  },
  // ... more config
});

Health checks & probes

The 0.6 release added the /ready endpoint and simplified /health. Read the migration guide.

Use the /health and /ready endpoints to configure health checks or probes.

  • /health: Returns an HTTP 200 response immediately after the process starts.
  • /ready: Returns an HTTP 200 response once indexing progress has reached realtime across all chains. During the historical backfill, the endpoint returns an HTTP 503 response.