Deployment troubleshooting
Four failures eat the first deploy. Run the preflight before anything else — it names the misconfiguration instead of making you guess:
pnpm import:self-host-checkdocker run --rm --env-file .env ghcr.io/capitality-io/mildport-aio \ bun dist/cli/self-host-check.jsOn Kubernetes, helm test mildport is the same check inside the cluster.
Images won’t pull (ImagePullBackOff)
Section titled “Images won’t pull (ImagePullBackOff)”Images live at ghcr.io/capitality-io/* and they are private. A pod stuck in
ImagePullBackOff / ErrImagePull is almost always one of:
-
Access not granted yet. Request it from Self-hosting in your account for the GitHub user that will pull. Pending grants look like a bad password.
-
The node is not logged in. A laptop
docker logindoes not help the cluster.Terminal window echo $GITHUB_TOKEN | docker login ghcr.io -u <your-github-username> --password-stdinOn Kubernetes, those credentials belong in an
imagePullSecret:Terminal window kubectl create secret docker-registry ghcr-pull \--docker-server=ghcr.io \--docker-username=<your-github-username> \--docker-password=$GITHUB_TOKEN \-n mildportimagePullSecrets:- name: ghcr-pull -
Wrong registry. We publish to GHCR, not Docker Hub. If values still say
capitality/mildport, the pull will never succeed. See Image access.
Every import returns 401
Section titled “Every import returns 401”401 means the license key did not verify. The body looks like
{ "status": "error", "code": "UNAUTHORIZED", "reason": "…" }. The reason is the fix:
reason |
What it means | What to do |
|---|---|---|
missing |
No Authorization: Bearer … header |
Pass the license key from Licenses |
not_configured |
Engine has no IMPORT_LICENSE_PUBLIC_KEY |
Paste the public key from the Self-hosting page; restart |
expired |
The key’s exp is in the past |
Regenerate on Licenses; the old key works until it expires |
revoked |
The license id is on IMPORT_LICENSE_REVOKED_IDS |
Take it off, or use a new key |
malformed / bad_signature / bad_attestation |
Truncated key, extra whitespace, or a key from a different issuer | Copy the key again; do not mint one yourself |
A missing public key is also a readiness failure (checks.license: missing) — the
engine answers /health and still 401s every import.
Not a 401:
403 ORIGIN_NOT_ALLOWED— the key is valid, but it was minted with an origin list that does not include this page. Regenerating the key with the right origins (or an empty list) is the fix. This is not CORS.- CORS in the browser — the request never gets a JSON body. Next section.
Confirm a key against a running engine with
GET /api/import/v1/license/verify. More on keys: Licensing.
The widget does nothing (CORS)
Section titled “The widget does nothing (CORS)”The production image (NODE_ENV=production) refuses every cross-origin browser call
when ALLOWED_ORIGINS is empty — including http://localhost:5173. Dev-mode localhost is
allowed only outside production.
Set the exact origin of the page that embeds the widget, scheme and port included:
ALLOWED_ORIGINS=https://app.example.com,http://localhost:5173On Helm, that is app.allowedOrigins. Server-to-server callers (no Origin header) are
unaffected; skip the variable only if nothing in a browser talks to the engine.
The license-key origin list (403 ORIGIN_NOT_ALLOWED) is a second, separate lock. CORS
decides whether the browser may speak; the key decides whether that origin may use this
key.
The pod never becomes Ready
Section titled “The pod never becomes Ready”GET /health is liveness. It returns {"status":"ok","service":"mildport",…} as soon
as the process can answer, before Mongo or licensing work. The chart’s readiness probe is
GET /health/ready. A pod that lives but never becomes Ready is failing that probe.
curl -i localhost:8090/health/ready503 with a body:
{ "status": "not_ready", "service": "mildport", "checks": { "mongo": "down", "license": "missing" }}| Check | down / missing means |
Fix |
|---|---|---|
mongo |
The engine cannot ping Mongo (wrong URI, network policy, Mongo not up) | MONGO_CONNECTION_STRING; wait for Mongo; helm test |
license |
No public key and development mode off | IMPORT_LICENSE_PUBLIC_KEY from the Self-hosting page |
license: dev-mode is ready — synthetic keys work, and you should not ship that.
Sidecars being down does not fail readiness. Unset decode URLs just mean those file
families are declined. A PDF that 503s while /health/ready is 200 is a sidecar URL, not
a stuck pod.
Next: Your first import · Configuration · Image access