Skip to content

Latest commit

 

History

History
427 lines (349 loc) · 20.5 KB

File metadata and controls

427 lines (349 loc) · 20.5 KB

Deployment

The repository provides a complete development stack, a locally compiled image profile and prebuilt community images produced by trusted pushes. These are application distribution artifacts, not infrastructure provisioning recipes. Production operators remain responsible for the surrounding deployment and its operational guarantees.

Compose profiles

Run the complete development stack:

docker compose up

Run only backing services for host-side development:

docker compose up redis qdrant garage garage-init

Build and run the experimental compiled image:

docker compose --profile distro up --build platform

The first compiled build can take more than 15 minutes depending on available CPU, memory and network cache state.

Pull the community image

Trusted pushes to main and next publish matching application and database initializer images. Names follow platform-<flavor>-<component>; tags carry only the build: the moving main and next tags are channels (latest follows main), sha-<commit> tags identify an immutable source revision, and v<version> tags identify a release - the same images as the platform/v<version> source snapshot and its GitHub Release.

The publication workflow derives the registry owner and image name from the GitHub repository. In chatbotkit/platform this resolves to the official image names below without repository-specific workflow configuration.

The public ChatBotKit images use ghcr.io/chatbotkit/platform-community-app and ghcr.io/chatbotkit/platform-community-init. Start the Compose profile without allowing a source build. Each tag is a multi-platform image for linux/amd64 and linux/arm64, so Docker selects the host architecture automatically:

docker compose --profile distro up --no-build --pull always platform

By default this pulls:

ghcr.io/chatbotkit/platform-community-app:next
ghcr.io/chatbotkit/platform-community-init:next

Select another matching channel or immutable revision by setting both image references:

PLATFORM_IMAGE=ghcr.io/chatbotkit/platform-community-app:main \
PLATFORM_INIT_IMAGE=ghcr.io/chatbotkit/platform-community-init:main \
  docker compose --profile distro up --no-build --pull always platform

Never mix application and initializer revisions. The database schema and generated application client must come from the same source and package flavor.

One-command distribution stack

Each trusted push also publishes the flavor's complete Compose application as an OCI artifact under ghcr.io/chatbotkit/platform-<flavor>. The artifact is self-contained - the application, database initializer, Redis, Qdrant and Garage with its configuration and provisioning - and every image reference in it is resolved to a digest, so an artifact tag identifies an exact, immutable stack. No checkout, no bind mounts:

docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest up

The latest tag follows main; next follows the next branch; pin a release with v<version>. Compose v2.34 or newer is required. On up, Compose shows the stack's variables - site URL, secrets, optional provider keys - and their defaults before proceeding; set them in the shell, in a .env file in the directory the command runs from (picked up automatically), or via an explicit --env-file, and pass -y to skip the confirmation in scripts. Shell values win over .env, and only variables the stack declares are consumed - the published artifact carries no env_file mounts, so arbitrary extra entries do nothing.

Persisted configuration

Values can also live in the platform data volume, where they survive restarts and upgrades and never touch a file on the host. The application entrypoint reads /data/config.env (one KEY=VALUE per line, no quoting) and exports every entry the container environment does not already set. Any variable the application honours is accepted, not only the ones the stack declares. setup writes the file:

# prompts for the provider keys, input hidden; Enter keeps, "-" clears
docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest run --rm --no-deps platform setup

# prompts for named variables, or sets them without a terminal
docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest run --rm --no-deps platform setup OPENROUTER_MODELS_API_KEY
docker compose -f oci://ghcr.io/chatbotkit/platform-community:latest run --rm --no-deps platform setup OPENROUTER_MODELS_API_KEY=sk-or-... OPENAI_API_KEY=

A running platform service applies the change on its own: the entrypoint polls config.env every PLATFORM_CONFIG_WATCH_INTERVAL seconds (default 5) and restarts the application process when it changes, so expect a short interruption rather than a reload. Set the interval to 0 in an override file to disable the watch and run the application as the container's main process; the service then needs a docker compose restart platform after each change. Precedence, highest first: the container environment (shell, .env, --env-file, -e), then config.env, then the secrets generated on first boot. An empty environment value counts as unset, so the stack's ${OPENAI_API_KEY:-} defaults never mask a persisted value; to override one for a single run, set it in the shell. The file is owned by the application user with mode 0600; back it up with the volume, and prefer PRISMA_FIELD_ENCRYPTION_KEY in the environment rather than next to the database it protects.

