Skip to main content

Install with Docker


You can install BOW with a single docker command. By default, it will use SQLite as the database. You can also configure it to use PostgreSQL by passing BOW_DATABASE_URL environment variable.
To use PostgreSQL, you need to set the BOW_DATABASE_URL environment variable. For example: BOW_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bagofwords

Update

  • Re-run the same docker run --pull always ... command to fetch and start the latest image.
  • Optionally, pull explicitly and restart:

Install with Docker Compose


Run BOW with Docker Compose and Caddy (built-in TLS on port 443). We recommend using the canonical files from the repo to avoid drift:

Steps

  1. Make sure Docker and Docker Compose are installed.
  2. Clone the repo:
  3. Create a .env file (for domain and credentials). Example:
    Generate BOW_ENCRYPTION_KEY with OpenSSL:
  4. Start services:
  5. Point your domain to the server’s public IP:
    • Create an A record for yourdomain.com → your instance public IP.
    • Caddy will automatically obtain/renew the TLS certificate and serve on port 443.
  6. Open https://yourdomain.com
Caddy is included by default as a reverse proxy on port 443. If you prefer to run without Caddy, remove the caddy service from docker-compose.yaml and expose the app directly on port 3000. For local, no-SSL testing, you can also use docker-compose.dev.yaml.

Update

Air-gapped downloads

Download the bundle on a connected machine, then transfer it to your offline environment. Choose the architecture of the destination Linux host: amd64 for Intel/AMD or arm64 for ARM. Each bundle includes the BOW, PostgreSQL, and Caddy container images, Docker Compose configuration, and an installation README. The destination host needs Docker Engine and Docker Compose v2 installed.

Download URL pattern

Find a version in GitHub Releases. Use the version number without the leading v and replace {arch} with amd64 or arm64:
Append .sha256 to the download URL for its checksum file. The architecture-independent source archive uses:
The source archive contains tracked source code and a SOURCE_COMMIT file identifying the build commit; it does not include Python or Node.js dependencies.

Example: version 0.0.587

Download the archive and matching checksum into the same directory, then verify before extracting. For example, on Linux:
Follow the README inside bagofwords-airgap/ for offline installation and internal TLS configuration.

Other configurations


You can also configure additional settings in the bow-config.yaml file.
To use the custom config file, you can run the following command:

Install with Kubernetes


The bow/bagofwords Helm chart (chart version 3.0.0) runs BOW on any Kubernetes cluster. It deploys:
  • A Deployment running the bagofwords/bagofwords image on port 3000, with startup, readiness and liveness probes on /health
  • A ClusterIP Service and an Ingress (class nginx by default)
  • A ServiceAccount (named bowapp by default) that you can annotate for IRSA or Workload Identity
  • A ConfigMap holding environment variables and the generated bow-config.yaml, mounted at /app/bow-config.yaml
  • Optionally, a bundled single-instance PostgreSQL 17 (a StatefulSet running the official postgres:17-bookworm image), enabled by default. The chart has no chart dependencies.
  • Optionally, a HorizontalPodAutoscaler, PodDisruptionBudget and NetworkPolicy (all disabled by default)
Prerequisites
  • A Kubernetes cluster and kubectl access to it
  • Helm 3.8 or newer
  • An ingress controller (the chart defaults to ingress-nginx), or set ingress.enabled=false and expose the Service yourself
  • For the bundled PostgreSQL: a default StorageClass (the database requests a 20Gi volume)
  • For TLS: cert-manager or an existing TLS Secret
The chart validates your values against a strict schema. Unknown keys, or a wrong type such as --set ingress.tls=false, fail the install with values don't meet the specifications of the schema. If you are coming from older docs or an older values file, see Upgrading from chart 2.x and Upgrading from chart 1.x.
The PostgreSQL volume does not persist application files. Before a production install or any rollout of an existing deployment, configure Kubernetes persistent storage. The guide covers AWS, Azure, other clusters, and copying files out of existing pods before replacing them.

Quick start

1

Add the Helm repository

2

