| title | Deployment Options |
|---|---|
| icon | server |
| description | Install Bag of Words on your own server using Docker or Kubernetes |
You can install Bag of Words 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.
docker run --pull always -d -p 3000:3000 bagofwords/bagofwords- Re-run the same
docker run --pull always ...command to fetch and start the latest image. - Optionally, pull explicitly and restart:
docker pull bagofwords/bagofwords:latest # stop/remove your existing container if needed, then start again # docker stop <container_name> && docker rm <container_name> docker run --pull always -d -p 3000:3000 bagofwords/bagofwords
Run Bag of Words with Docker Compose and Caddy (built-in TLS on port 443). We recommend using the canonical files from the repo to avoid drift:
docker-compose.yaml: https://github.com/bagofwords1/bagofwords/blob/main/docker-compose.yamlCaddyfile: https://github.com/bagofwords1/bagofwords/blob/main/Caddyfile
-
Make sure Docker and Docker Compose are installed.
-
Clone the repo:
git clone https://github.com/bagofwords1/bagofwords cd bagofwords -
Create a
.envfile (for domain and credentials). Example:# Domain used by Caddy for HTTPS (must resolve to your server's public IP) DOMAIN=yourdomain.com # PostgreSQL (use stronger values for production) POSTGRES_USER=bow POSTGRES_PASSWORD=your_secure_pw POSTGRES_DB=bagofwords # Optional but recommended: encryption key (Fernet, 44 chars incl. '=') # Generate with OpenSSL: openssl rand -base64 32 | tr '+/' '-_' BOW_ENCRYPTION_KEY=
Generate
BOW_ENCRYPTION_KEYwith OpenSSL:openssl rand -base64 32 | tr '+/' '-_'
-
Start services:
docker compose up -d
-
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.
- Create an A record for
-
Open
https://yourdomain.com
# pull latest images and recreate containers
docker compose pull
docker compose up -dYou can also configure additional settings in the bow-config.yaml file.
# bow-config.yaml
# Deployment Configuration
base_url: http://0.0.0.0:3000
database:
url: ${BOW_DATABASE_URL}
# Feature Flags
features:
allow_uninvited_signups: false
allow_multiple_organizations: false
verify_emails: false
google_oauth:
enabled: false
client_id: ${BOW_GOOGLE_CLIENT_ID}
client_secret: ${BOW_GOOGLE_CLIENT_SECRET}
smtp_settings:
host: "smtp.resend.com"
port: 587
username: "resend"
password: ${BOW_SMTP_PASSWORD}
encryption_key: ${BOW_ENCRYPTION_KEY}
intercom:
enabled: trueTo use the custom config file, you can run the following command:
docker run --pull always -d -p 3000:3000 -v $(pwd)/bow-config.yaml:/app/bow-config.yaml bagofwords/bagofwordsYou can install Bag of Words on a Kubernetes cluster. The Helm chart can deploy the app with a bundled PostgreSQL instance or connect to an external managed database such as AWS Aurora with IAM authentication.
helm repo add bow https://helm.bagofwords.com/
helm repo updateHere are a few examples of how to install or upgrade the Bag of Words Helm chart:
Deploy with a bundled PostgreSQL instance:
helm upgrade -i --create-namespace \
-nbowapp-1 bowapp bow/bagofwords \
--set postgresql.auth.username=<PG-USER> \
--set postgresql.auth.password=<PG-PASS> \
--set postgresql.auth.database=<PG-DB>Deploy without TLS with a custom hostname:
helm upgrade -i --create-namespace \
-nbowapp-1 bowapp bow/bagofwords \
--set host=<HOST> \
--set postgresql.auth.username=<PG-USER> \
--set postgresql.auth.password=<PG-PASS> \
--set postgresql.auth.database=<PG-DB> \
--set ingress.tls=falseDeploy with TLS, cert-manager, and Google OAuth:
helm upgrade -i --create-namespace \
-nbowapp-1 bowapp bow/bagofwords \
--set host=<HOST> \
--set postgresql.auth.username=<PG-USER> \
--set postgresql.auth.password=<PG-PASS> \
--set postgresql.auth.database=<PG-DB> \
--set config.googleOauthEnabled=true \
--set config.googleClientId=<CLIENT_ID> \
--set config.googleClientSecret=<CLIENT_SECRET>When using a managed database like AWS Aurora PostgreSQL, the chart skips the bundled PostgreSQL subchart and connects directly to your Aurora cluster. Passwords are never stored — short-lived IAM tokens are generated at runtime for every new database connection.
Prerequisites:
- An Aurora PostgreSQL cluster with IAM database authentication enabled
- A database user created with:
GRANT rds_iam TO <username> - An IAM role/policy with
rds-db:connectpermission - In EKS: an IRSA (IAM Roles for Service Accounts) annotation on the pod's service account so the app can assume the IAM role
helm upgrade -i --create-namespace \
-nbowapp-1 bowapp bow/bagofwords \
--set host=<HOST> \
--set database.auth.provider=aws_iam \
--set database.auth.region=us-east-1 \
--set database.auth.sslMode=require \
--set database.host=<AURORA-CLUSTER-ENDPOINT> \
--set database.port=5432 \
--set database.username=<DB-USER> \
--set database.name=<DB-NAME> \
--set serviceAccount.annotations.'eks\.amazonaws\.com/role-arn'=arn:aws:iam::<ACCOUNT>:role/<ROLE-NAME>Or use a values file:
# aurora-values.yaml
host: bow.example.com
database:
auth:
provider: aws_iam
region: us-east-1
sslMode: require
host: my-cluster.cluster-xxx.us-east-1.rds.amazonaws.com
port: 5432
username: bow_user
name: postgres
serviceAccount:
name: bowapp
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/bow-rds-role
config:
encryptionKey: "<your-encryption-key>"
baseUrl: "https://bow.example.com"helm upgrade -i --create-namespace \
-nbowapp-1 bowapp bow/bagofwords \
-f aurora-values.yaml# Restart the Bag of Words deployment(s) to pick up the latest image
# Adjust namespace (-n) and selector if you used different names
kubectl rollout restart deployment -n bowapp-1 -l app.kubernetes.io/instance=bowapp
kubectl rollout status deployment -n bowapp-1 -l app.kubernetes.io/instance=bowappBag of Words 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.
- The app's service account assumes an IAM role (via IRSA in EKS, or instance profile on EC2)
- On every new database connection, the app calls
generate_db_auth_token()to get a temporary password - The token is used as the PostgreSQL password — established connections are not affected when it expires
- SSL is required (
requireorverify-full)
1. Enable IAM authentication on the Aurora cluster:
aws rds modify-db-cluster \
--db-cluster-identifier <cluster-id> \
--enable-iam-database-authentication \
--apply-immediately2. Create the database user with IAM grants:
CREATE USER bow_user;
GRANT rds_iam TO bow_user;
GRANT ALL PRIVILEGES ON DATABASE postgres TO bow_user;3. Create an IAM policy allowing rds-db:connect:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": "arn:aws:rds-db:<REGION>:<ACCOUNT>:dbuser:<DB-RESOURCE-ID>/<DB-USER>"
}
]
}4. For EKS — create an IRSA-enabled service account:
eksctl create iamserviceaccount \
--name bowapp \
--namespace bowapp-1 \
--cluster <CLUSTER-NAME> \
--attach-policy-arn arn:aws:iam::<ACCOUNT>:policy/bow-rds-connect \
--approveThen set the IRSA annotation in the Helm chart:
--set serviceAccount.annotations.'eks\.amazonaws\.com/role-arn'=arn:aws:iam::<ACCOUNT>:role/<ROLE-NAME>If you are running Bag of Words on EC2 or ECS (not Kubernetes), you can configure Aurora IAM auth directly in bow-config.yaml:
database:
host: "my-cluster.cluster-xxx.us-east-1.rds.amazonaws.com"
port: 5432
name: "postgres"
username: "bow_user"
auth:
provider: "aws_iam"
region: "us-east-1"
ssl_mode: "require"The EC2 instance or ECS task must have an IAM role with the rds-db:connect policy attached.
To enable Google OAuth authentication, configure the following parameters in your bow config (or in env/k8s configmap):
google_oauth:
enabled: true
client_id: ${BOW_GOOGLE_CLIENT_ID}
client_secret: ${BOW_GOOGLE_CLIENT_SECRET}You should also set the following in your Google OAuth configurations:
- Callback URL:
https://yourbaseurl.com/api/auth/google/callback - Scopes:
/auth/userinfo.email,/auth/userinfo.profile,openid - Enable People API
oidc_providers:
- name: okta
enabled: true
issuer: https://***********.okta.com/oauth2/default
client_id: ${OKTA_CLIENT_ID}
client_secret: ${OKTA_CLIENT_SECRET}
scopes: ["openid", "profile", "email"]
pkce: true
client_auth_method: basic
discovery: true
uid_claim: sub- Set a new OIDC application: web
- Set callback URL
https://your-base-bow-url.com/api/auth/okta/callback