Skip to content

Deployment ​

Seepient Agent Server is a stateless Node.js process that can be deployed as a Docker container, on Cloud Run, or directly on any Node.js host.

Docker ​

Build and run ​

Build the Docker container image locally from the repository root:

bash
git clone https://github.com/seepient/seepient.git
cd seepient
docker build -t seepient .

Run the server with an API key and persistent storage volume for the non-root appuser:

bash
docker run -d -p 7337:7337 \
  -v seepient-data:/home/appuser/.seepient \
  seepient

With custom session directory ​

bash
docker run -d -p 7337:7337 \
  -e SEEPIENT_SESSION_DIR=/data/sessions \
  -v session-data:/data/sessions \
  -v seepient-data:/home/appuser/.seepient \
  seepient

Docker Compose ​

yaml
services:
  seepient:
    image: seepient
    build: .
    ports:
      - "7337:7337"
    environment:
      - SEEPIENT_SESSION_TTL=86400
    volumes:
      - seepient-data:/home/appuser/.seepient
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:7337/v1/health"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  seepient-data:

Google Cloud Run ​

bash
gcloud run deploy seepient \
  --image seepient-server \
  --port 7337 \
  --min-instances 1 \
  --max-instances 10 \
  --timeout 3600 \
  --set-env-vars "SEEPIENT_SESSION_TTL=86400"

Cloud Run WebSocket considerations ​

Cloud Run has WebSocket limitations

  • Heartbeat: Send a ping message every 30 seconds to keep the connection alive. Cloud Run may close idle connections.
  • Request timeout: Cloud Run has a maximum request duration of 60 minutes. Long-running conversations should use the reconnection protocol.
  • Session externalization: File-based sessions do not persist across Cloud Run instances. Use the SEEPIENT_SESSION_DIR environment variable to point to a mounted volume, or externalize session storage with Redis.

Cloud Run with secrets ​

bash
gcloud run deploy seepient \
  --image seepient-server \
  --port 7337 \
  --min-instances 1 \
  --max-instances 10 \
  --timeout 3600 \
  --set-env-vars "SEEPIENT_SESSION_TTL=86400"

Bare metal / Node.js ​

Direct Node.js ​

bash
# Install
npm install -g seepient

# Run server
seepient server

Programmatic ​

typescript
import { runSeepientServer } from "seepient/server";

const server = await runSeepientServer({
  port: 7337,
  host: "0.0.0.0",
  cors: true,
  sessionTTL: 86400,
});

Embedding the server in another process ​