Create a namespace and a Secret

Keep sensitive values in a Kubernetes Secret, not in your values file. The chart loads every key of the Secret named in config.secretRef as an environment variable.
  • BOW_ENCRYPTION_KEY encrypts stored credentials. Back it up. See Day 2 operations.
  • password and postgres-password are the keys the bundled PostgreSQL reads for the app user and the postgres superuser. The app user is created as the database owner, not as a superuser.
  • BOW_DATABASE_URL points the app at the bundled database. The host is <release-name>-postgresql, so adjust it if your release is not called bowapp.
3

Write a values file

4

Install

5

Verify

The first start runs database migrations, so the pod can take a minute or two to become ready. Then open https://bow.example.com. Before DNS and TLS are in place, you can reach the app with:
If neither config.encryptionKey nor config.secretRef is set, a new encryption key is generated each time a pod starts. Stored LLM keys, data source credentials and OAuth tokens then become unreadable after any restart. The chart prints a warning in its install notes when this happens.
For a quick evaluation you can skip the Secret and pass values inline. These values are stored in plain text in the ConfigMap, and the chart warns about it:

Database options

This is the default (postgresql.enabled: true). The quick start above uses it.The chart runs its own PostgreSQL 17 StatefulSet from the official postgres image, with a <release>-postgresql Service and a 20Gi volume. Common settings:
Other keys: postgresql.image.registry / repository / tag (default docker.io / library/postgres / 17-bookworm), postgresql.imagePullSecrets, postgresql.primary.podAnnotations, podSecurityContext, containerSecurityContext and terminationGracePeriodSeconds (60). Run helm show values bow/bagofwords for the full list. Keys from the old Bitnami subchart are rejected.
Keep postgresql.image.tag on PostgreSQL 17 and the Debian 12 (bookworm) base. A major-version change needs a dump and restore or pg_upgrade, and a different base image can change text sort order and corrupt existing indexes.
If you render the chart without cluster access (helm template, Argo CD, Flux), set postgresql.auth.postgresPassword or use postgresql.auth.existingSecret. Otherwise the chart cannot read the stored superuser password and writes a new random one to its Secret on every sync, while the database keeps the original.
The chart sets max_connections = 300 on the bundled server. The bundled database is a single instance. For production, prefer a managed database. See Day 2 operations for backups.
config.database.idleSessionTimeoutMs sends PostgreSQL’s idle_session_timeout parameter, which exists only on PostgreSQL 14 and newer. On older servers every connection fails. The chart turns it on (10 minutes) only for the bundled database. For an external or IAM-authenticated database, set it yourself only if you know the server is 14 or newer.

Connection pool sizing

Each Uvicorn worker keeps its own connection pool. The most connections BOW can open is:
With the defaults (20 + 20 per worker, up to 4 workers per pod), one pod can open 160 connections. Keep the total under your server’s max_connections minus its reserved superuser slots, or new connections are refused and agent runs fail. Tune it with:

Ingress and TLS

