Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ here. The release version is defined in the workspace root `package.json`.

## [Unreleased]

## [0.3.0] - 2026-09-10

### Added

- Declare where the Community and Studio Compose stacks answer in an `x-cbk`
endpoint manifest inside each published artifact, resolved with
`docker compose config --format json`, so a launcher reads the stack's
addresses instead of assuming port 3000.

### Changed

- Move the published application port with `PLATFORM_PORT` and the site
hostname with `PLATFORM_HOST` in the Community and Studio stacks. The site,
sign-in, app shell, space and portal addresses follow the port, and
`RELAY_URL` now follows `RELAY_PORT`, so a second instance on one host needs
the port variables rather than an override file.
- Publish the Studio stack on `31000`, `31001` and `31900` instead of
Community's `3000`, `3001` and `3900`, so it runs beside a developer's
existing services on `3000` and beside a Community stack on the same host.
Container ports are unchanged.

## [0.2.2] - 2026-09-10

### Fixed
Expand Down
86 changes: 75 additions & 11 deletions docker/distro/community/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,73 @@
# The developer stack with profiles, source builds and hot reload lives in
# docker-compose.yml at the repository root; this file never builds anything.
#
# Where the stack answers is not fixed: PLATFORM_PORT moves the application
# (and with it the app shells, spaces and portals), RELAY_PORT the realtime
# relay and STORAGE_PORT the object store, and every address a browser or a
# host process dials is declared once in the `x-cbk` manifest below. A
# launcher such as ChatBotKit Studio reads the resolved manifest back with
# `docker compose config --format json` rather than assuming port 3000.
#
# One folder per package flavor lives under docker/distro/. Each publishes as
# ghcr.io/chatbotkit/platform-<flavor>, pinned by CI to the matching
# application and initializer image digests of the same flavor.
# =============================================================================

