Uh oh!
There was an error while loading. Please reload this page.
feat(mastra-server): self-hosted Mastra server sample, as an alternative to the Helm chart - #662
feat(mastra-server): self-hosted Mastra server sample, as an alternative to the Helm chart#662defang-sam[bot] wants to merge 4 commits into
Conversation
Mastra published a Helm chart for self-hosting the Mastra server on Kubernetes. The chart is Enterprise-only: it is served from a private OCI registry and needs a licence key. This sample deploys the same topology using only the Apache-2.0 parts of Mastra, from one Compose file, with no cluster. The three Mastra samples we already have embed Mastra inside a Next.js app. This one runs the Mastra server on its own, so other applications can call it over HTTP. - Managed PostgreSQL for threads, messages, traces, and workflow state - A managed model, so no provider API key is stored anywhere - Token auth on both the agent API and the Studio UI - A README that maps each Helm chart requirement to its Compose line Co-Authored-By: Claude <noreply@anthropic.com>
Caught by running the built artifact twice, 14 minutes apart. The second run failed to boot. `mastra build` writes its own package.json into .mastra/output and runs a second npm install there. That install does not read package-lock.json, so caret ranges let it resolve different versions on every build. Between the two runs, @mastra/loggers 1.3.0 was published (21:45 UTC). It imports `buildLogRecordData` from @mastra/core, which exists only in 1.63 pre-releases, so it throws against the current stable core 1.62.0. The mastra CLI depends on ^1.2.0, so a fresh install picks up the broken 1.3.0 on its own. Changes: - Pin every Mastra version exactly, so .mastra/output inherits the pins - Drop @mastra/loggers as a direct dependency; the default logger is enough for a sample, and one fewer package is one fewer version couple - Override the CLI's transitive @mastra/loggers to 1.2.0, with a note to remove it once @mastra/core 1.63.0 is stable Re-verified against a real PostgreSQL: /health open, API 401 without a token and 200 with one, Studio serves, and a generate call reaches the model endpoint and writes memory rows. Co-Authored-By: Claude <noreply@anthropic.com>
Independent review plus a fact-check of every claim in the README. Code, now 135 lines across 4 files instead of 186 across 5: - Delete model.ts; fold model resolution into the one agent that uses it - Replace the `store ??=` / `memory ??=` memoisation with plain consts. It was copied from the Next.js samples, where the point is surviving hot reload — and those use globalThis precisely because a module-local `let` does not survive it. Neither applies to a plain Node server. - Delete a comment claiming the server starts before the database is reachable. It does not: PostgresStore.init runs at boot and the process exits if the database is down. Verified. - Drop the duplicate PORT setting from the Dockerfile and index.ts; Mastra already defaults to 4111. Verified by booting with PORT unset. - Remove the dead tsconfig outDir and the `dist` ignore entries README, four claims were wrong: - "mode: host, so it gets a private address" — wrong, and a security claim. In Defang, networks decide public or private; port mode only picks load-balancer versus direct public IP. Rewritten. - "no long-lived OPENAI_API_KEY" — contradicted by the sample's own code. Defang does inject a gateway key; what is absent is a provider key. - The chart has an umbrella plus three sub-charts, not four - global.cloud also accepts `local` Also: note that third-party Mastra auth providers need an Enterprise licence in production while SimpleAuth does not, since the sample's whole pitch is that no licence is required. Add the Pro-plan gate to autoscaling, name the three worker types, drop the Playground section (Playground is shut down, and this sample needs managed Postgres and managed models), and switch local dev to qwen2.5:3B because the demo is a tool call and gemma3:1B cannot call tools. Verified again against a real PostgreSQL, in a copy of .mastra/output with no parent node_modules, which is what the runner image gets: boots on 4111, /health open, API 401/200/401, Studio 200, a forced tool call returns real values, memory persists, and zero module-resolution errors. Co-Authored-By: Claude <noreply@anthropic.com>
The staging deploy failed with: service "chat": memory 2048 MiB exceeds quota 1024 MiB `chat` is not a service this sample writes. The CLI synthesizes a LiteLLM gateway for the top-level `models:` block and reserves 2048 MiB, because `1024 MiB OOMed during AWS E2E` (defang src/pkg/cli/compose/fixup.go:254). The staging tenant runs the Playground driver and caps a service at 1024 MiB, so the deploy is rejected before anything starts. Lowering the reservation would deliberately reintroduce a known OOM, so this excludes the sample instead, next to self-improving, which is skipped for the same quota reason. The sample also uses managed Postgres, which the Playground does not support. Note for reviewers: this is not specific to this sample. Any sample with a `models:` block now exceeds the staging per-service quota. mastra-extended and crewai only pass today because the job deploys changed samples only, and neither has been touched since the gateway default became 2048 MiB. Co-Authored-By: Claude <noreply@anthropic.com>
CI update: staging deploy failed, and the cause is not this sampleThe
defaultLLMMemoryMiB=2048// 1024 MiB OOMed during AWS E2E; use the next size up.
The staging tenant runs the Playground driver and caps a service at 1024 MiB, so the deploy is rejected before anything starts. The reservation is only defaulted when unset ( Instead the sample is excluded from the staging deploy, next to
The part worth your attentionThis is not specific to this sample. Any sample with a So there are two follow-ups, neither of which belongs in this PR:
What was verified insteadSince CI cannot deploy this, here is what I ran locally against a real PostgreSQL, using the actual build artifact copied somewhere with no parent
One consequence of the exclusion: 🤖 Generated with Claude Code |
defangdevs
commented
Aug 26, 2026
The first failure ( That unblocked the config step, but surfaced the real, pre-existing issue underneath: the managed model gateway's default memory (2048 MiB, bumped from 1024 to avoid AWS E2E OOMs) exceeds the retired Playground staging quota (1024 MiB) — the same constraint |
Thanks for adding the secret — and you're right about the quota being the real blocker. Two corrections to the record, both with logs, because one of them is mine. 1. My "green" was vacuous, and I reported it as if it meant something
That is the exclusion working as designed — but it means this sample now has zero CI deploy coverage, and the green tick does not say otherwise. I flagged the run as a pass in my previous comment without checking what it did. That is the mirror image of DefangLabs/defang#2227, and I walked into it. 2. The config step did not failThere was only ever one failing run (
The None of that changes your conclusion, and the secret is good to have. But it does mean the secret is currently unused, since the sample is excluded before any config is set. The choice this leavesThe exclusion makes CI green by not testing the sample. That is honest, but it is not coverage, and it quietly extends to If you'd rather have real coverage, raising the staging tenant's per-service memory quota to 2048 MiB would let me drop the exclusion in this PR and put the secret you added to work. Happy to do that in this branch — just say which you prefer. Until then, the only evidence this sample runs is local: the real build artifact against a real PostgreSQL, in a copy with no parent 🤖 Generated with Claude Code |
What this is
Mastra published a Helm chart for self-hosting the Mastra server on Kubernetes. This adds a Defang sample that deploys the same thing from one Compose file.
The chart is Enterprise-only. Mastra's docs say it is "available exclusively to Mastra Enterprise customers", served from a private registry against a licence key. I confirmed this directly:
oci://us-central1-docker.pkg.dev/mastra-cloud/mastra-helm-ee/mastra-projectsreturns HTTP 401 anonymously, and an anonymous token exchange is denied.So this is not a port of the chart. It reproduces the chart's topology using only the Apache-2.0 parts of Mastra, which anyone can deploy.
Why a new sample
The three Mastra samples we already have —
mastra-nextjs,mastra-extended,self-improving— all embed Mastra inside a Next.js app. None deploys the standalone Mastra server, which is exactly what the chart deploys.What it deploys
One
serverservice (agent API + Studio UI, frommastra build --studio), managed PostgreSQL viax-defang-postgres, and a managed model via themodels:block. No provider API key anywhere.The README carries a table mapping each chart requirement to its Compose equivalent: cert-manager to Defang-issued certificates, ingress controller to
mode: ingress,global.cloudto--provider,imagePullSecretsto build-from-source, the EE licence to nothing.Verification
No Docker in this environment, so I installed the Defang CLI and a real PostgreSQL and ran the actual build artifact. To match what the runner image gets, I copied
.mastra/outputsomewhere with no parentnode_modules:/healthwith no token/api/agentswith no token/api/agentswith a valid token/api/agentswith a wrong token/defang compose config./scripts/check-sample-files.shTwo things worth a reviewer's attention
1. A live upstream Mastra break, and why versions are pinned.
mastra buildwrites its ownpackage.jsoninto.mastra/outputand runs a secondnpm installthere. That install does not readpackage-lock.json, and it copies your version ranges verbatim. So caret ranges make every build non-reproducible, including every Defang CD build.This bit during review. Two builds 14 minutes apart gave different results:
@mastra/loggers@1.3.0was published at 21:45 UTC, it importsbuildLogRecordDatafrom@mastra/core, and that export only exists in 1.63 pre-releases. Against stable core 1.62.0 the server dies at boot. ThemastraCLI depends on^1.2.0, so a fresh install picks it up with no direct dependency on it.Fix: pin every Mastra version exactly, drop
@mastra/loggersas a direct dependency, and override the CLI's transitive copy to 1.2.0. The override has a removal condition inpackage.json: delete it once@mastra/core1.63.0 is stable.2. Deployment is BYOC-only, and there is no Playground section.
Managed PostgreSQL and managed models both need a real cloud account. The README says AWS, GCP, or Azure — not DigitalOcean, where managed Postgres is unmanaged and GenAI is unsupported.
Needs a human
TEST_MASTRA_API_TOKENis added todeploy-changed-samples.yml, but the repository secret does not exist yet. CI will still pass without it — a missing secret becomes an empty string, and the sample then runs with auth off in staging. Setting a real value would exercise the auth path properly.Security posture
The API and Studio are token-gated by default.
compose.yamldeclaresMASTRA_API_TOKENwith no value, so Defang refuses to deploy until it is set — an agent endpoint open on the internet is a budget-drain risk. Studio is an admin surface and shares that token.One README claim I had written was wrong and is now corrected: I had said
mode: hostgives the database a private address. It does not. In Defang, networks decide public versus private; port mode only chooses load-balancer versus direct public IP.Process notes
Every claim in the README was independently fact-checked against Mastra's docs, its bundled offline docs, npm, and the Defang docs. That pass found four wrong claims, all fixed. A separate reviewer flagged that the runner image might not resolve five externalised dependencies — I tested that directly and it is a false positive; the specifiers sit in code paths that never execute.
The samples table in
README.mdis stale repo-wide:node scripts/generate-samples-list.jsrewrites 84 unrelated rows. I added only my row by hand to keep this diff reviewable. Worth a separate cleanup.🤖 Generated with Claude Code
Samples Checklist
✅ All good!