Also set config.baseUrl to the public URL (for example https://bow.example.com). BOW uses it to build links in emails and OAuth callback URLs. HTTP only, custom hostname:
TLS with cert-manager and larger uploads (nginx):
Behind a proxy: set config.forwardedAllowIps to your ingress controller’s pod CIDR (comma-separated IPs or CIDRs). Without it, BOW sees every request as coming from the ingress controller and all anonymous users share one rate limit.

Secrets

Values that start with ${ are placeholders. The chart does not put them in the ConfigMap, and BOW reads the real value from the environment at startup. Put the real values in the Secret named by config.secretRef, using these keys: If you set a sensitive value as plain text (for example config.googleClientSecret=abc), the chart stores it in the ConfigMap and prints a warning.

Authentication

config.authMode controls sign-in: hybrid (default, local accounts and SSO), local_only or sso_only. Related flags: config.allowUninvitedSignups, config.allowMultipleOrganizations and config.verifyEmails (all false by default).
See Google OAuth for the callback URL and scopes.
Optional per-provider keys: redirectPath, extraAuthorizeParams and extraTokenParams. See OpenID Connect for provider-side setup.
Put BOW_LDAP_BIND_PASSWORD in the Secret. Other keys, with their defaults: startTls (false), kerberosRealm, allowLocalSuperuserLogin (false), userSearchBase, userSearchFilter ((objectClass=person)), userEmailAttribute (mail), userNameAttribute (displayName), groupSearchBase, groupSearchFilter ((objectClass=group)), groupNameAttribute (cn), groupMemberAttribute (member), groupMemberFormat (dn or uid), connectionTimeout (10) and pageSize (500).

Email (SMTP)

Setting config.smtp.host also enables SMTP. If enabled is true and host is empty, the install fails. See SMTP Settings for what email is used for.

Persistent storage for uploads

The chart does not mount a volume for uploaded files or branding assets. Without one, they are lost when a pod is rescheduled. Create a PersistentVolumeClaim and mount it:
If you run more than one replica, the claim must support ReadWriteMany (for example EFS, Filestore or Azure Files).

Scaling and availability

Running more than one replica needs PostgreSQL and shared upload storage. Read High availability before you scale out.

Observability

Send traces to an OpenTelemetry collector:

Other values

To see every value with its default:

Update

With a pinned tag, change image.tag in your values file and run the same upgrade command:
If you use the default latest tag, restart the deployment to pull the newest image:
Migrations run automatically on startup and cannot be rolled back. Back up the database first. See Upgrades.

Upgrading from chart 2.x

Chart 3.0.0 replaces the Bitnami PostgreSQL subchart with the chart’s own PostgreSQL, running the official postgres:17 image. Bitnami no longer publishes the image that chart 2.x used, so new 2.x installs cannot start their database. Your data is reused in place. The StatefulSet name, volume (data-<release>-postgresql-0), on-disk layout and Secret keys are the same, so the upgrade restarts the database pod on the new image against the same volume. Expect about a minute of database downtime. There is no rollback to 2.x.
1

Back up the database

Snapshot the data volume or run pg_dump. This backup is your only way back if something goes wrong.
2

Clean up your values file

Remove Bitnami-only keys under postgresql:, such as architecture, global, primary.resourcesPreset, metrics and readReplicas. Also remove any postgresql.image override that points at bitnami or bitnamilegacy. The chart now rejects them.If you changed the volume’s storage class in 2.x (for example primary.persistence.storageClass or global.storageClass), set the same value under postgresql.primary.persistence.storageClass.
3

Upgrade with the migration flag

Without this flag, the upgrade stops on purpose while the release still runs the Bitnami database, so an unpinned pipeline cannot migrate your database unattended. You only need it once. Leaving it set afterwards is harmless.
The safety check needs cluster access, so it does not run under helm template or Argo CD. Pin the chart there (for example targetRevision: 2.*) until you are ready to migrate.
If the upgrade fails with updates to statefulset spec for fields other than ... are forbidden, delete only the StatefulSet object (the volume and data are kept) and upgrade again:
If you used AWS IAM authentication on 2.x, the upgrade removes the unused Bitnami database. Its volume is kept, so delete it yourself once you are sure you don’t need it.

Upgrading from chart 1.x

If you are on 1.x, follow these steps first and then Upgrading from chart 2.x. Chart 2.0.0 changes the Deployment’s selector labels to app.kubernetes.io/name and app.kubernetes.io/instance. Kubernetes does not allow selectors to change, so a plain helm upgrade fails with field is immutable: spec.selector. Delete the Deployment, then upgrade. The app is unavailable until the new pod is ready. The database and its volume are not affected.
With Argo CD, you can instead add the sync option argocd.argoproj.io/sync-options: Replace=true to the Deployment. Also update your values file:

Uninstall

The bundled PostgreSQL volume (data-bowapp-postgresql-0) is kept. Delete it with kubectl delete pvc only if you no longer need the data.

AWS Aurora Configuration


BOW supports connecting to AWS Aurora PostgreSQL using IAM database authentication. This eliminates static database passwords entirely — the application generates short-lived tokens (valid for 15 minutes) at connection time using AWS IAM.

How it works

  1. The app’s service account assumes an IAM role (via IRSA in EKS, or instance profile on EC2)
  2. On every new database connection, the app calls generate_db_auth_token() to get a temporary password
  3. The token is used as the PostgreSQL password — established connections are not affected when it expires
  4. SSL is required (require or verify-full)

AWS Setup

1. Enable IAM authentication on the Aurora cluster:
2. Create the database user with IAM grants:
3. Create an IAM policy allowing rds-db:connect:
4. For EKS — create an IAM role for the service account (IRSA): The Helm chart creates the bowapp ServiceAccount itself, so create only the IAM role with eksctl:
Then set the role on the chart’s ServiceAccount:
See the AWS Aurora / RDS with IAM tab for the full Helm values.

bow-config.yaml for Aurora (non-Kubernetes)

If you are running BOW on EC2 or ECS (not Kubernetes), you can configure Aurora IAM auth directly in bow-config.yaml:
The EC2 instance or ECS task must have an IAM role with the rds-db:connect policy attached.

Google OAuth

To enable Google OAuth authentication, configure the following parameters in your bow config (or in env/k8s configmap):
You should also set the following in your Google OAuth configurations:
  1. Callback URL: https://yourbaseurl.com/api/auth/google/callback
  2. Scopes: /auth/userinfo.email, /auth/userinfo.profile, openid
  3. Enable People API

OpenID Connect (OIDC)

For Okta

  1. Set a new OIDC application: web
  2. Set callback URL https://your-base-bow-url.com/api/auth/okta/callback

For Microsoft Entra ID (Azure AD)

  1. In the Azure portal, register a new application under Microsoft Entra ID → App registrations.
  2. Set the redirect URI (type: Web) to https://your-base-bow-url.com/api/auth/entra/callback.
  3. Copy the Directory (tenant) ID into ${ENTRA_TENANT_ID} and the Application (client) ID into ${BOW_ENTRA_CLIENT_ID}.
  4. Under Certificates & secrets, create a client secret and set it as ${BOW_ENTRA_CLIENT_SECRET}.
  5. Use client_auth_method: post — Entra ID expects credentials in the request body rather than the Authorization header.
Group sync When sync_groups: true, group memberships from the groups claim are synced to BOW on each login. To make Entra ID emit the claim, edit the app’s Token configuration → Add groups claim and select the group types you want.
  • group_claim — the token claim that carries group membership (default groups).
  • resolve_group_names: true — by default Entra ID emits group object IDs, not display names. Enabling this resolves the IDs to human-readable names via Microsoft Graph (requires the app to have the GroupMember.Read.All or Directory.Read.All permission granted).

SAML 2.0

For identity providers that require SAML, add a saml_providers entry. base_url must be an HTTPS origin.
Register https://your-base-bow-url.com/api/auth/saml/company-sso/acs as the Reply (ACS) URL and https://your-base-bow-url.com/saml/company-sso as the Entity ID. See Sign-in and SSO for trust options, admission rules, and troubleshooting.

LDAP / Active Directory

For environments that authenticate against an on-premises directory, enable the LDAP provider:
  • url — the directory endpoint. Use ldaps:// (port 636) for TLS; avoid plain ldap:// (port 389) outside a trusted network.
  • bind_dn — the distinguished name of a read-only service account used to look up users.
  • bind_password — the service account password, supplied via the ${BOW_LDAP_BIND_PASSWORD} environment variable rather than committed to the config.

SMTP Settings

Configure an outbound mail server to enable email features — dashboard and conversation share notifications, scheduled report and prompt results (with optional PDF attachments), password-reset and email-verification links, and the AI send_email tool. Define the smtp_settings block in bow-config.yaml:
Email is enabled only when credentials are present (or use_credentials: false for anonymous relays). If no mail client can be built at startup, email features are silently inert — outgoing notifications are skipped and the send_email tool stays disabled.