Troubleshooting
Start with the App itself: its phase, conditions and message say which stage of the reconcile pipeline stopped.
kubectl get apps -Akubectl -n <namespace> describe app <name> # conditions + recent eventskubectl -n <namespace> get app <name> -o jsonpath='{.status.message}{"\n"}'Condition False | Stage | Look at |
|---|---|---|
Validated | The spec or namespace was refused; nothing was created. Terminal until the spec changes. | status.message (below) |
WorkloadReady | Pods are not ready. | kubectl -n <ns> get pods -l apps.nebari.dev/app=<name>, pod logs |
RoutingReady | The NebariApp is not ready: route, certificate or Keycloak client. | kubectl -n <ns> describe nebariapp app-<name> |
The UI’s app page and the MCP get_app_status tool show the same conditions.
The App is Failed with Validated=False
Section titled “The App is Failed with Validated=False”status.message | Fix |
|---|---|
namespace "x" is not opted in: label nebari.dev/managed=true is required | kubectl label namespace x nebari.dev/managed=true. The App re-validates on its own. |
reason SubdomainTaken | Another App already serves that subdomain; the oldest App keeps it. Pick another access.subdomain (it is immutable, so delete and re-create the App). |
source.git.url: only https:// URLs are allowed …, … must not contain credentials …, host "…" is not allowed … | Git sources must be public https:// URLs on a public host. Private repositories are not supported yet. |
source.git.ref must not start with '-', source.git.subdir must not contain '..' | Fix the ref or subdir. |
source.pvc: PersistentVolumeClaim "x" not found … | The claim must exist in the App’s namespace. |
source.pvc: PersistentVolumeClaim "x" is not labeled apps.nebari.dev/mountable=true … | The claim’s owner opts in with kubectl -n <ns> label pvc x apps.nebari.dev/mountable=true. |
source.inline.files: path "…" must be relative … | Inline file names are relative paths without ... |
reason ImageNotAllowed: image sources are disabled on this cluster … or … is not in the allowed image registries (…) | The image’s registry (after normalization: nginx:1.27 is docker.io/library/nginx) is not in apps.image.allowedRegistries. Use an allowed image, or ask an admin to allow it. The operator scales the App’s Deployment to zero until it is fixed. |
source.image.reference "…": needs a tag (:1.2.3) or a digest (@sha256:...) | Add a tag or digest; an implicit :latest is refused. |
source.image.port … must be 1024-65535 and not 8080 or 8082 | Make the image listen on another port (most accept $PORT or a flag) and set port to it. |
ForbiddenEnvReference.runtime.envusesvalueFrom.secretKeyRef, or aconfigMapKeyRefto another App’sapp-<name>-content/app-<name>-server. Those would read data that is not the App’s own, so the operator refuses the workload (it is not created or updated;kubectl describe app <name>shows the Warning event). Move credentials toruntime.secretKeys(values inapp-<name>-secrets, or set them in the UI’s Secrets) and remove the reference.
Pods never become ready
Section titled “Pods never become ready”kubectl -n <ns> get pods -l apps.nebari.dev/app=<name>kubectl -n <ns> logs deploy/app-<name> -c git-clone # git sources (init container)kubectl -n <ns> logs deploy/app-<name> -c copy-source # Python apps (init container)kubectl -n <ns> logs deploy/app-<name> -c app # the app itselfInit:CrashLoopBackOffongit-clone. The clone failed: the repository is private or gone, orrefis a commit SHA. Refs are cloned withgit clone --branch, so use a branch or tag.- Static app serves 404 / a blank page. The content root has no
index.html. For git sources, pointsource.git.subdirat the directory that holds it. - Python app restarts or is
OOMKilled. Theappcontainer runspixi installbefore your task, and the install needs more memory than the app does. Raiseruntime.resources.limits.memory(cluster default:apps.defaultResources.python). Apixi.lockmakes the install--lockedand faster. - Python app is
Runningbut never ready. The task must listen on0.0.0.0:$PORT(the pod getsPORT=8080). A server bound to127.0.0.1fails the readiness probe. An App that can idle getsPORT=8081, because the authz sidecar takes 8080: a task that hard-codes 8080 then collides with the sidecar. - A restricted app returns
403to someone who should get in. The gateway refused them: their access token’sgroupsclaim holds none of the App’saccess.groups. Entries are full group paths matched exactly and case-sensitively, so/financedoes not admit someone who is only in/finance/analysts: list the subgroup itself. Group changes reach the token on its next refresh (5 minutes by default); someone just added should sign out and back in. The values the gateway enforces are in the SecurityPolicy:kubectl -n <ns> get securitypolicy app-<name>-security -o jsonpath='{.spec.authorization}'. nebari-operator’s troubleshooting guide covers the rest. - Every signed-in user gets into a restricted app. nebari-operator is older than
v0.2.0, which is the first release that enforcesauth.groupsat the gateway: upgrade it (Upgrading). The SecurityPolicy above has noauthorizationsection then. - A restricted app stays
DeployingwithGroupsNotResolvedinRoutingReady. A path inaccess.groups(such as/ops/oncall) does not exist in Keycloak. Paths are never created: create the group, or list a top-level group by bare name (oncall) to have nebari-operator create it.GroupsEmptymeans every entry names no group (such as/), andGroupsClaimConflictthat a mapper on the app’s Keycloak client writes thegroupsclaim in another form; the gateway denies everyone until it is fixed. - An app pod’s
authzcontainer exits with “AUTHZ_GROUPS is set”. The authz image is newer than the apps-operator, which still asks the sidecar to check groups. The sidecar refuses rather than run the App open: upgrade the operator to the same release as the image (the chart ships them together). - An image app stays
Failedwith “—authz-image”. Image apps always run the sidecar: keep the chart’sauthz.image. CreateContainerConfigErrorwithsecret "app-<name>-secrets" not foundorcouldn't find key. The App listsruntime.secretKeysthat its Secret does not hold. Set the missing values (UI Secrets,PATCH /apps/{ns}/{name}/secrets), or, for an App managed with kubectl, create the Secret or remove the name fromsecretKeys. Right after a launch with secrets this clears by itself within seconds, once apps-api has written the Secret.- A rotated secret has no effect. Pods read environment variables at start. apps-api
rolls them on every write; after editing the Secret with kubectl, run
kubectl -n <ns> rollout restart deployment/app-<name>. - Image app
Failedwith reasonImageRunsAsRoot. The image’sUSERis root (or a name the kubelet cannot check), and app pods must run as non-root. Rebuild the image with a numeric non-rootUSER(e.g.USER 1000, owning the directories the app writes), or setsource.image.runAsUserto the uid of the image’s named user. The policy is never relaxed. - Image app
Failedwith reasonImagePullFailed. Checksource.image.reference; for a private registry the Secret named inapps.image.pullSecretsmust exist in the App’s namespace and grant access (kubectl -n <ns> describe pod -l apps.nebari.dev/app=<name>shows the registry’s answer). - Image app
Runningbut never ready, orCrashLoopBackOff. The server must listen onsource.image.port(on0.0.0.0or127.0.0.1): readiness goes through the sidecar, which dials that port. An image that writes to its home or a root-owned directory may fail as a non-root user; point its cache or home at/tmpwith an environment variable. PendingwithFailedScheduling. The namespace’s quota or the nodes cannot fit the requests; lowerruntime.resources.requestsorreplicas.
RoutingReady=False
Section titled “RoutingReady=False”The NebariApp is reconciled by the nebari-operator, not this pack:
kubectl -n <ns> describe nebariapp app-<name>kubectl -n nebari-operator-system logs deploy/nebari-operator-controller-manager- Reason
NebariAppMissingthat does not clear. TheNebariAppCRD is missing: install nebari-operatorv0.2.0or later. - Certificate not ready. cert-manager could not issue
<subdomain>.<appsDomain>; checkkubectl -n <ns> get certificate,challenge. Local clusters without public DNS should settls.enabled=false. - The URL resolves nowhere.
*.apps.<cluster-domain>(theappsDomain) needs a wildcard DNS record pointing at the gateway.
Signing in and API errors
Section titled “Signing in and API errors”- The UI keeps redirecting to Keycloak, or every call returns
401. The API refuses tokens that were not issued by this pack’s own Keycloak clients or that fail JWKS verification; the401body says why (invalid token: …). An issuer or JWKS failure usually means the browser-facing Keycloak URL does not resolve inside the cluster: setkeycloak.internalUrl. - The API pod is not ready (
/api/v1/readyzreturns503). The response marks the failing check:kubernetes(the API server is unreachable or the ServiceAccount lacks access) orjwks(Keycloak’s JWKS endpoint is unreachable from the pod). 403 namespace "x" is not available for apps. Only namespaces inapi.allowedNamespaces(or, when that is empty, labelednebari.dev/managed=true) can host apps;defaultandkube-*never can.403 you do not have access to app …. Apps are visible to their owner, their managers (access.managers) and members ofapi.adminGroupsonly. A manager group counts once the user’s access token carries it (sign out and in, or wait for it to refresh).403 only the owner or an admin can change who manages this app. A manager editedaccess.managers; leave it out of the request (or send it unchanged).- An app’s Resources card says “Live usage needs metrics-server”. Live CPU and memory
usage come from
metrics.k8s.io; install metrics-server. Limits and restarts come from the pods and show either way.
Install and upgrade
Section titled “Install and upgrade”helm installfails withinsecureAllowAnonymous. The chart refusesapi.auth.enabled=falseon a TLS install. That combination is for local clusters; see Configuration.helm installfails creating a RoleBinding: namespace not found. Every namespace inapi.allowedNamespacesmust exist first (kubectl create namespace apps).- A new App field is silently dropped after an upgrade.
helm upgradedoes not update CRDs; re-apply them. See Upgrading. - Pods run old images from a source install. A chart from a checkout defaults its image
tags to
appVersion; set*.image.tag=latestonmain, or install a released chart.
The MCP server
Section titled “The MCP server”- Claude Code shows the server as needing authentication. Run
/mcp, select it and choose Authenticate, orclaude mcp login nebari-apps. - The browser login fails with “Invalid parameter: redirect_uri”. The
--callback-port(oroauth.callbackPort) does not match the client’s redirect URI in Keycloak, or the server was added without--client-id. Re-add it with the command from the UI’s Settings page. - Claude Code says dynamic client registration failed. No client id was configured, so
it tried to register one and Keycloak refused. Add the server with
--client-id; see The sign-in client. - Every request gets
401witherror="invalid_token"after signing in. The token is from a client the pack does not accept (setmcp.oauth.clientIdto the client you sign in with), or apps-mcp cannot fetch the realm JWKS: its log saystoken rejectedwith the reason. A TLS error there means Keycloak’s certificate comes from a private CA; trust it withmcp.orgCABundleor setkeycloak.internalUrl. remove_appdoes nothing. It requiresconfirm=true.