Several instances on one host

The stack declares no project name, so Compose derives one. Two instances started from the same artifact would share that project - and with it the platform-data volume - and both would try to publish port 3000. Give each instance its own project with -p and move its published ports through the port variables: PLATFORM_PORT for the application (the app shells, spaces and portals follow it), RELAY_PORT for the realtime relay and STORAGE_PORT for the object store. Every derived address - SITE_URL, NEXTAUTH_URL, the app shell origins, RELAY_URL, STORAGE_URL - picks up the new port, so nothing else needs setting. Pass the variables in the shell or through --env-file on every command for that instance, since a single .env in the working directory cannot describe both:

PLATFORM_PORT=3100 RELAY_PORT=3101 STORAGE_PORT=3901 docker compose -p cbk-staging \
  -f oci://ghcr.io/chatbotkit/platform-community:latest up -d
PLATFORM_PORT=3100 RELAY_PORT=3101 STORAGE_PORT=3901 docker compose -p cbk-staging \
  -f oci://ghcr.io/chatbotkit/platform-community:latest logs platform

Volumes, networks and container names are all prefixed with the project name, so each instance keeps its own database, generated secrets, object store and vector index, and -p is also how logs, ps and down find the right one.

Endpoint manifest

Where a stack answers is not fixed, so it says so itself. Both Compose files carry an x-cbk block that lists every address something outside the Compose network dials - the site, the Apps and Labs shells, the realtime relay, the object store, and the space and portal wildcard apexes - with the service that answers, the host port it is published on and the variable that overrides it. The url values are the same expressions the services receive, so resolving the file resolves the manifest against the same environment:

PLATFORM_HOST=studio.localhost \
  docker compose -f oci://ghcr.io/chatbotkit/platform-studio:latest \
  config --format json | jq '."x-cbk"'
{
  "version": 1,
  "endpoints": {
    "site": {
      "service": "platform",
      "published": "31000",
      "variable": "SITE_URL",
      "url": "http://studio.localhost:31000"
    },
    "apps": { "url": "http://cbk-apps.localhost:31000", "...": "..." },
    "labs": { "url": "http://cbk-labs.localhost:31000", "...": "..." },
    "relay": { "url": "http://cbk-relay.localhost:31001", "...": "..." },
    "storage": { "url": "http://cbk-storage.localhost:31900", "...": "..." }
  },
  "apexes": {
    "space": { "apex": "cbk-space.localhost", "published": "31000", "...": "..." },
    "portal": { "apex": "cbk-portal.localhost", "published": "31000", "...": "..." }
  }
}

A launcher such as CBK Studio therefore never assumes a port: it keeps the flavor's defaults or sets PLATFORM_PORT, RELAY_PORT and STORAGE_PORT to free ones (and PLATFORM_HOST or any endpoint's variable to rename a host), reads the resolved manifest back and forwards, opens and trusts exactly the addresses it lists - including the wildcard apexes, which answer on the site's port. A bundle can move or rename its endpoints without a launcher release, since the manifest travels inside the published artifact. Plain Compose ignores x- keys, and the version field changes when an entry changes shape. The manifest is data from the bundle: a launcher validates it - loopback hosts only for a desktop install - before dialing anything it names.

The artifact is published from docker/distro/community/compose.yml, which also runs directly from a checkout. One folder per package flavor lives under docker/distro/; a future PostgreSQL flavor publishes as ghcr.io/chatbotkit/platform-postgresql from its own folder, built from the matching image flavor.

Browser-facing file upload and download flows presign URLs against the in-stack store, published on port 3900 (STORAGE_PORT; 31900 in Studio) under one name, http://cbk-storage.localhost:3900: a *.localhost name browsers resolve to loopback like the relay and app shells, and an alias of the garage service inside the Compose network, since the application fetches the same URLs. Set STORAGE_URL to an address both browsers and the containers can reach (with TLS if the site has it) when browsers do not reach the host itself. garage-init grants every bucket a CORS rule for STORAGE_CORS_ORIGINS (default * - the presigned URL is the access control; narrow it for a store reachable beyond the host).

Distribution flavors

