Deploy, upgrade and diagnose

Goal: run the stack, change it safely, and find out what broke.

Environment variables and ports are in Reference: services. For a first install, follow Get it running instead — this page assumes the stack already exists.

Deploy a change

docker compose up -d --build <service>            # source changed
docker compose up -d --force-recreate <service>   # .env changed
docker compose restart nextcloud                  # PHP annotation or constructor changed
docker compose restart onlyoffice                 # OnlyOffice plugin changed

The three traps, all silent

docker compose up -d alone is not enough. It neither rebuilds on a source change nor recreates on an .env change. The symptom is that you debug behaviour that is not the code you are reading — twice in a row, because the two cases look identical.

PHP annotations are opcache'd. The bind mount updates the file; opcache keeps serving the old compiled version. Anything touching annotations or a constructor signature needs a Nextcloud restart.

${VAR:-} in compose passes an empty string, not nothing, so a ?? default never fires. This broke the orchestrator's boot once via an empty model name, and would have been far worse with an empty context-window override — Number("") === 0, a context window of zero, compaction on every turn, silently. Configuration now treats empty as unset and refuses non-positive numbers.

After touching authentication

./scripts/oidc-verify.sh <user> <password>

It walks the real authorization-code flow and asserts both halves: that the token authenticates the workspace APIs, and that another account's files stay unreachable. It is the only thing that proves the central property still holds, and a change in that area is not finished until it passes.

Find out what broke

One correlation id spans every layer: Nextcloud's own request id, forwarded downstream. The id shown in the interface, the id in Nextcloud's log and the id in the orchestrator's output are the same string — which is why it is shown to users rather than hidden.

# Everything about one failed turn
docker compose logs orchestrator | grep <request-id>
docker exec -u www-data nextcloud-ai-nextcloud-1 php occ log:tail | grep <request-id>
LayerHolds
the interfacea human message and a reference id
Nextcloud's logdeployment faults, token renewal failures
orchestrator outputstructured JSON — status, provider URL, stack; credentials redacted
tool server outputwhich protocol era each request used
the audit databaseevery tool call and why it failed → Data

Credentials are redacted by key name and by value shape, because a token in a log file is a token an attacker can replay. There is a test for it.

Health checks

curl -s localhost:8090/health

# Which models does the gateway actually offer? Ids drift.
docker exec nextcloud-ai-orchestrator-1 node -e "
  import('@ai-sdk/gateway').then(async ({gateway}) => {
    const {models} = await gateway.getAvailableModels();
    console.log(models.map(m => m.id).filter(i => /gemini|claude/.test(i)).join('\n'));
  })"

# Did the MCP tools load from inside the network?
docker compose logs orchestrator | grep "mcp tools loaded"

If every turn fails at once, check the gateway before the code: an expired key, an exhausted budget or a renamed model id all present as a total outage, and the orchestrator reports them faithfully to the log while telling the user only that the assistant is unavailable.

What users see when something breaks

Four messages. No service name, no status code, no host, no model, no stack.

CategoryThe message
workspace unavailable"I couldn't reach your files, calendar or contacts just now. Reconnect Tamebi and try again."
assistant unavailable"The assistant is unavailable for a moment. Try again shortly."
too long"This conversation got too long for me to continue. Start a new one to carry on."
unknown"Something went wrong on my side. Try again — nothing was changed."

Deliberately coarse: more categories means more chances to leak a detail that only means something to us. The full status, URL and stack are kept for the log. That split is the entire point. → why the island uses an allow-list

Run the tests

cd orchestrator   && npm test    # offline
cd mcp-nextcloud  && npm test    # offline
cd mcp-mail       && npm test
cd tamebi-nc-app/frontend && npm test

# Against a live Nextcloud — the ones that prove the invariant
cd mcp-nextcloud && NC_TEST_USER=NC_TEST_PASSWORD=npm run test:integration
cd orchestrator  && NC_TEST_USER=NC_TEST_PASSWORD=npm test

The integration suites skip themselves without a password, so the offline run stays green. They are the only tests that prove:

  • another account's files return 404, and a listing returns nothing
  • an admin-only capability stays refused
  • a request with no credential never reaches Nextcloud
  • an action tool stops for approval and writes zero audit rows

That last one deserves emphasis. A test that only checks "the tool ran" would pass in a world where the approval mechanism is silently disabled — and two mechanisms were silently disabled at one point. → Verified facts

Back up

Three SQLite files hold the conversations, the memory and the audit trail. Their -wal/-shm siblings are normal; a clean shutdown checkpoints them, which is why the process closes the databases on SIGTERM. node:sqlite exposes a backup API, so hot backup needs no external tool — not wired up yet.

The document index is the one you do not need to back up: it is derived from the drive and rebuilds itself. Do treat it as sensitive, though — it holds the extracted text of every document a search has reached.

Fixtures on the development instance

Worth knowing before you wonder where they came from: a spike account, a Deck board named "Tamebi Test", and files prefixed tamebi-test- that the integration suite cleans up after itself.

overwritehost is set instance-wide to the browser-facing port. That is a fix rather than a hack — the browser genuinely uses that port, and OIDC breaks without it — but it changes URL generation everywhere, so know that it is there.