# @note the endpoint manifest. Each entry is one address that something
# outside the Compose network dials - a browser, a desktop launcher, the
# application itself when it fetches a presigned URL - 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 (the
# anchors below feed `environment`), so `docker compose config --format json`
# resolves them against the caller's environment and `.["x-cbk"]` is the exact
# addressing this stack runs with; no port is assumed on either side. Plain
# Compose ignores `x-` keys. Bump `version` when an entry changes shape.
#
# Hostnames stay under `*.localhost`, which browsers resolve to loopback with
# no DNS setup. A launcher that forwards the published ports must route every
# host named here, including the wildcard apexes, to the same ports.
x-cbk:
version: 1
endpoints:
site:
service: platform
published: ${PLATFORM_PORT:-3000}
variable: SITE_URL
url: &site-url ${SITE_URL:-http://${PLATFORM_HOST:-cbk.localhost}:${PLATFORM_PORT:-3000}}
apps:
service: platform
published: ${PLATFORM_PORT:-3000}
variable: APP_MAIN_ORIGIN
url: &apps-url ${APP_MAIN_ORIGIN:-http://cbk-apps.localhost:${PLATFORM_PORT:-3000}}
labs:
service: platform
published: ${PLATFORM_PORT:-3000}
variable: APP_LABS_ORIGIN
url: &labs-url ${APP_LABS_ORIGIN:-http://cbk-labs.localhost:${PLATFORM_PORT:-3000}}
relay:
service: platform
published: ${RELAY_PORT:-3001}
variable: RELAY_URL
url: &relay-url ${RELAY_URL:-http://cbk-relay.localhost:${RELAY_PORT:-3001}}
storage:
service: garage
published: ${STORAGE_PORT:-3900}
variable: STORAGE_URL
url: &storage-url ${STORAGE_URL:-http://cbk-storage.localhost:${STORAGE_PORT:-3900}}
# @note wildcard host families served on the site endpoint's port: a space
# published as `acme` answers at `http://acme.<space apex>:<published>`
apexes:
space:
service: platform
published: ${PLATFORM_PORT:-3000}
variable: SPACE_APEX
apex: &space-apex ${SPACE_APEX:-cbk-space.localhost}
portal:
service: platform
published: ${PLATFORM_PORT:-3000}
variable: PORTAL_APEX
apex: &portal-apex ${PORTAL_APEX:-cbk-portal.localhost}

# @note the storage environment every application service shares. The store
# is Garage, spoken to over the plain S3 protocol. Left empty, the access key
# is generated by Garage on first boot and persisted in the platform-data
Expand All @@ -38,7 +100,7 @@
# so the two must agree. Set STORAGE_URL to an address both can reach (and
# TLS if the site has it) when browsers do not reach the machine itself.
x-storage-env: &storage-env
STORAGE_ENDPOINT: ${STORAGE_URL:-http://cbk-storage.localhost:${STORAGE_PORT:-3900}}
STORAGE_ENDPOINT: *storage-url
STORAGE_REGION: garage
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-}
STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-}
Expand All @@ -58,33 +120,35 @@ services:
platform:
image: ${PLATFORM_IMAGE:-ghcr.io/chatbotkit/platform-community-app:next}
ports:
- '3000:3000'
# @note PLATFORM_PORT moves the published side, the container keeps 3000
- '${PLATFORM_PORT:-3000}:3000'
# @note the built-in realtime relay - see RELAY_URL below
- '${RELAY_PORT:-3001}:3001'
environment:
<<: *storage-env
NODE_ENV: production
PORT: 3000
SITE_URL: ${SITE_URL:-http://cbk.localhost:3000}
SITE_URL: *site-url
STATIC_URL: ${STATIC_URL:-}
API_URL: ${API_URL:-}
HOSTS_CONFIG: ${HOSTS_CONFIG:-}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://cbk.localhost:3000}
# @note sign-in callbacks land on the site itself unless moved
NEXTAUTH_URL: ${NEXTAUTH_URL:-${SITE_URL:-http://${PLATFORM_HOST:-cbk.localhost}:${PLATFORM_PORT:-3000}}}
# @note realtime channels (voice, avatars) meet at a relay the platform
# process hosts itself on RELAY_PORT. Both that process and a host
# browser dial RELAY_URL, so loopback serves both; a browser elsewhere
# needs an address it can reach instead (and TLS if the site has it)
RELAY_PORT: 3001
RELAY_URL: ${RELAY_URL:-http://cbk-relay.localhost:3001}
# @note host routing reads its settings at server startup;
# recreate the container to change domains without rebuilding the image
SPACE_APEX: ${SPACE_APEX:-cbk-space.localhost}
PORTAL_APEX: ${PORTAL_APEX:-cbk-portal.localhost}
RELAY_URL: *relay-url
# @note host routing reads its settings at server startup; recreate the
# container to change domains without rebuilding the image. See x-cbk
SPACE_APEX: *space-apex
PORTAL_APEX: *portal-apex
APP_APEX: ${APP_APEX:-}
PARTNERS_APEX: ${PARTNERS_APEX:-}
# @note cookies do not cross hosts, so sign in on the shell host itself
APP_MAIN_ORIGIN: ${APP_MAIN_ORIGIN:-http://cbk-apps.localhost:3000}
APP_LABS_ORIGIN: ${APP_LABS_ORIGIN:-http://cbk-labs.localhost:3000}
APP_MAIN_ORIGIN: *apps-url
APP_LABS_ORIGIN: *labs-url
# @note left empty, the image generates these secrets on first boot and
# persists them in the platform-data volume; set explicitly to override
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-}
Expand Down
101 changes: 85 additions & 16 deletions docker/distro/studio/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,77 @@
# The developer stack with profiles, source builds and hot reload lives in
# docker-compose.yml at the repository root; this file never builds anything.
#
# Where the stack answers is not fixed: PLATFORM_PORT moves the application
# (and with it the app shells, spaces and portals), RELAY_PORT the realtime
# relay and STORAGE_PORT the object store, and every address a browser or a
# host process dials is declared once in the `x-cbk` manifest below. A
# launcher such as ChatBotKit Studio reads the resolved manifest back with
# `docker compose config --format json` rather than assuming a port.
#
# Studio publishes on 31000, 31001 and 31900 rather than Community's 3000
# family: it runs beside whatever a developer already has on 3000, and the
# published ports are the only difference - every container keeps its port.
#
# One folder per package flavor lives under docker/distro/. Each publishes as
# ghcr.io/chatbotkit/platform-<flavor>, pinned by CI to the matching
# application and initializer image digests of the same flavor.
# =============================================================================

# @note the endpoint manifest. Each entry is one address that something
# outside the Compose network dials - a browser, a desktop launcher, the
# application itself when it fetches a presigned URL - 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 (the
# anchors below feed `environment`), so `docker compose config --format json`
# resolves them against the caller's environment and `.["x-cbk"]` is the exact
# addressing this stack runs with; no port is assumed on either side. Plain
# Compose ignores `x-` keys. Bump `version` when an entry changes shape.
#
# Hostnames stay under `*.localhost`, which browsers resolve to loopback with
# no DNS setup. A launcher that forwards the published ports must route every
# host named here, including the wildcard apexes, to the same ports.
x-cbk:
version: 1
endpoints:
site:
service: platform
published: ${PLATFORM_PORT:-31000}
variable: SITE_URL
url: &site-url ${SITE_URL:-http://${PLATFORM_HOST:-cbk.localhost}:${PLATFORM_PORT:-31000}}
apps:
service: platform
published: ${PLATFORM_PORT:-31000}
variable: APP_MAIN_ORIGIN
url: &apps-url ${APP_MAIN_ORIGIN:-http://cbk-apps.localhost:${PLATFORM_PORT:-31000}}
labs:
service: platform
published: ${PLATFORM_PORT:-31000}
variable: APP_LABS_ORIGIN
url: &labs-url ${APP_LABS_ORIGIN:-http://cbk-labs.localhost:${PLATFORM_PORT:-31000}}
relay:
service: platform
published: ${RELAY_PORT:-31001}
variable: RELAY_URL
url: &relay-url ${RELAY_URL:-http://cbk-relay.localhost:${RELAY_PORT:-31001}}
storage:
service: garage
published: ${STORAGE_PORT:-31900}
variable: STORAGE_URL
url: &storage-url ${STORAGE_URL:-http://cbk-storage.localhost:${STORAGE_PORT:-31900}}
# @note wildcard host families served on the site endpoint's port: a space
# published as `acme` answers at `http://acme.<space apex>:<published>`
apexes:
space:
service: platform
published: ${PLATFORM_PORT:-31000}
variable: SPACE_APEX
apex: &space-apex ${SPACE_APEX:-cbk-space.localhost}
portal:
service: platform
published: ${PLATFORM_PORT:-31000}
variable: PORTAL_APEX
apex: &portal-apex ${PORTAL_APEX:-cbk-portal.localhost}

# @note the storage environment every application service shares. The store
# is Garage, spoken to over the plain S3 protocol. Left empty, the access key
# is generated by Garage on first boot and persisted in the platform-data
Expand All @@ -38,7 +104,7 @@
# so the two must agree. Set STORAGE_URL to an address both can reach (and
# TLS if the site has it) when browsers do not reach the machine itself.
x-storage-env: &storage-env
STORAGE_ENDPOINT: ${STORAGE_URL:-http://cbk-storage.localhost:${STORAGE_PORT:-3900}}
STORAGE_ENDPOINT: *storage-url
STORAGE_REGION: garage
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-}
STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-}
Expand All @@ -58,36 +124,39 @@ services:
platform:
image: ${PLATFORM_IMAGE:-ghcr.io/chatbotkit/platform-studio-app:next}
ports:
# @note studio trusts the local user, so its ports stay on loopback
- '127.0.0.1:3000:3000'
# @note studio trusts the local user, so its ports stay on loopback;
# PLATFORM_PORT moves the published side, the container keeps 3000.
# Same for the relay (RELAY_PORT, container 3001) below
- '127.0.0.1:${PLATFORM_PORT:-31000}:3000'
# @note the built-in realtime relay - see RELAY_URL below
- '127.0.0.1:${RELAY_PORT:-3001}:3001'
- '127.0.0.1:${RELAY_PORT:-31001}:3001'
environment:
<<: *storage-env
NODE_ENV: production
PORT: 3000
SITE_URL: ${SITE_URL:-http://cbk.localhost:3000}
SITE_URL: *site-url
STATIC_URL: ${STATIC_URL:-}
API_URL: ${API_URL:-}
HOSTS_CONFIG: ${HOSTS_CONFIG:-}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://cbk.localhost:3000}
# @note sign-in callbacks land on the site itself unless moved
NEXTAUTH_URL: ${NEXTAUTH_URL:-${SITE_URL:-http://${PLATFORM_HOST:-cbk.localhost}:${PLATFORM_PORT:-31000}}}
# @note an explicitly empty value restores ordinary email sign-in
NEXTAUTH_TRUSTED_SIGNIN: ${NEXTAUTH_TRUSTED_SIGNIN-true}
# @note realtime channels (voice, avatars) meet at a relay the platform
# process hosts itself on RELAY_PORT. Both that process and a host
# browser dial RELAY_URL, so loopback serves both; a browser elsewhere
# needs an address it can reach instead (and TLS if the site has it)
RELAY_PORT: 3001
RELAY_URL: ${RELAY_URL:-http://cbk-relay.localhost:3001}
# @note host routing reads its settings at server startup;
# recreate the container to change domains without rebuilding the image
SPACE_APEX: ${SPACE_APEX:-cbk-space.localhost}
PORTAL_APEX: ${PORTAL_APEX:-cbk-portal.localhost}
RELAY_URL: *relay-url
# @note host routing reads its settings at server startup; recreate the
# container to change domains without rebuilding the image. See x-cbk
SPACE_APEX: *space-apex
PORTAL_APEX: *portal-apex
APP_APEX: ${APP_APEX:-}
PARTNERS_APEX: ${PARTNERS_APEX:-}
# @note cookies do not cross hosts, so sign in on the shell host itself
APP_MAIN_ORIGIN: ${APP_MAIN_ORIGIN:-http://cbk-apps.localhost:3000}
APP_LABS_ORIGIN: ${APP_LABS_ORIGIN:-http://cbk-labs.localhost:3000}
APP_MAIN_ORIGIN: *apps-url
APP_LABS_ORIGIN: *labs-url
# @note left empty, the image generates these secrets on first boot and
# persists them in the platform-data volume; set explicitly to override
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-}
Expand Down Expand Up @@ -185,7 +254,7 @@ services:
ports:
# @note the local browser uses presigned URLs (see x-storage-env);
# containers reach the same port through the Docker network alias
- '127.0.0.1:${STORAGE_PORT:-3900}:${STORAGE_PORT:-3900}'
- '127.0.0.1:${STORAGE_PORT:-31900}:${STORAGE_PORT:-31900}'
networks:
default:
aliases:
Expand Down Expand Up @@ -215,7 +284,7 @@ services:
environment:
GARAGE_ADMIN_URL: http://garage:3903
GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN:-dev-admin-token}
GARAGE_S3_URL: http://garage:${STORAGE_PORT:-3900}
GARAGE_S3_URL: http://garage:${STORAGE_PORT:-31900}
# @note origins allowed to use presigned URLs from a browser; the URLs
# themselves are the access control
STORAGE_CORS_ORIGINS: ${STORAGE_CORS_ORIGINS:-*}
Expand Down Expand Up @@ -258,7 +327,7 @@ configs:
# @note the region is part of every SigV4 signature: STORAGE_REGION
# must match it, or every request fails authentication
s3_region = "garage"
api_bind_addr = "[::]:${STORAGE_PORT:-3900}"
api_bind_addr = "[::]:${STORAGE_PORT:-31900}"
root_domain = ".s3.garage.localhost"

[admin]
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ APP_LABS_ORIGIN=https://labs.example.com
Shell host routing reads these origins at server startup. Recreate the container
after changing them; no image rebuild is needed. The Community and Studio stacks
default to `APP_MAIN_ORIGIN=http://cbk-apps.localhost:3000` and
`APP_LABS_ORIGIN=http://cbk-labs.localhost:3000`. Existing app paths, sign-in,
`APP_LABS_ORIGIN=http://cbk-labs.localhost:3000`, with the port following
`PLATFORM_PORT`. Existing app paths, sign-in,
callbacks and manifests remain available on the new hosts. Cookies do not cross
hosts, so sign in on the shell host itself.

Expand Down
Loading