runSeepientServer() is designed to be embedded inside a host application (an existing Express/Fastify app's process, a worker, a control plane).

Return value. It returns the Node.js http.Server (typed as SeepientHttpServer, i.e. http.Server & { dispose(): void }). You may attach your own listeners to it, call server.address(), close it, etc.

listen: false. Pass listen: false to receive the configured http.Server without it listening — for example to mount the server on your own host/port, or behind a router you control:

typescript
import { runSeepientServer } from "seepient/server";

const server = await runSeepientServer({ listen: false });
await new Promise<void>((resolve) => server.listen(8080, "127.0.0.1", resolve));

Signal handlers. When the server listens, runSeepientServer registers SIGINT/SIGTERM handlers that close the server and force-exit after a 5-second drain. When listen: false is used, no signal handlers are registered — the embedder owns process lifecycle entirely. Repeated construction never accumulates listeners.

Dispose. The returned server carries a dispose() handle for full teardown: it un-registers the signal handlers runSeepientServer registered for that instance AND closes the server (which also detaches its WebSocket layer). Note that server.close() alone is already enough — the close event removes the signal handlers too:

typescript
server.dispose();   // un-registers handlers + closes the server
// or simply:
server.close();     // close event detaches the signal handlers as well

Multiple servers per process. runSeepientServer can be called more than once in the same process (for example, to serve from an Express or Fastify app, a CLI, or a test harness). Each call creates its own isolated HTTP server and WebSocket server instances.

However, ambient operator state (~/.seepient/setting.json and ~/.seepient) is process-wide by default — when embedding multiple servers, inject per-server persist, auditStore, policyStore, and capabilityLedger contracts to keep their state separated (see Stateless workers).

Process manager (PM2) ​

bash
npm install -g pm2 seepient

# Start with PM2
pm2 start "seepient server" --name seepient

# Save for auto-restart
pm2 save
pm2 startup

Server environment variables ​

VariableDescriptionRequired
SEEPIENT_API_KEYS_FILEPath to server API keys JSON fileNo
SEEPIENT_SESSION_DIRDirectory for file-backed sessionsNo
SEEPIENT_SESSION_TTLSession TTL in seconds (default: 86400)No
SEEPIENT_SKILLS_PATHColon-separated paths to skill directoriesNo
SEEPIENT_MAX_BODY_BYTESRequest body size cap in bytes across all REST routes — chat, settings, gateway, and provider management (default: 10485760). Set to 0 for an unlimited body sizeNo
SEEPIENT_CORS_ORIGINSComma-separated CORS origin allowlist, or * to reflect any origin (default: no CORS headers at all)No
SEEPIENT_WS_MAX_CONNECTIONS_PER_KEYPer-key WebSocket connection cap (default: 50); dead peers are terminated by a 30s heartbeat sweep and release their slotNo
SEEPIENT_RATE_LIMIT_RPMPer-key requests-per-minute cap for REST and WebSocket traffic (default: 300). Set to 0 to disableNo

Server provider isolation

The standalone server binary boots with an isolated empty provider runtime by default, and never reads provider keys from the host environment (022-5 demolition). There are exactly two ways to give the server durable providers (on a multi-tenant server, provider-management mutations and model refresh are limited to each configured account's own host — the operator baseline is derived from the providers file at boot and printed to stderr at startup):

  1. --providers-file <path> (or runSeepientServer({ providersFile })) — an operator-owned JSON file read once at boot into the isolated runtime. It carries provider accounts, model assignments, and credentials (the file is the operator's plaintext secret surface — mount it read-only and keep it out of tenant-visible volumes; chmod 600):

    json
    {
      "providers": {
        "openai-main": {
          "adapter": "pi-ai",
          "upstreamProvider": "openai",
          "credential": { "kind": "seepient", "id": "openai-main-key" }
        }
      },
      "modelAssignments": {
        "text": { "standard": { "providerAccount": "openai-main", "model": "gpt-5-nano" } }
      },
      "credentials": {
        "openai-main-key": { "kind": "api_key", "keyValue": "sk-..." }
      }
    }

    The file is never written back to; runtime mutations via the provider management API stay in-memory and are lost on restart.

  2. Inject a runtime when embedding: runSeepientServer({ runtime }) with an isolated ProviderRuntime you constructed yourself, stamped tenancyMode: "multi" (e.g. createIsolatedProviderRuntime({ tenancyMode: "multi", ... })) — an unstamped runtime is refused at boot with TENANCY_RUNTIME_REQUIRED rather than silently composing without egress enforcement.

Note: .seepient/setting.json in a mounted volume is not read for provider configuration — the isolated runtime never reads ambient settings files.

Egress gating for baseUrl accounts (multi-tenant arming). The server composes its runtime in multi-tenant mode, and every provider account that carries a baseUrl must have that host covered by an operator-baseline network-destination capability. Without the grant:

  • POST /v1/providers/:id/refresh-models denies with EGRESS_REQUIRED before any network traffic (not even DNS resolves).
  • PUT /v1/providers/:id with a baseUrl denies with 400 EGRESS_REQUIRED — an ungranted host cannot be planted, even by a provider:admin key.

Accounts without a baseUrl (default provider endpoints) are unaffected. The standalone CLI has no flag for baselines; embedders grant them at boot:

typescript
const server = await runSeepientServer({
  host: "127.0.0.1",
  port: 7337,
  providersFile: "/config/providers.json",
  operatorBaseline: [
    // Grant the operator's own relay; port is optional and narrows the grant.
    { kind: "network-destination", scheme: "https", host: "relay.example.com" },
  ],
});

Error codes ​

REST error codes ​

CodeHTTP StatusRetryableDescription
UNAUTHORIZED401NoInvalid or missing API key
FORBIDDEN403NoAPI key lacks required scope
BAD_REQUEST400NoInvalid request body or missing fields
EGRESS_REQUIRED400NoProvider account baseUrl host is not covered by an operator-baseline network-destination capability (multi-tenant egress gating)
NOT_FOUND404NoEndpoint or session not found
PROVIDER_ERROR502YesLLM provider returned an error
GENERATION_ERROR500YesText generation failed
INTERNAL_ERROR500NoUnexpected server error

WebSocket error codes ​

CodeRetryableDescription
UNAUTHORIZEDNoAuthentication failed on upgrade
INVALID_MESSAGENoMalformed JSON
UNKNOWN_MESSAGE_TYPENoUnrecognized message type
PROVIDER_ERRORYesLLM provider error
STREAM_ERRORNoInternal streaming failure
SESSION_NOT_FOUNDNoSession expired or missing
ABORTEDNoRequest cancelled by client

Retry strategy ​

For retryable errors (PROVIDER_ERROR, GENERATION_ERROR):

Attempt 1 ──► wait 1s ──► Attempt 2 ──► wait 2s ──► Attempt 3 ──► fail
  • Maximum 3 retries
  • Exponential backoff: 1s, 2s, 4s
  • Do not retry non-retryable errors

Graceful shutdown ​

When runSeepientServer() starts a listening server it handles SIGINT and SIGTERM with a 5-second drain timeout (listen: false servers register no signal handlers — the embedder owns shutdown):

  1. Stops accepting new connections
  2. Closes all active WebSocket connections with code 1001
  3. Stops the session cleanup timer
  4. Waits up to 5 seconds for in-flight requests to complete
  5. Force exits if drain exceeds the timeout

Health monitoring ​

Use the /v1/health endpoint for load balancer health checks:

bash
curl -f http://localhost:7337/v1/health || exit 1

Response:

json
{
  "status": "ok",
  "version": "0.8.0",
  "uptime": 3600
}

Production checklist ​

  • [ ] Configure at least one provider account via --providers-file (the server never reads provider API keys from the host environment; configured accounts' hosts form the derived egress baseline)
  • [ ] Generate API keys with minimal required scopes
  • [ ] Verify ~/.seepient/server-keys.json permissions are 0600
  • [ ] Mount a persistent volume for ./.seepient/sessions/ (or SEEPIENT_SESSION_DIR) if using sessions
  • [ ] Configure health check against /v1/health
  • [ ] Set SEEPIENT_SESSION_TTL appropriate for your use case
  • [ ] Enable WebSocket heartbeat (ping/pong every 30s) for Cloud Run deployments
  • [ ] Configure reverse proxy (nginx, Cloud Load Balancer) with WebSocket upgrade support
  • [ ] Set up log aggregation for [server] and [ws] log prefixes

Serverless chat routes ​

Deploying per-turn chat functions (no tools, no sandbox, sessions owned by your app)? Install seepient-core instead of the full package — the same chat/agent engine at a ~102 MB install closure, with the same injectable store contracts and tenancy behavior. See Stateless workers → Slim install for serverless. The full seepient server binary documented above is unchanged.

Released under the Business Source License 1.1.