Self-hosting
Two minutes
OpenCalc runs on your own servers. Nothing phones home, there is no licence check, and no document leaves your network.
# from a clone of the repository cp .env.example .env # change OPENCALC_SHARED_SECRET — the file ships with a value the server refuses docker compose up open http://localhost:8080
The images are published, so this pulls rather than builds:
casualoffice/calc is the collaboration server,
casualoffice/calc-host serves the editor and keeps the
documents, and casualoffice/wopi is the adapter for a host
that speaks WOPI. Each is multi-arch, and each ships an SBOM and a
build-provenance attestation naming the repository, workflow and commit
it was built from — so what you pull can be traced to what is here.
Add --build to build from your own checkout instead.
New spreadsheet → Share → send the link to somebody else. You are both
editing the same sheet. Download takes away an .xlsx
containing what you both typed.
Change the secret first. .env.example is a public file, so
the value in it is not a secret and the collaboration server rejects it by
name at startup rather than booting into a deployment anybody can mint tokens
for. openssl rand -hex 32 is enough.
What the pieces are
| Container | What it does | Who supplies it in the end |
|---|---|---|
host | Documents, identity, tokens, saving | You. This is your product |
collab | Ordering, presence, the WebSocket | OpenCalc |
proxy | Puts the page and the socket on one origin | Yours, or use ours |
wopi | Makes OpenCalc appear inside Nextcloud, SharePoint and friends | OpenCalc, optional |
That split is the integration boundary, not an artefact of the demo. The collaboration server holds no per-document state and deliberately cannot mint tokens. Per join, it is told by a party that already knows: where the file lives, where the finished bytes go, who is joining and what they may do.
server/casual-calc-host is that party, written small enough to read
in one sitting. When you integrate, you replace it — the collaboration server and
the editor stay as they are.
Standalone or cluster
Two stacks, both docker compose up.
# one collaboration node — what most deployments should run docker compose up # :8080 # two nodes behind a proxy, coordinating through Redis docker compose -f docker-compose.cluster.yml up # :8090
The cluster buys horizontal capacity, not correctness. Standalone is a first-class mode and not a degraded one, which is a deliberate architectural position rather than a limitation we have not got round to.
The host contract
Four endpoints and a signature is the whole of it.
GET /api/documents/{id}/content # what the collaboration server fetches
POST /api/documents/{id}/callback # where the finished bytes come back
GET /api/documents/{id}/session # a minted token, for the browser
GET /api/documents/{id}/download # what a user takes away
Implement those four against your own storage and identity, and you have replaced the reference host. Nothing else in the system needs to know.
The two addresses that get confused
This is the most common first failure, and it produces an editor that says connecting forever with nothing in any log.
| Variable | Who dials it | What to set |
|---|---|---|
OPENCALC_COLLAB_WS |
The browser | Usually leave it unset. The host derives it from the request the browser just made — same origin, /collab, and wss:// when X-Forwarded-Proto says the page arrived over TLS. Set it only when collaboration lives on a hostname of its own. |
OPENCALC_HOST_INTERNAL |
The collaboration server | What that server calls your host. On a compose network, a service name — http://host:8080. Point it at localhost and the server fetches itself. |
They look interchangeable and are on different networks. If you do set
OPENCALC_COLLAB_WS, the /collab path is added when you
leave it off, because the server serves that path and nothing else — an endpoint
without it is an address nothing answers.
Tokens and identity
Your host mints a short-lived JWT per join; the collaboration server verifies it and cannot produce one. The claims are the whole contract: who the user is, which document, where its bytes live, where to post them back, and what this person may do.
| Variable | Meaning |
|---|---|
OPENCALC_SHARED_SECRET | HS256, the same string on both sides. Fine for a demo. It means the collaboration server can mint tokens as well as check them, and it says so loudly at boot. |
OPENCALC_JWKS_URL | What a real deployment should use. The server fetches your public keys and can verify a token without being able to forge one. Keys are re-read periodically, and a token naming an unknown kid prompts one refresh — throttled, so an invented kid cannot make the server hammer your endpoint. |
OPENCALC_ISSUERS | Multi-tenant. issuer=https://…/jwks.json, comma-separated. Each tenant gets its own key set, and a token is checked against the keys of the issuer it names. This is the only configuration in which the issuer is a boundary: with one shared key set, any tenant holding a trusted key can mint a token naming any other tenant. |
OPENCALC_ISSUER | Pins the expected iss for a single-tenant deployment. Refuses a token the same signer minted for somebody else. It does not make the deployment multi-tenant — one key set cannot. |
OPENCALC_AUDIENCE | The audience a token must name. Any string, the same on both sides. |
OPENCALC_TOKEN_LEEWAY_SECS | Clock skew allowed when checking expiry. Defaults to 60. |
Access is view, comment or edit, and it is
enforced at the operation, on the server. A client that hides its toolbar
and a server that enforces nothing look identical until somebody edits anyway.
Host variables
| Variable | Default | What it is |
|---|---|---|
OPENCALC_HOST_BIND | 0.0.0.0:8080 | Where the host listens. |
OPENCALC_STORE | — | Directory for documents and their metadata. A volume in compose. |
OPENCALC_SHARED_SECRET | — | Signs the tokens it mints. See above. |
OPENCALC_AUDIENCE | opencalc-demo | Audience claim. |
OPENCALC_HOST_INTERNAL | — | What the collaboration server calls this host. |
OPENCALC_COLLAB_WS | derived | What the browser dials. Usually unset. |
OPENCALC_MAX_UPLOAD_BYTES | 67108864 (64 MB) | Largest workbook accepted. The web framework's own default is 2 MB, which is smaller than many real spreadsheets. |
OPENCALC_ADMIN_TOKEN | unset | Switches /admin on. Unset means there is no admin page at all. |
<NAME>_FILE | Any secret can be read from a file instead: OPENCALC_SHARED_SECRET_FILE, OPENCALC_ADMIN_TOKEN_FILE, OPENCALC_REDIS_URL_FILE. Use it. An environment variable is readable in docker inspect, in /proc/1/environ, and in whatever your process manager logs. Setting both forms is refused, not ranked — a silent precedence rule is how a deployment that believes it moved to files keeps running on the variable it forgot to delete. | |
OPENCALC_FONT_DIR | unset | Extra faces for the headless renderer. See Fonts. |
OPENCALC_EDITOR_DIR | /editor | Where the editor bundle is served from. |
Collaboration variables
The ones an operator actually changes:
| Variable | What it is |
|---|---|
OPENCALC_BIND | Public listener. |
OPENCALC_INTERNAL_BIND | A second listener for metrics and health, so they need not be exposed with the socket. |
OPENCALC_ALLOWED_HOSTS | Hosts this server will fetch documents from and post results to. A token names a URL, so this is what stops a forged one pointing it anywhere. |
OPENCALC_ALLOW_PLAIN_CALLBACKS | Permits plain http for those calls. Local development only; off by default. |
OPENCALC_MAX_DOCUMENTS, OPENCALC_MAX_PARTICIPANTS | Capacity per node. |
OPENCALC_MAX_DOCUMENT_BYTES, OPENCALC_MAX_MESSAGE_BYTES | Size bounds on what it will hold and accept. |
OPENCALC_MAX_PENDING_CONNECTIONS, OPENCALC_JOIN_TIMEOUT_MS | A WebSocket upgrade cannot require a token — the token arrives in the first frame — so these bound how many sockets may be open and silent. Separate from MAX_PARTICIPANTS, which counts people in a document. |
OPENCALC_IDLE_EVICTION_MS | How long a document with nobody in it stays resident. |
OPENCALC_CLIENT_PING_MS, OPENCALC_CLIENT_IDLE_MS | How the server decides a participant is gone. See the proxy note below. |
OPENCALC_DRAIN_TIMEOUT_MS, OPENCALC_DRAIN_DEADLINE_MS, OPENCALC_DRAIN_CONCURRENCY | Shutdown: how long it will spend saving open documents before it stops. |
OPENCALC_TLS, OPENCALC_TLS_CERT, OPENCALC_INTERNAL_TLS | Terminate TLS in the process rather than at a proxy. |
OPENCALC_TRUSTED_PROXIES, OPENCALC_TRUST_ANY_PROXY | Whose X-Forwarded-* headers to believe. Believing anybody's is how a client picks its own address. |
OPENCALC_REDIS_URL, OPENCALC_REDIS_NAMESPACE, OPENCALC_NODE_ID, OPENCALC_ADVERTISE, OPENCALC_LEASE_MS | Cluster only: coordination, identity and how long a node's lease on a document lives. For a replicated coordinator point the URL at the sentinels rather than a box — redis+sentinel://h1:26379,h2:26379,h3:26379/opencalc, or rediss+sentinel://… over TLS. The address is then re-asked on every failure instead of fixed at startup, so a promotion is found — including the case where the old primary is demoted rather than killed and would otherwise refuse every write forever on a socket that looks perfectly healthy. |
OPENCALC_PUBLIC_URL | Cluster only, and optional. Where a browser reaches this node — wss://node-2.example/collab — which is deliberately not OPENCALC_ADVERTISE: that one is a service name on the cluster network, the address a client cannot use. Set it and a node at its document cap answers a refusal with the least-loaded peer that has room, instead of only saying no. Leave it unset behind a single load balancer, where a redirect would come straight back through the balancer to an arbitrary node; placement then does nothing, which is the honest outcome rather than a guess. |
OPENCALC_REDIS_MIN_REPLICAS | How many in-sync replicas the coordinator must be configured to require. Redis replication is asynchronous, so a failover can drop an append the primary already said was saved; min-replicas-to-write turns that window into a refusal instead — clients are told NotSaving and /readyz answers 503. Set this to the same number and the node checks the setting is really there, at startup and again after every failover: it is per server, and the usual mistake is configuring the original primary and nothing else, which passes a startup check and fails exactly when it matters. The price, stated rather than hidden: a failover leaving no replica in sync becomes a visible write outage. Unset means unchecked, which is right for a single-box coordinator. (ADR-020) |
OPENCALC_REDIS_CA, OPENCALC_REDIS_CLIENT_CERT, OPENCALC_REDIS_CLIENT_KEY | Encrypting the coordinator link. Use a rediss:// URL, and OPENCALC_REDIS_CA for a private CA — which is the normal case, since an internal Redis has no public certificate. The two client settings are mutual TLS and are both-or-neither. Certificates configured against a plain redis:// URL are refused at startup: that shape reads as encrypted and is not. |
Everything is read once at startup and checked before the listener opens. A node that starts and is subtly wrong costs more than one that refuses to start and says why.
WOPI variables
| Variable | What it is |
|---|---|
OPENCALC_WOPI_ALLOWED_HOSTS | Required, no default, and the process will not start without it. A WOPISrc arrives in a query string, so it is chosen by whoever wrote the link the user clicked. Without a list, the service fetches whatever that link says — a server-side request forgery with a friendly interface. |
OPENCALC_WOPI_PUBLIC_URL | Where a browser reaches the adapter. It goes into the discovery document, so it is the address the file store sends people to. Behind a proxy it is never the bind address. |
OPENCALC_WOPI_INTERNAL_URL | Where the collaboration server reaches it, which is not the same thing. |
OPENCALC_WOPI_SESSION_TTL_MS | How long an editing session — and the WOPI lock it holds — lives. Default 8 hours. A lock left behind makes somebody's file read-only in their own file store. |
OPENCALC_WOPI_MAX_SESSIONS | Default 500. |
OPENCALC_WOPI_PROOF_KEY | A PKCS#8 private key, as a file path so the secret arrives on a mounted file rather than an environment variable. Signs every outgoing request so a host can prove it came from this editor. Optional; a key that will not load is fatal at boot rather than a warning. |
OPENCALC_WOPI_ALLOW_PLAIN | Permits a plain http WOPISrc. Local development only. |
OPENCALC_BRAND_NAME, OPENCALC_BRAND_FAVICON_URL, OPENCALC_BRAND_ACCENT | White-labelling: the entry in the file store's editor list, the browser tab and the toolbar. |
Changing things while it runs
/admin is off entirely unless OPENCALC_ADMIN_TOKEN
is set. There is no default password — a demo that ships with a known one is a
demo somebody exposes to the internet once and then explains.
It changes what can safely move under a live server: the endpoint new sessions are given, whether uploads are offered, the banner. Settings are written beside the documents, so they survive a restart.
Fonts
Only the headless renderer needs these — thumbnails and PNG export. The editor never does; the browser draws cells with its own faces.
Latin is built in. Drop a .ttf, .otf or
.ttc into OPENCALC_FONT_DIR for any other script and it
is registered at boot. Empty is fine and is the normal case.
Behind a reverse proxy
Working configurations for nginx and Caddy are in
deploy/.
Two things are not obvious, and each produces the same symptom — an editor that
reconnects forever:
-
The WebSocket upgrade headers
Without
UpgradeandConnection, the handshake is answered with a plain200. -
A read timeout longer than the server's client ping
A co-editing socket is idle whenever nobody is typing, and nginx closes idle proxied connections after sixty seconds by default. The server should decide when a participant has gone; the proxy deciding for it looks like a network fault to everybody in the document.
Serve everything from one origin — editor, API and WebSocket. A share link
is then a single URL, with no CORS to configure and no second certificate. The
standalone stack does this itself: docker compose up starts an nginx
in front of both services, so the page and the socket share a hostname.
TLS
Terminate at your proxy, which is what most deployments already do, and forward
X-Forwarded-Proto so the host hands out wss:// rather
than ws://. An https page cannot open a
ws:// socket at all, and the browser's refusal is the only thing that
reports it.
If you would rather the collaboration server terminate TLS itself, point
OPENCALC_TLS_CERT at a certificate and key. Set
OPENCALC_TRUSTED_PROXIES to the proxies whose forwarding headers you
are willing to believe — believing anybody's lets a client choose its own
apparent address.
Health and metrics
| Endpoint | Answers |
|---|---|
/healthz | Is the process alive. |
/readyz | Is it ready to take work. |
/stats | Documents and participants, as JSON — "is it working now", for a person. |
/metrics | Prometheus text — "has it been working", for a machine. |
The counters worth putting on a dashboard:
| Metric | Answers |
|---|---|
opencalc_saves_accepted_total / _failed_total | Are documents getting back to the host? |
opencalc_save_duration_milliseconds_total | Divided by the counts, how slow is your callback? |
opencalc_fetches_ok_total / _failed_total | Can the server reach your host at all? |
opencalc_documents_unreadable_total | Is the host answering 200 with something that is not a workbook? |
opencalc_revisions_total | Is anything being edited? Counts operations, so it moves in step with the revision number rather than with submissions. |
opencalc_connections_refused_pending_total | Is the node full while still answering /healthz? |
opencalc_joins_refused_capacity_total | Are arrivals being turned away by a cap? |
opencalc_joins_redirected_total | Capacity refusals that named a node with room. A subset of the refusals above — the client is still refused. The gap between the two is the number worth alerting on: refusals naming nowhere mean the cluster is full, or that nobody set OPENCALC_PUBLIC_URL and placement is silently doing nothing. |
opencalc_slow_consumers_total | Are clients being dropped for lagging? Survivable, but silent. |
opencalc_appends_refused_total | Cluster: is a fenced or stale leader still trying to write? |
opencalc_documents / opencalc_participants | Current load, as gauges. |
Alert on saves_failed_total increasing first. It is the only
counter that means work is at risk, rather than that something is merely busy.
Storage and backup
The reference host keeps each document and its metadata as files under
OPENCALC_STORE, which compose mounts as a named volume. Back that
directory up and you have backed up every document.
When you replace the reference host with your own, this stops being ours: storage, retention and backup become whatever your product already does. That is the point of the boundary.
The reference host keeps the previous contents of a document each time it is
saved, bounded by OPENCALC_MAX_VERSIONS (default 20). They are
listed, downloadable and restorable:
GET /api/documents/{id}/versions # newest first
GET /api/documents/{id}/versions/{at} # look at one
POST /api/documents/{id}/versions/{at}/restore
Restoring keeps the current document as a version first, so a restore is itself undoable — the one button whose purpose is undoing a mistake should not be the only one that cannot be undone.
A restore lands on the stored file, not on open sessions. Anyone with the document open is still editing what was there before, and their next save overwrites the restore. Everyone should reload. Making a restore reach live sessions means the collaboration server evicting the document, which is a change to a boundary that is deliberate — it holds no per-document state.
When you replace the reference host with your own, this stops being ours: retention and restore become whatever your product already does, and the callback is where each finished version arrives.
Sizing and performance
Measured, not modelled — and labelled where the two differ.
Two machines, and they are not the same one. Memory comes from the
Linux CI runner, which is the only machine here with /proc. The
timings come from an Apple M-series laptop in a release build. Treat the
timings as shape and headroom rather than as a spec for your hardware.
What the engine costs, per operation
| Operation | Measured | Budget | Headroom |
|---|---|---|---|
| Cell edit, kept dependency graph | 0.13 – 0.25 µs | — | — |
| Range edit, kept graph | 1.5 – 2.0 µs | — | — |
| Visible-window repaint (1600×900) | 0.09 ms | ≤ 8 ms engine-side | ~86× |
| Recalc, cold first edit | 2.2 ms | < 50 ms | ~23× |
| Recalc, adversarial chain + fan-out | 3.1 ms | < 50 ms (T3 cap) | ~16× |
| Snapshot round trip, 10 000 cells | 2.9 ms | not interactive | — |
Open a small .xlsx package | 9 µs | — | — |
The adversarial recalc case is the one that matters: a deep dependency chain and a wide fan-out, which is the shape a worst case takes. At 3.1 ms against a 50 ms cap, recalculation is not close to being the limit.
What a sheet costs in memory
83.64 bytes per cell, measured as the peak resident-set delta for a 50 000-cell build on the CI runner.
| Sheet | Memory |
|---|---|
| 1 000 cells | ~84 KB |
| 10 000 cells | ~0.8 MB |
| 100 000 cells | ~8.4 MB |
| 1 000 000 cells | ~84 MB |
That figure is numeric cells. Text interns, so repeated strings are cheap — but a formula holds an AST per cell, and a formula-heavy sheet costs materially more. How much more is not yet measured. Size from your own documents if they are formula-dense.
What a live document costs on the server
The per-cell figure above is the workbook. A document that is open costs more than its cells: an operation log, participant state, and the broadcast path to everyone in it. That overhead is now measured — 190 KB per open document at three editors — from a sweep against a Linux container, with the node reporting its own resident set.
| Documents | Editors | Edits | p50 | p99 | Node RSS | Per document |
|---|---|---|---|---|---|---|
| idle | 0 | — | — | — | 6.6 MB | — |
| 10 | 30 | 1 200 / 1 200 | 27 µs | 1.5 ms | 11.6 MB | 514 KB |
| 25 | 75 | 3 000 / 3 000 | 16 µs | 2.8 ms | 12.4 MB | 235 KB |
| 50 | 150 | 6 000 / 6 000 | 18 µs | 1.1 ms | 15.9 MB | 188 KB |
Every edit was acknowledged at every step — 10 200 of 10 200. The per-document column falls as documents are added because it still carries a share of the 6.6 MB a node costs when empty; the marginal cost between the 25- and 50-document runs is 143 KB. Round to 190 KB and it is a safe number to size with.
This corrects the sizing advice that was on this page. Sizing from cells alone is fine for large sheets and badly wrong for small ones: at 1 000 cells the overhead is 70% of the total, so a deployment of many small documents was under-sized by 3.3×. At 100 000 cells it is 2% and does not matter. If your documents are small, the document count is what you are sizing for, not the cell count.
Latency is a Docker Desktop VM on a laptop, so treat p99 as an upper bound rather
than a specification — it did not degrade between 50 and 150 concurrent editors,
which is the part that matters. RSS is the figure to trust: it is read from the
node's own opencalc_resident_bytes gauge inside the container.
Nodes, sheets and people
At three concurrent editors per sheet, and with the default
OPENCALC_MAX_DOCUMENTS of 1 000 per node:
| RAM for documents | 1k-cell sheets | 10k-cell sheets | 100k-cell sheets | 1M-cell sheets |
|---|---|---|---|---|
| 4 GB | ~15 400 · 46 200 users | ~4 150 · 12 400 users | ~500 · 1 500 users | ~51 · 153 users |
| 8 GB | ~30 800 · 92 500 users | ~8 300 · 24 900 users | ~1 000 · 3 000 users | ~102 · 306 users |
| 16 GB | ~61 600 · 185 000 users | ~16 600 · 49 800 users | ~2 000 · 6 000 users | ~204 · 612 users |
Nothing here is a configured ceiling. There is no cap on documents or on people; a node admits work until its memory is short, and the figures above are what that works out to. It scales by adding nodes — a document is owned by exactly one node at a time, so nodes do not contend for it.
Admission stops at 85% of the node's limit, not at 100%. Reaching the
cgroup ceiling is an OOM kill, and that takes every document on the node
— including the unsaved work of everybody in them. Refusing one new document
costs one person a click. The limit is read from the container's cgroup, not
from the host: a 2 GB container on a 64 GB machine is a 2 GB node.
OPENCALC_MEMORY_HIGH_WATER_PERCENT moves the mark between 50 and 95.
A rule of thumb
If you want one line to size a node from, this is it:
# RAM for documents, in GB GB = users ÷ 3 × (average cells per sheet × 84 bytes + 190 KB) × 1.2 # or, backwards — how many people a node holds users = GB × 3 ÷ ((average cells × 84 bytes + 190 KB) × 1.2)
÷ 3 is the editors-per-sheet assumption — change it to yours.
84 bytes is the measured per-cell cost and 190 KB the measured
per-document one; both are figures this project ran for, not estimates.
×1.2 is what is left of the headroom once the overhead is a term rather
than a guess — it covers the 85% admission mark and nothing else.
The previous form of this line multiplied cells alone by a generous ×1.4 to stand
in for an unmeasured overhead, and that stand-in was too small by 3× for small
documents. Add the 190 KB; do not fold it into a fudge.
Worked ranges
At three editors a sheet, sized with the headroom above. Pick the row that matches your documents, not your ambitions — sheet size moves this far more than user count does.
| Concurrent users | Sheets | Small (10k cells) | Typical (50k cells) | Large (250k cells) |
|---|---|---|---|---|
| 30 | 10 | < 0.1 GB | ~0.06 GB | ~0.3 GB |
| 150 | 50 | ~0.06 GB | ~0.3 GB | ~1.5 GB |
| 300 | 100 | ~0.12 GB | ~0.6 GB | ~3 GB |
| 1 500 | 500 | ~0.6 GB | ~3 GB | ~15 GB |
| 3 000 | 1 000 | ~1.2 GB | ~6 GB | ~30 GB |
| 15 000 | 5 000 | ~6 GB | ~30 GB | ~147 GB · split across nodes |
Start at 2 GB and watch. Nearly every deployment below a few hundred
people fits there, and the two gauges on /metrics —
opencalc_documents and opencalc_participants — tell
you what is actually resident rather than what a table predicted. Size from
those after a week; the table is for the first day.
Add nodes rather than growing one past about 16 GB. A document is owned by exactly one node, so capacity adds up cleanly, and one large node is a single thing to lose.
The engine is not what runs out first
Worth stating plainly, because it is where sizing intuition usually goes wrong. Three thousand editors each committing two edits a second is 6 000 edits a second; at 0.25 µs of engine time each, that is 1.5 ms of CPU per wall second — about a sixth of one percent of a single core.
What you will actually run out of is memory, and then socket and broadcast work. Provision for resident documents and connection count, not for calculation.
What is not measured, and so is not claimed. There is no server-side
load harness: the numbers above are the engine measured directly, not a node
under sustained multi-user load. Per-document overhead in the collaboration
server — the operation log, participant state — is on top of the per-cell
figure and is unmeasured. Nothing here is a substitute for watching
opencalc_documents and opencalc_participants on your
own traffic.
Nextcloud, SharePoint, Moodle, ownCloud, Alfresco
The WOPI adapter is what makes OpenCalc installable into a file store: an administrator pastes one URL into their settings and OpenCalc appears in the editor list.
docker compose --profile wopi up -d
# then give your file store:
https://your-host/hosting/discovery
A file opened that way is fetched from the store, edited — with everybody who
opens it in the same session — and written back under its own name, in the format
it arrived in. .xlsx, .csv, .tsv and
.psv round-trip today. .ods does not yet, and is
deliberately absent from the advertised list rather than accepted and silently
converted.
Failures worth knowing about first
| Symptom | Almost always |
|---|---|
| The editor says connecting forever | OPENCALC_HOST_INTERNAL pointing at localhost, so the collaboration server fetches itself. Or a proxy without the upgrade headers. |
| It works on your machine and nowhere else | OPENCALC_COLLAB_WS set to a loopback address. Unset it and let the host derive it. |
| Everybody drops out after about a minute | The proxy's read timeout is shorter than the server's client ping, so the proxy is deciding who has left. |
An https page never connects | X-Forwarded-Proto is not reaching the host, so it is handing out ws://, which the browser will not open from an https page. |
| The server refuses to start and names the secret | Working as intended — .env.example's value is public and is rejected by name. |
| The WOPI container will not start | OPENCALC_WOPI_ALLOWED_HOSTS is empty. It has no default on purpose. |
| Uploads over a couple of megabytes fail | OPENCALC_MAX_UPLOAD_BYTES, and your proxy's own body limit, which is a separate number. |
| Non-Latin text is boxes in a PNG export | No face for that script in OPENCALC_FONT_DIR. The editor is unaffected. |
What is not done yet
- No Helm chart or Kubernetes manifests. Compose is the supported path today.
- Redis in the cluster stack is a single unreplicated instance. Fine for capacity, not yet an availability story.
- Secrets are environment variables and mounted files — there is no integration with a secret manager yet.
These are tracked in the open, not hidden: the execution tracker lists every one with its priority.
The operator's reference is
docs/65,
the WOPI handshake is in
docs/74,
and every variable's authoritative default is the main.rs of the
binary that reads it — the only copy that cannot go stale.