A flavor is the baseline of module defaults plus the backing services its stack provisions. Everything not listed keeps the default - in the community flavor the database is SQLite in the platform data volume, the queue is immediate and non-durable, sign-in codes are read from the container log, and agent code runs in the default in-process sandbox with its workspaces kept under /data/sandbox in the same volume.

Flavor Database Cache Vector Storage
community SQLite (default module) Redis Qdrant Garage
studio SQLite (default module) Redis Qdrant Garage

Studio starts as a copy of Community, with the same Docker build targets, module defaults and services. It is the flavor embedded by CBK Studio, the native macOS app that runs the platform in an app-private VM without a Docker install, and can also be run directly with Compose. Its separate Compose file lives at docker/distro/studio/compose.yml, and the publish workflow produces platform-studio, platform-studio-app and platform-studio-init under ghcr.io/chatbotkit, using the same channel tags as Community. Once published from main, run it with:

docker compose -f oci://ghcr.io/chatbotkit/platform-studio:latest up

Studio publishes on its own port family - 31000 for the application, 31001 for the relay and 31900 for the object store - so it runs beside whatever a developer already has on Community's 3000, 3001 and 3900, and the two stacks can share a host. The containers listen on the ports they publish: the application reaches itself and its relay through the addresses in the manifest, so a port that differs inside and out would refuse the application's own calls. The Studio app learns where the stack answers from its endpoint manifest rather than a fixed port.

A PostgreSQL flavor would swap the database column only; the other services travel unchanged.

Trusted sign-in

For an install that only its owner can reach - a laptop, a desktop build, a lab box - the sign-in code round trip through the container log is friction without a purpose. NEXTAUTH_TRUSTED_SIGNIN=true replaces it: the sign-in page asks for an email address and signs straight into that account, creating it on first use. Sessions, audit records and the allowed-email checks are the same as after a verified code. Studio enables this mode by default and binds its application, relay and storage ports (31000, 31001, 31900) to 127.0.0.1. Community keeps ordinary email sign-in.

It is exactly as unsafe as it sounds. Anyone who can reach the port can sign in as anyone, including whoever holds the administrator addresses. So the process refuses to start, with a named error, for an invalid value or when trusted sign-in is enabled alongside hosted configuration:

  • the value is anything other than the literal true or an empty value
  • TARGET_ENV is production or staging
  • an OAuth sign-in provider is configured (NEXTAUTH_GOOGLE_APP_ID, NEXTAUTH_AZURE_AD_CLIENT_ID or NEXTAUTH_GITHUB_APP_ID)
  • LIMITS_CONFIG is set, meaning plans are sold to other people

An environment file that enables the flag alongside hosted configuration therefore fails the boot rather than opening every account. Keep trusted installs accessible only to their owner; the environment checks do not enforce network isolation. Studio's published ports enforce the local desktop default, but an additional reverse proxy or tunnel can expose them.

Start Studio with trusted sign-in:

docker compose -f oci://ghcr.io/chatbotkit/platform-studio:latest up -d

To restore ordinary email sign-in, explicitly pass an empty value:

NEXTAUTH_TRUSTED_SIGNIN= docker compose \
  -f oci://ghcr.io/chatbotkit/platform-studio:latest up -d

An empty value disables trusted sign-in; the string false is rejected. If the flag was also saved in the data volume with platform setup, clear that persisted value first: an empty container variable does not override persisted configuration.

Production boundary

The distro profile demonstrates that the application can be compiled and run from the published tree. It deliberately does not stand up an operator's production infrastructure. A production deployment still needs:

  • TLS termination and a trusted reverse proxy that overwrites forwarded headers
  • protection and backup of the runtime secrets generated into the persistent platform data volume, or explicit operator-provided values
  • durable database, object-storage and backup policies
  • a durable queue when delayed delivery, retries, callbacks or ordering matter
  • a sandbox with kernel-level isolation and per-tenant resource accounting if agent code execution is exposed to untrusted users; the default runs agent code in a userspace VM inside the application process - see module defaults
  • monitoring, restore testing and an upgrade and rollback procedure

Promotions to main produce versioned source snapshots (platform/v*), GitHub Releases with changelog notes and source downloads, and v<version> image and Compose artifact tags (see CONTRIBUTING.md). Images still ship without SBOMs or signed provenance, so they remain pre-release artifacts. This status concerns release provenance and compatibility, not whether Compose should provision the operator-owned infrastructure listed above.

