Klara is four containers plus three data stores, all reachable through one nginx edge. You run everything on a single server with Docker Compose — there's no cluster, no registry, and no dependency on any outside infrastructure beyond the AI and storage providers you choose in Part 2.
postgres · redis · qdrant · admin-postgres sit behind these, reachable only inside Docker's own network
The tenant web app (webapp) is the one exception — it gets its own port rather than routing through the edge above. More on that in Part 3.
Source: KlaraApp/word-addin/nginx.prod.conf, KlaraApp/backend/docker/docker-compose.yml
Before you start
- A Linux server (VM, bare metal, or cloud instance) with Docker and Docker Compose v2 installed
- A domain name you control, with its DNS
Arecord pointed at that server - The Klara source code, delivered to you as a
gitrepository or archive - An Azure Storage account (document uploads currently only support Azure Blob Storage — see Part 2)
- Either an Azure OpenAI resource or AWS Bedrock access, for the AI analysis (see Part 2)
- Optional an Azure AD App Registration, only if you want single sign-on from day one
Get the code onto the server
git clone <the repository URL you were given> klara
cd klara
Everything from here on assumes you're working from the repository root, referred to below as klara/.
Configure the environment
One file holds every setting for the whole stack: KlaraApp/backend/.env. Start from the example and fill it in.
cp KlaraApp/.env.production.example KlaraApp/backend/.env
Then open KlaraApp/backend/.env and work through it in the groups below.
App basics
| Setting | What to put |
|---|---|
FRONTEND_URL | Your real domain, HTTPS, trailing slash — e.g. https://klara.yourcompany.com/. This gets baked into the add-in at build time. |
CORS_ORIGINS | A JSON array with that same origin, no trailing slash — ["https://klara.yourcompany.com"] |
JWT_SECRET_KEY | Generate with openssl rand -hex 32 |
POSTGRES_PASSWORD | A strong password of your choosing |
DATABASE_URL | Update the password inside it to match POSTGRES_PASSWORD exactly — see the warning below |
DATABASE_URL has the word changeme written into it literally — it does not automatically pick up whatever you set POSTGRES_PASSWORD to. Change one and forget the other, and the backend can't log in to its own database.Leave API_BASE_URL alone — the build already fixes it to /api/v1 (a same-origin path, since the add-in and API are served from the same domain through the edge in Part §overview), regardless of what's in this file.
AI provider — pick one
Klara's document analysis runs on either Azure OpenAI or AWS Bedrock (Claude). Fill in one block, leave the other blank:
| Provider | Settings |
|---|---|
| Azure OpenAI | AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT, AZURE_OPENAI_VISION_DEPLOYMENT, AZURE_OPENAI_API_VERSION |
| AWS Bedrock | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, ANTHROPIC_MODEL |
Document storage — required
AZURE_STORAGE_CONNECTION_STRING and AZURE_STORAGE_CONTAINER_PREFIX. As shipped, uploaded documents only support Azure Blob Storage — there's no alternative storage backend documented in this codebase today.
Single sign-on — optional
Leave AZURE_AD_TENANT_ID, AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET, and AZURE_AD_AUTHORITY blank to start with local email/password login only. Fill them in later from an Azure AD App Registration whenever you're ready to turn SSO on.
Admin console
| Setting | What to put |
|---|---|
ADMIN_JWT_SECRET | openssl rand -hex 32 |
ADMIN_SECRETS_MASTER_KEY | openssl rand -base64 32 |
ADDIN_BRIDGE_KEY | openssl rand -hex 32 — a different value than JWT_SECRET_KEY |
ADMIN_PLATFORM_PROVISIONING_KEY | openssl rand -hex 32 |
ADMIN_POSTGRES_PASSWORD | A strong password, separate from the main POSTGRES_PASSWORD |
ADMIN_FRONTEND_BASE_URL | Your domain's origin only, no path — https://klara.yourcompany.com — see the note below |
KlaraApp/.env.production.example now defaults ADMIN_FRONTEND_BASE_URL to your main domain (no subdomain, no path), matching how this compose setup's nginx actually serves the admin console — at /admin/ on the same domain as everything else, confirmed in nginx.prod.conf. The backend uses this value as a CORS allowed-origin (admin-console/backend/src/main/resources/application.yml), and an origin never includes a path. Still worth testing login end-to-end after your first deploy.Source: KlaraApp/.env.production.example, KlaraApp/backend/.env.example, KlaraApp/admin-console/.env.example, KlaraApp/backend/docker/docker-compose.yml
Add a real TLS certificate
The Word add-in build bakes in a self-signed certificate so it runs out of the box in development — Office rejects self-signed certs in production, so this has to be replaced before real users install the add-in.
- Get a certificate for your domain (Let's Encrypt/
certbot, your organization's CA, or one you already hold — this repo doesn't prescribe which). - Name the files
cert.pemandkey.pemand place them on the server at/opt/klara-certs/. - That's it —
docker-compose.prod.ymlalready mounts that exact path into thefrontendcontainer at/etc/nginx/certs, overriding the baked-in self-signed pair automatically.
webapp is exposed on its own port and its nginx only serves plain HTTP inside the container — nothing in this compose file terminates TLS for it. Put your own reverse proxy (or a second certificate on the same box) in front of the webapp port before exposing it to real users; there's no built-in path for that here yet.Source: KlaraApp/backend/docker/docker-compose.prod.yml, KlaraApp/word-addin/nginx.prod.conf, KlaraApp/word-addin/Dockerfile.prod
Build and start the stack
Run everything from KlaraApp/backend/docker/, using both compose files together and the admin profile (it's off by default — without this flag, admin-console never starts).
cd KlaraApp/backend/docker
docker compose --env-file ../.env \
-f docker-compose.yml -f docker-compose.prod.yml \
--profile admin up -d --build
The --env-file ../.env flag points Compose's own variable substitution (the values used for things like the frontend's build args) at the same KlaraApp/backend/.env you just filled in — so you only maintain one file, not two.
First boot builds four images locally (no registry needed) and runs the database migration automatically — the backend's start command is alembic upgrade head followed by the app server. Expect the first run to take several minutes.
Source: KlaraApp/backend/docker/docker-compose.yml, docker-compose.prod.yml
Verify it's running
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile admin ps
# every service should say "running (healthy)" or "running"
curl -sk https://klara.yourcompany.com/health
# backend liveness/readiness, proxied through the edge
curl -sk -o /dev/null -w "%{http_code}\n" https://klara.yourcompany.com/taskpane.html
curl -sk -o /dev/null -w "%{http_code}\n" https://klara.yourcompany.com/admin/
# both should print 200
Source: KlaraApp/word-addin/nginx.prod.conf (routes), Documentation/DEVOPS.md (health endpoints)
First login — and locking it down
In this Docker Compose setup, the demo-tenant bootstrap defaults to on (BOOTSTRAP_ENABLED defaults to true here — unlike the Kubernetes deployment, where it's off by default). That means a built-in login is already active the moment the stack comes up:
| Tenant | system |
|---|---|
superuser@klara.test | |
| Password | ChangeMe123! |
Log in at https://klara.yourcompany.com/admin/super-login and change that password immediately — it's public knowledge, sitting in this same source code, so leaving it as-is is a live risk, not a formality. From there, use the admin console's own on-screen flow to create your organization's real tenant and admin account.
If you don't want the demo tenant at all, set BOOTSTRAP_ENABLED=false in your .env before the first start, and provision your real tenant through the admin console's own onboarding flow instead.
Source: KlaraApp/backend/docker/docker-compose.yml (BOOTSTRAP_ENABLED default), Documentation/deployment.md
Get the Word add-in to your users
Once the manifest is pointing at your live domain (it already is — FRONTEND_URL from Part 2 got baked in at build time), distribute it one of these ways:
- Microsoft 365 Admin Center (recommended) — Settings → Integrated Apps → Upload custom apps → upload the production
manifest.xmland assign it to users or groups. It then just appears in their Word ribbon. - SharePoint App Catalog — upload
manifest.xmlthere; users add it via Insert → Add-ins → My Organization. - Manual sideload — fine for testing a handful of machines, not for a real rollout.
The manifest is served directly from your domain at https://klara.yourcompany.com/manifest.xml.
What this guide doesn't cover
- TLS for the tenant web app. As noted in Part 3,
webapphas no certificate handling built in here — that's on you to add. - Backups. Nothing in this Docker Compose setup backs up
postgresoradmin-postgresautomatically. Both store their data in named Docker volumes on the host — back those up (or the underlying disk) on your own schedule. - Scaling and failover. This is a single-server deployment.
docker-compose.prod.ymlsets every service torestart: always, so Docker brings a crashed container back on the same box — but there's no failover to a second machine if the server itself goes down. - Sizing. No official server-sizing numbers exist for this Docker Compose path. As a rough reference point, the equivalent Kubernetes deployment budgets roughly 250m CPU / 512Mi RAM per backend-type service under normal load — plan for a few GB of RAM and 2+ cores at minimum, plus disk for the Postgres, Qdrant, and document-upload volumes.
Quick reference
| What | Where |
|---|---|
| Environment file (the one you edit) | KlaraApp/backend/.env |
| Example to copy from | KlaraApp/.env.production.example |
| Compose files (run together) | KlaraApp/backend/docker/docker-compose.yml + docker-compose.prod.yml |
| TLS certificate location on the host | /opt/klara-certs/cert.pem and key.pem |
| Word add-in manifest | https://<your-domain>/manifest.xml |
| Admin console | https://<your-domain>/admin/ |
| Tenant web app | http://<your-domain>:5173 (put your own TLS in front) |
| Health check | https://<your-domain>/health |