Deploy
For platform operators. These tasks affect the whole deployment. Have server access or platform admin rights as the task requires. Start with the administrator manual.
This page shows how to run OpenBot in production. You build a Docker image, prepare the database, set up the platform, and start the server behind a reverse proxy. For every setting, see Configuration.
What you need
Section titled “What you need”- A Linux server with Docker and Docker Compose.
- A domain name for OpenBot, such as
api.example.com, and a TLS certificate for it. - An OpenID Connect provider for the platform, such as
https://id.example.com. Register a client with the redirect URIhttps://api.example.com/auth/callback/oidc. You can skip it when people sign in only through the SSO connections of organizations. - A model service, if you want the Bot to do work. You add it through the API after setup. See Models.
Build the images
Section titled “Build the images”All Docker files are in deploy/docker/. One Dockerfile makes both images.
Each image is a build target (a named last stage in the file):
| Target | Image | What it runs |
|---|---|---|
openbot |
openbot |
OpenBot and its migration program. |
protocol-test |
openbot-protocol-test |
The test service for the App Protocol. See Protocol test. |
The file docker-bake.hcl tells
Docker Bake how to build them. Run this
from the root of the repository:
export OPENBOT_RELEASE=$(git rev-parse HEAD)TAG=$OPENBOT_RELEASE docker buildx bake -f deploy/docker/docker-bake.hcl --loadThis builds openbot:$OPENBOT_RELEASE and
openbot-protocol-test:$OPENBOT_RELEASE for linux/amd64. --load puts them
in your local Docker. Without TAG, the tag is local. To build only one
image, add its target name, for example openbot.
On a computer that is not x86_64, Docker needs QEMU (a program that runs code
for another processor) to build the images. Install it once with
docker run --privileged --rm tonistiigi/binfmt --install amd64.
Both images start from
distroless, with the same
Node.js version as development. Distroless images have no shell and no package
manager, so you cannot docker exec a shell in them. They run as the user
nonroot (65532).
The OpenBot image has Node.js, the bundled server, the bundled migration and setup programs, and the database migrations. It has no build tools and no secrets.
| Path in the image | What it is |
|---|---|
/app/dist/app/Main.mjs |
The server. The image runs it by default. |
/app/dist/app/Migrate.mjs |
The migration program. It prepares the database. |
/app/dist/app/Setup.mjs |
The setup program. It writes the first platform settings. |
/app/migrations |
The database migrations. |
The server in the image listens on 0.0.0.0:3000. The image has a health check
that calls /health every five seconds.
Secrets as files
Section titled “Secrets as files”OpenBot reads each setting from an environment variable first. When the variable
is not set, OpenBot reads the file /run/secrets/<NAME> instead, for example
/run/secrets/OPENBOT_SECRET_KEYS. This is where Docker and Compose put
secrets. An empty file counts as not set. See
Configuration.
Environment variables show in docker inspect. Files in /run/secrets do not.
Compose uses files for all secrets.
Run with Docker
Section titled “Run with Docker”Put your settings in a file, for example openbot.env. It needs only
OPENBOT_DATABASE_URL, OPENBOT_SECRET_KEYS, and OPENBOT_CLUSTER_SECRET.
Start from deploy/docker/production.env.example. docker run does not fill in
${POSTGRES_PASSWORD}, so write the full OPENBOT_DATABASE_URL yourself.
Make the secret keys once:
echo "OPENBOT_SECRET_KEYS=k1:$(openssl rand -hex 32)" >> openbot.envecho "OPENBOT_CLUSTER_SECRET=$(openssl rand -hex 32)" >> openbot.envSee Secret keys.
Private node calls use /internal/executions/rpc and /internal/resources/rpc.
Keep both paths inside your private network. Give every node the same random
secret. Set a distinct reachable host for each node when you use more than one
node. The private RPC client is separate from the App network guard.
Prepare the database
Section titled “Prepare the database”The migration program needs OPENBOT_DATABASE_URL and OPENBOT_SECRET_KEYS.
Stop all OpenBot instances before you run it. Back up the database and the list
of object storage keys. Keep the old secret keys: the data move needs them to
open old records. Run it before the first server start and each new release:
docker run --rm --env-file openbot.env \ openbot:$OPENBOT_RELEASE /app/dist/app/Migrate.mjsThe same migration changes supported UUID columns from text to PostgreSQL
uuid. These columns store each ID in 16 bytes. Old v4 values keep their value;
new stored IDs use v7 where time order helps. Columns with mixed or outside IDs
keep their text form. Run this change with all servers stopped, as above.
Run the same command again after a stopped data move. Each batch commits its changes and progress together. A bad record stops the move and saves a sealed failure report. The program checks counts, references, secrets and states before it marks the data version ready. A server cannot start before that check passes. Live servers hold a shared database lock; the data move needs the only lock.
Keep resource IDs, history and pending decisions in the backup. An old outside
action with no known result stays outcome_unknown. Work with no safe
checkpoint is marked interrupted with its reason. A missing original token
never becomes an unrestricted member call.
Before traffic returns, you can restore the backup. Once outside actions start again, repair data by moving forward. Do not restore old data and repeat those actions.
Set up the platform
Section titled “Set up the platform”Run the setup program once, on a new database:
docker run --rm --env-file openbot.env openbot:$OPENBOT_RELEASE \ /app/dist/app/Setup.mjs \ --public-origin=https://api.example.com \ --oidc-issuer=https://id.example.com \ --oidc-client-id=openbot \ --admin-email=alice@example.comIt writes the public origin, the platform provider, and the first platform
admins to the database. Give a client secret with
-e OPENBOT_SETUP_OIDC_CLIENT_SECRET=..., not on the command line. See
Platform for every flag.
You can also run setup after the server starts. Until then, every route except
/health answers 503 with SetupRequired.
Start the server
Section titled “Start the server”docker run -d --name openbot --env-file openbot.env \ -p 127.0.0.1:8080:3000 openbot:$OPENBOT_RELEASEWhen the server starts, it checks the tables and the completed data version. If either check fails, it stops and writes an error to the log.
Check it
Section titled “Check it”curl http://127.0.0.1:8080/health{ "status": "ok" }/health only shows that the HTTP server can answer. It does not check the
database, the setup, or the model.
Alice can now sign in. She is a platform admin. Next, add a model and a model provider that links it. See Models.
Without Docker
Section titled “Without Docker”You can also run the programs with Node.js on the server:
pnpm install --frozen-lockfilepnpm --filter-prod @openbot-build/openbot... buildpnpm --filter @openbot-build/openbot migratepnpm --filter @openbot-build/openbot setup --public-origin=https://api.example.com --admin-email=alice@example.compnpm --filter @openbot-build/openbot startThe package @openbot-build/openbot also has the commands openbot-server,
openbot-migrate and openbot-setup. With the package @openbot-build/cli,
you can also run them as openbot server, openbot migrate and
openbot setup.
Run with Compose
Section titled “Run with Compose”The file deploy/docker/compose.production.yaml runs these services:
| Service | Image | Port on the host |
|---|---|---|
postgres |
postgres:18.6-alpine3.24, pinned by digest, data in the volume postgres |
none |
migrate |
openbot:$OPENBOT_RELEASE |
none |
openbot |
openbot:$OPENBOT_RELEASE |
127.0.0.1:8080, or OPENBOT_PUBLISHED_PORT |
protocol-test |
openbot-protocol-test:$OPENBOT_RELEASE |
127.0.0.1:3001 |
migrate runs the migration program once, after PostgreSQL is healthy.
openbot starts only when migrate succeeds. So up always migrates the
database first.
The protocol-test service is the public test service for the App Protocol. See
Protocol test. The file needs PROTOCOL_TEST_PUBLIC_ORIGIN
even if you only use OpenBot.
Compose gives POSTGRES_PASSWORD, OPENBOT_DATABASE_URL, OPENBOT_SECRET_KEYS
and OPENBOT_CLUSTER_SECRET as files in /run/secrets. Their values come from
.env.production. The openbot service also gets OPENBOT_OUTBOUND_ALLOW and
OPENBOT_CLUSTER_HOST. Product settings live in the database. The migrate
service needs OPENBOT_DATABASE_URL and OPENBOT_SECRET_KEYS.
In the template, OPENBOT_DATABASE_URL uses ${POSTGRES_PASSWORD}, so you set
the password only once.
Prepare the settings
Section titled “Prepare the settings”cp deploy/docker/production.env.example .env.productionopenssl rand -hex 32 # use the output for POSTGRES_PASSWORDecho "k1:$(openssl rand -hex 32)" # use the output for OPENBOT_SECRET_KEYSopenssl rand -hex 32 # use the output for OPENBOT_CLUSTER_SECRETFill in the values in .env.production.
Build both images
Section titled “Build both images”export OPENBOT_RELEASE=$(git rev-parse HEAD)TAG=$OPENBOT_RELEASE docker buildx bake -f deploy/docker/docker-bake.hcl --loadCheck the settings
Section titled “Check the settings”docker compose --env-file .env.production -f deploy/docker/compose.production.yaml config --quietThis shows an error when a required setting is missing. Without --quiet, it
prints the full setup with all values filled in.
Start everything
Section titled “Start everything”docker compose --env-file .env.production -f deploy/docker/compose.production.yaml up -d --wait--wait returns when all services are healthy. The images check their health
every five seconds. When a service process stops, Compose starts it again.
Set up the platform
Section titled “Set up the platform”Run the setup program once, after the first up:
docker compose --env-file .env.production -f deploy/docker/compose.production.yaml \ run --rm openbot /app/dist/app/Setup.mjs \ --public-origin=https://api.example.com \ --oidc-issuer=https://id.example.com \ --oidc-client-id=openbot \ --admin-email=alice@example.comUntil you do this, every route except /health answers 503 with
SetupRequired. See Platform.
To release a new version, build new images and set the new OPENBOT_RELEASE.
Stop every OpenBot instance, back up the database and the object key list, then
run up -d --wait again. migrate runs again before the new OpenBot starts.
Never delete the postgres volume. It holds all your data.
Many instances
Section titled “Many instances”You can run many OpenBot instances behind one load balancer. Give every instance:
- the same
OPENBOT_DATABASE_URL, - the same
OPENBOT_SECRET_KEYSandOPENBOT_CLUSTER_SECRET, - a distinct
OPENBOT_CLUSTER_HOSTthat every other node can reach, - the same
OPENBOT_OUTBOUND_ALLOW, if you set it. See Outbound connections.
Use a private address for each node, not the load balancer address. Do not scale
the Compose openbot service without giving each new node its own host. The two
nodes in compose.check.yaml show this setup.
All shared settings live in the database. When an admin changes them, every instance uses the change at once. See Configuration. Run the migration program and the setup program only once, not on each instance.
TLS and the reverse proxy
Section titled “TLS and the reverse proxy”In production, a reverse proxy handles HTTPS and sends the traffic to OpenBot on
127.0.0.1:8080. OpenBot serves plain HTTP only, so the proxy is the only part
that serves HTTPS.
The proxy must:
-
reject all public requests under
/internal/, -
send the
Origin,Cookie, andAuthorizationheaders to OpenBot without changes, -
not buffer responses, because Turns can stream their output as server-sent events (SSE),
-
allow long responses. An execution event stream can stay open until the execution ends.
server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/ssl/api.example.com.pem; ssl_certificate_key /etc/ssl/api.example.com.key;
location /internal/ { return 404; }
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; proxy_read_timeout 1h; }}api.example.com { handle /internal/* { respond 404 } handle { reverse_proxy 127.0.0.1:8080 { flush_interval -1 } }}Your own client on the same origin
Section titled “Your own client on the same origin”A browser client on the same origin as OpenBot can use the sign-in cookie. It needs no user token. To do this, your proxy sends some paths to OpenBot and all other paths to your client.
| Path | Send to | What it is |
|---|---|---|
/auth/* |
OpenBot | Sign-in, sign-out, and the App authorization callback. |
/v1/* |
OpenBot | The API. |
/health |
OpenBot | The health check. |
/openapi.json |
OpenBot | The OpenAPI document. |
/docs |
OpenBot | The interactive API reference. Optional. |
| everything else | Your client | Your pages, including /. |
When you mount your client at /, the service document of GET / is no longer
public. That is fine. Clients can use the paths above directly.
After sign-in, OpenBot sends the browser back to a path on your client. The
default is /. Your client must also handle these return addresses:
/?authorization=<flowId>after an App authorization. CallcompleteAppAuthorizationnext. See Apps./?developerTest=<testId>after a developer test authorization. See Developer tools.
A return path can be any path on the same origin, except paths under /auth/,
/v1/, and /development/.
Backups
Section titled “Backups”Back up these things, and keep them together:
- The database. It holds the platform and organization settings, accounts, organizations and their members, user tokens, models and model providers, SSO connections, the audit log, Bots, Chats and their history, and App connections.
OPENBOT_SECRET_KEYS. The keys decrypt the secrets in the database: App secrets, model provider keys, SSO client secrets, and the sign-in secret. A database backup without the keys cannot restore them.- The bucket of files. It holds the bytes of the files that people uploaded. The database only names them.
With Compose, make a database backup like this:
docker compose --env-file .env.production -f deploy/docker/compose.production.yaml \ exec -T postgres pg_dump -U openbot -Fc openbot > openbot-$(date +%F).dumpTo restore it, first stop OpenBot with
docker compose --env-file .env.production -f deploy/docker/compose.production.yaml stop openbot.
Then run pg_restore:
docker compose --env-file .env.production -f deploy/docker/compose.production.yaml \ exec -T postgres pg_restore -U openbot -d openbot --clean < openbot-2026-10-20.dumpGitHub Actions
Section titled “GitHub Actions”For this development task, see Publish and check the project.