The experimental Dockerfile builds with .env.example; Compose attaches the optional operator .env file only to the running container. Configuration consumed by Next.js while it builds, including ZONE_CONFIG and the build-time parts of host and subscription configuration, is therefore not baked into the distro image. A production pipeline must supply those values during the build and keep secrets out of image layers.

The current community image deliberately bakes the neutral single-host topology: SITE_URL=http://cbk.localhost:3000, with no external zones. The Community and Studio stacks configure the two app shells at http://cbk-apps.localhost:3000 and http://cbk-labs.localhost:3000 through APP_MAIN_ORIGIN and APP_LABS_ORIGIN; the port in all three follows PLATFORM_PORT and the site's hostname PLATFORM_HOST (see Endpoint manifest). Browsers resolve any *.localhost name to loopback, so a space site published as acme answers at http://acme.cbk-space.localhost:3000 with no DNS or hosts-file setup (curl needs --resolve). The Community and Studio Compose stacks default SPACE_APEX to cbk-space.localhost and PORTAL_APEX to cbk-portal.localhost. Space and portal routing read these values at server startup. Changing them and recreating the container moves those sites to the new domains without rebuilding the image. Portal authentication and app configuration continue to apply on the new domain. App hosts work the same way through APP_APEX, APP_MAIN_ORIGIN and APP_LABS_ORIGIN; see Configuration. Partner routing and branding also read PARTNERS_APEX at startup, using the installed partner catalogue for custom domains and branding. Runtime service variables such as the database, Redis, Qdrant and S3-compatible storage endpoints remain configurable. Deployment identity that Next currently exposes through next.config.js is still frozen at build time; do not present the same digest as portable across arbitrary public domains until that migration is complete.

API endpoint

Every deployment serves the API at /api/v1 on its own host - nothing to configure. To advertise and serve it on a dedicated origin instead, set API_URL (e.g. https://api.example.com), point that DNS name at the deployment, and restart the server: the host is then routed to the API under the clean /v1 path. Runtime API URL helpers use the configured origin. Unset, advertised URLs stay on the site host under /api. Multi-domain deployments name their API hosts in HOSTS_CONFIG instead; see Configuration. Both are read at server startup, so changing API hosts requires recreating the container without rebuilding. The existing CORS policy applies to /v1 on dedicated API hosts and /api/v1 on every host: any origin may call the API with a bearer token; cookie credentials are not enabled. Community and Studio expose API_URL and HOSTS_CONFIG, leaving both empty by default.

Static host

STATIC_URL selects the origin for public assets and widget embeds. A dedicated static hostname applies the existing static path restrictions, including the text fallback for application pages. Leaving it unset serves those assets on SITE_URL without restricting the site.

Static host routing reads STATIC_URL and the static targets in HOSTS_CONFIG at server startup. Change the values and recreate the container to move static hosts without rebuilding. Community and Studio expose STATIC_URL and leave it empty by default.

Sitemaps and crawl policy

The platform does not generate a root sitemap or sitemap chunks during the build. Public examples, hub resources, connections and model pages retain their dynamic section sitemaps. A frontend site can include those endpoints in its own sitemap index.

public/robots.txt is a checked-in, origin-independent crawl policy. It keeps the existing allow and content-signal directives without embedding a deployment host or sitemap URL. SITE_URL remains runtime configuration for absolute URLs, including links emitted by the dynamic section sitemaps.

Reverse proxy trust

Set TRUST_PROXY_HEADERS=true only when the reverse proxy overwrites forwarded host, protocol and client-address headers and the application origin cannot be reached directly. See Configuration for the complete trust-boundary requirements.

Persistent state

The development stack stores state in Compose volumes. A production design must make the retention, backup and restore behavior explicit for:

  • the application database
  • object-storage buckets
  • Redis when it carries shared rate-limit or cache state
  • Qdrant or the selected vector implementation
  • encryption keys and other runtime secrets

Backing up ciphertext without its encryption key is not a recoverable backup.

Module limits

The public defaults prioritize a vendor-free boot and an honest development experience. Some defaults are intentionally not production implementations. Read Module defaults before selecting production backends.