Skip to content

feat(mastra-server): self-hosted Mastra server sample, as an alternative to the Helm chart - #662

Open
defang-sam[bot] wants to merge 4 commits into
mainfrom
sample/mastra-server
Open

feat(mastra-server): self-hosted Mastra server sample, as an alternative to the Helm chart#662
defang-sam[bot] wants to merge 4 commits into
mainfrom
sample/mastra-server

Conversation

@defang-sam

@defang-samdefang-samBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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-projects returns 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 server service (agent API + Studio UI, from mastra build --studio), managed PostgreSQL via x-defang-postgres, and a managed model via the models: 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.cloud to --provider, imagePullSecrets to 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/output somewhere with no parent node_modules:

CheckResult
Boots on Mastra's default port 4111pass
/health with no token200
/api/agents with no token401
/api/agents with a valid token200
/api/agents with a wrong token401
Studio UI at /200
Agent call that forces a real tool executionreturns real values
Memory rows written to PostgreSQLthreads + messages persist
Module-resolution errors0
defang compose configOK
./scripts/check-sample-files.shclean, whole repo

Two things worth a reviewer's attention

1. A live upstream Mastra break, and why versions are pinned.

mastra build writes its own package.json into .mastra/output and runs a secondnpm install there. That install does not read package-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.0 was published at 21:45 UTC, it imports buildLogRecordData from @mastra/core, and that export only exists in 1.63 pre-releases. Against stable core 1.62.0 the server dies at boot. The mastra CLI 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/loggers as a direct dependency, and override the CLI's transitive copy to 1.2.0. The override has a removal condition in package.json: delete it once @mastra/core 1.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_TOKEN is added to deploy-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.yaml declares MASTRA_API_TOKEN with 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: host gives 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.md is stale repo-wide: node scripts/generate-samples-list.js rewrites 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!

rtm-defangand others added 3 commits August 26, 2026 21:49
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>
@defang-sam
defang-samBothad a problem deploying to deploy-changed-samples August 26, 2026 22:09 Failure
@defang-sam
defang-samBothad a problem deploying to deploy-changed-samples August 26, 2026 22:11 Failure
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>
@defang-sam
defang-samBotdeployed to deploy-changed-samples August 26, 2026 22:15 Active
@defang-sam

Copy link
Copy Markdown
ContributorAuthor

CI update: staging deploy failed, and the cause is not this sample

The Deploy Changed Samples job failed in 1.3 seconds. The console log hides the reason — the step pipes through grep -v and the real error only reaches the uploaded artifact. From program-output/mastra-server-*/stderr.log:

Server error: invalid_argument: service "chat": memory 2048 MiB exceeds quota 1024 MiB;
consider using BYOC (https://s.defang.io/byoc)

chat is not a service this sample writes. The CLI synthesizes a LiteLLM gateway for the top-level models: block and reserves 2048 MiB. That value is deliberate:

defaultLLMMemoryMiB=2048// 1024 MiB OOMed during AWS E2E; use the next size up.

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. The reservation is only defaulted when unset (fixup.go:432), so it is overridable — but overriding it means shipping a sample at a memory size the CLI authors already recorded as OOMing. I did not do that.

Instead the sample is excluded from the staging deploy, next to self-improving, which is skipped for the same quota reason. This sample also uses managed Postgres, which the Playground does not support, so it is BYOC-only either way.

Check Samples and Test Scripts both pass.

The part worth your attention

This is not specific to this sample. Any sample with a models: block now exceeds the staging per-service quota. mastra-extended and crewai both have one; they pass today only because the job deploys changed samples, and neither has been touched since the gateway default became 2048 MiB. Touch either one and it fails the same way.

So there are two follow-ups, neither of which belongs in this PR:

  1. Raise the staging tenant's per-service memory quota to at least 2048 MiB, or give the load-test tool a way to run these on BYOC.
  2. Surface the real error in the console. Losing a deploy failure to grep -v cost most of the debugging time here.

What was verified instead

Since CI cannot deploy this, here is what I ran locally against a real PostgreSQL, using the actual build artifact copied somewhere with no parent node_modules — which is what the runner image gets:

CheckResult
Boots on Mastra's default port 4111pass
/health with no token200
/api/agents with no token401
/api/agents with a valid token200
/api/agents with a wrong token401
Studio UI at /200
Agent call forcing a real tool executionreturned real values
Memory rows written to PostgreSQLthreads + messages persist
Module-resolution errors0
defang compose config, playground and --provider awsOK

One consequence of the exclusion: TEST_MASTRA_API_TOKEN in deploy-changed-samples.yml is now unused. I left it in place so the sample works immediately if it is ever re-enabled.

🤖 Generated with Claude Code

@defangdevs

Copy link
Copy Markdown
Contributor

The first failure (defang config create MASTRA_API_TOKEN: exit status 5) was because the TEST_MASTRA_API_TOKEN repo secret referenced by the new workflow line didn't exist yet. Since this token is a self-chosen bearer secret for the sample's own auth (not an external API key), I generated a random value and added it as a repo secret so CI can create the config.

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 self-improving already works around by being excluded from deploy-changed-samples.yml's staging deploy. Your last push already added the matching exclusion for mastra-server, and deploy_changed_samples is now green.

@defang-sam

Copy link
Copy Markdown
ContributorAuthor

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

Deploy Changed Samples is green on b3d4331, but it deployed nothing:

Identify changed samples: No samples have changed. Exiting...

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 fail

There was only ever one failing run (33018451998, 22:09Z). Its test.log, in full:

22:12:13 Running test mastra-server ... by deployer 0
22:12:13 Creating config MASTRA_API_TOKEN
22:12:14 Creating config POSTGRES_PASSWORD
22:12:15 Command exited with error: exit status 3
22:12:16 Failed to run `defang ps --json` ...: exit status 5

SetConfigs logs Creating config Xbefore running the command and returns on the first error (tools/testing/deployer/deployer.go:313-329). Both keys were logged and there is no failed to create config anywhere in test.log, stdout.log, or stderr.log. defang config create also accepts an empty value — no emptiness check on the stdin path (defang src/cmd/cli/command/config.go:129-143) — so a missing secret resolves to "" and passes.

The exit status 5 was defang ps --json, after exit status 3 from the deploy. The deploy's own error is the quota one.

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 leaves

The exclusion makes CI green by not testing the sample. That is honest, but it is not coverage, and it quietly extends to mastra-extended and crewai the moment either is touched.

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 node_modules, covering boot on 4111, /health 200 unauthenticated, /api/agents 401/200/401, Studio 200, a forced tool execution returning real values, and memory rows persisting.

🤖 Generated with Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@defangdevs@rtm-defang