<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged backstage</title>
<link>https://coderlegion.com/tag/backstage</link>
<description>Powered by Question2Answer</description>
<item>
<title>Deploying Backstage on Kubernetes with the Helm Chart: The Infrastructure-First Guide</title>
<link>https://coderlegion.com/15068/deploying-backstage-on-kubernetes-with-the-helm-chart-the-infrastructure-first-guide</link>
<description>&lt;hr&gt;
&lt;h3 id=&quot;a-practical-field-guide-for-engineers-who-start-from-the-backstage-helm-chart-covering-the-real-configuration-gaps-the-demo-image-limitation-and-the-exact-values-nesting-that-makes-or-breaks-your-deployment-&quot;&gt;A practical field guide for engineers who start from the Backstage Helm chart — covering the real configuration gaps, the demo image limitation, and the exact values nesting that makes or breaks your deployment.&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers deploying Backstage on Kubernetes via the&lt;br&gt;
official Helm chart who want a working portal, not just a running pod.&lt;br&gt;
This guide starts where most tutorials end — after &lt;code&gt;helm install&lt;/code&gt; succeeds&lt;br&gt;
but before anything actually works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A few weeks ago I published an article called&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/sodiqjimoh/nine-ways-backstage-breaks-before-your-developer-portal-works-4eo1&quot;&gt;&quot;Nine Ways Backstage Breaks Before Your Developer Portal Works&quot;&lt;/a&gt;.&lt;br&gt;
A Backstage maintainer read it and gave me structured feedback. The core of&lt;br&gt;
it was this: several of the failures I documented were caused by not&lt;br&gt;
following the official getting-started documentation before using the Helm&lt;br&gt;
chart, and by using the demo image as if it were a production-ready base.&lt;/p&gt;
&lt;p&gt;They were right. This article is the follow-up they suggested — and the one&lt;br&gt;
I should have written first.&lt;/p&gt;
&lt;p&gt;It does not repeat the previous article. It starts earlier, goes deeper on&lt;br&gt;
Helm-specific configuration, and correctly attributes failures to their&lt;br&gt;
actual causes rather than blaming Backstage for things that are ArgoCD,&lt;br&gt;
Traefik, or operator error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Official resources you should read alongside this guide:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://backstage.io/docs/getting-started/&quot;&gt;Backstage getting started documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/backstage/charts/blob/main/charts/backstage/README.md&quot;&gt;Backstage Helm chart README&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/backstage/charts?tab=readme-ov-file#backstage-helm-chart&quot;&gt;Backstage Helm chart disclaimer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://backstage.io/docs/features/software-catalog/configuration#catalog-rules&quot;&gt;Catalog rules documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://backstage.io/docs/features/software-templates/adding-templates&quot;&gt;Adding templates documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Project repo referenced throughout:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sodiq-code/neuroscale-platform&quot;&gt;github.com/sodiq-code/neuroscale-platform&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-one-thing-you-must-understand-before-installing-the-helm-chart&quot;&gt;The one thing you must understand before installing the Helm chart&lt;/h3&gt;
&lt;p&gt;The Backstage Helm chart uses a demo image by default. The chart README&lt;br&gt;
contains this explicit warning:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The Backstage chart is not an official Backstage project and is not&lt;br&gt;
supported by the Backstage core team. The default image used in this chart&lt;br&gt;
is for demo purposes only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This single fact explains most of the configuration friction you will&lt;br&gt;
encounter. The demo image does not behave like a real Backstage application&lt;br&gt;
built with &lt;code&gt;backstage new app&lt;/code&gt;. It has different startup characteristics,&lt;br&gt;
different configuration defaults, and different failure modes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What this means practically:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you are building a real developer portal — not just running a demo — you&lt;br&gt;
should follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://backstage.io/docs/getting-started/&quot;&gt;official getting started guide&lt;/a&gt;&lt;br&gt;
to create your own Backstage application first, build a custom Docker image&lt;br&gt;
from it, and then use the Helm chart to deploy that image. The chart's&lt;br&gt;
&lt;code&gt;image.repository&lt;/code&gt; and &lt;code&gt;image.tag&lt;/code&gt; values are where you point to your&lt;br&gt;
own image.&lt;/p&gt;
&lt;p&gt;If you are experimenting, learning, or building an integration platform&lt;br&gt;
where Backstage is one component (as in the NeuroScale project), the demo&lt;br&gt;
image path is workable — but you need to understand its limitations and&lt;br&gt;
configure it correctly.&lt;/p&gt;
&lt;p&gt;This guide covers the Helm chart path specifically, with the official docs&lt;br&gt;
as the reference point throughout.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-values-hierarchy-that-breaks-everything-silently&quot;&gt;The values hierarchy that breaks everything silently&lt;/h3&gt;
&lt;p&gt;This is the most important configuration concept in the entire Helm chart.&lt;br&gt;
Get this wrong and every override you write will be silently ignored.&lt;/p&gt;
&lt;p&gt;The Backstage Helm chart is a &lt;strong&gt;wrapper chart&lt;/strong&gt; — Backstage itself is a&lt;br&gt;
dependency inside it. The dependency is named &lt;code&gt;backstage&lt;/code&gt;. This means&lt;br&gt;
configuration for the Backstage application container must be nested under&lt;br&gt;
&lt;code&gt;backstage.backstage.*&lt;/code&gt;, not &lt;code&gt;backstage.*&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wrong — values are silently ignored:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# This looks correct but is placed at the wrong hierarchy level
backstage:
  appConfig:
    app:
      title: My Platform
  startupProbe:
    initialDelaySeconds: 120
  resources:
    requests:
      cpu: 100m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Correct — values reach the Backstage container:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:           # &amp;lt;-- this second level is required
    appConfig:
      app:
        title: My Platform
    startupProbe:
      initialDelaySeconds: 120
    resources:
      requests:
        cpu: 100m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Helm chart processes the outer &lt;code&gt;backstage&lt;/code&gt; key as the dependency name.&lt;br&gt;
Values placed directly under &lt;code&gt;backstage.*&lt;/code&gt; are interpreted as chart-level&lt;br&gt;
configuration, not as container configuration. Kubernetes then uses chart&lt;br&gt;
defaults — including probe timings — rather than your overrides.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to verify your values are actually applied:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Render the Helm chart before applying it and inspect the output Deployment&lt;br&gt;
spec directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm template neuroscale-backstage backstage/backstage \
  -f infrastructure/backstage/values.yaml \
  --namespace backstage \
  | grep -A 30 &quot;startupProbe&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see &lt;code&gt;initialDelaySeconds: 120&lt;/code&gt; in the output, your probe override&lt;br&gt;
reached the container. If you see &lt;code&gt;initialDelaySeconds: 5&lt;/code&gt; or a very small&lt;br&gt;
number, your values are at the wrong nesting level.&lt;/p&gt;
&lt;p&gt;This verification step should be part of your CI pipeline. In the NeuroScale&lt;br&gt;
platform, &lt;code&gt;scripts/ci/render_backstage.sh&lt;/code&gt; runs this check on every PR:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
# scripts/ci/render_backstage.sh
helm template neuroscale-backstage backstage/backstage \
  -f infrastructure/backstage/values.yaml \
  --namespace backstage \
  | grep &quot;initialDelaySeconds&quot; \
  | grep -q &quot;120&quot; || {
    echo &quot;ERROR: startupProbe initialDelaySeconds not set correctly&quot;
    exit 1
  }
echo &quot;Helm values nesting verified&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;required-configuration-keys-for-the-demo-image&quot;&gt;Required configuration keys for the demo image&lt;/h3&gt;
&lt;p&gt;The demo image requires specific configuration keys to be present at&lt;br&gt;
startup. Missing any of them causes the frontend to crash on load with a&lt;br&gt;
JavaScript error that is only visible in browser developer tools — the page&lt;br&gt;
itself shows a blank white screen with no visible error.&lt;/p&gt;
&lt;p&gt;The minimum required &lt;code&gt;appConfig&lt;/code&gt; block:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:
    appConfig:
      app:
        title: Your Platform Name    # required — crash if absent
        baseUrl: http://localhost:7010
      backend:
        baseUrl: http://localhost:7010
        cors:
          origin: http://localhost:7010
        database:
          client: better-sqlite3
          connection: ':memory:'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;baseUrl&lt;/code&gt; matters:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;app.baseUrl&lt;/code&gt; and &lt;code&gt;backend.baseUrl&lt;/code&gt; values must match the URL you are&lt;br&gt;
actually using to access Backstage. If you port-forward on port 7010 but&lt;br&gt;
the config says port 7007, the frontend React app loads but all API calls&lt;br&gt;
fail — the UI appears to work while the backend connection is broken.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;better-sqlite3&lt;/code&gt; for local deployments:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The demo image ships with SQLite support. For local Kubernetes deployments&lt;br&gt;
where you want zero external dependencies, the in-memory SQLite connection&lt;br&gt;
is sufficient. For production, replace this with a PostgreSQL connection&lt;br&gt;
pointing at a managed database service. The chart includes optional&lt;br&gt;
PostgreSQL deployment — see&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/backstage/charts/blob/main/charts/backstage/README.md&quot;&gt;the chart's database configuration docs&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;probe-timings-the-demo-image-starts-slowly&quot;&gt;Probe timings: the demo image starts slowly&lt;/h3&gt;
&lt;p&gt;Backstage is a Node.js application. The demo image takes approximately 60&lt;br&gt;
to 90 seconds to complete startup on a typical Kubernetes node. Kubernetes&lt;br&gt;
default probe timings assume a 2-second initial delay. The result is&lt;br&gt;
predictable: the startup probe fires before the application is ready, the&lt;br&gt;
pod fails the probe, Kubernetes kills it, and the pod enters&lt;br&gt;
&lt;code&gt;CrashLoopBackOff&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is not a Backstage bug. It is a configuration requirement that the&lt;br&gt;
Helm chart does not prominently surface. The correct probe settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:
    startupProbe:
      httpGet:
        path: /healthcheck
        port: 7007
      initialDelaySeconds: 120    # give Node.js time to start
      periodSeconds: 10
      failureThreshold: 30        # 30 × 10s = 5 minutes maximum wait
    readinessProbe:
      httpGet:
        path: /healthcheck
        port: 7007
      initialDelaySeconds: 120
      periodSeconds: 10
      failureThreshold: 3
    livenessProbe:
      httpGet:
        path: /healthcheck
        port: 7007
      initialDelaySeconds: 300    # only check liveness after 5 minutes
      periodSeconds: 30
      failureThreshold: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How to diagnose probe failures:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Watch pod status in real time
kubectl get pods -n backstage -w

# When you see CrashLoopBackOff, describe the pod
kubectl describe pod -n backstage &amp;lt;pod-name&amp;gt;

# Look for this in Events:
# Warning  Unhealthy  kubelet  Startup probe failed: connection refused

# Check logs from the previous container instance
kubectl logs -n backstage &amp;lt;pod-name&amp;gt; --previous --tail=100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see &lt;code&gt;Startup probe failed: connection refused&lt;/code&gt; in events but the&lt;br&gt;
previous container logs show normal Node.js startup messages, the&lt;br&gt;
application is starting correctly — the probe is just firing too early.&lt;br&gt;
Increase &lt;code&gt;initialDelaySeconds&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A full incident postmortem for this specific failure, including the exact&lt;br&gt;
Kubernetes events and the Helm values diff before and after the fix, is in&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sodiq-code/neuroscale-platform/blob/main/infrastructure/INCIDENT_BACKSTAGE_CRASHLOOP_RCA.md&quot;&gt;infrastructure/INCIDENT_BACKSTAGE_CRASHLOOP_RCA.md&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;authentication-local-dev-vs-production&quot;&gt;Authentication: local dev vs production&lt;/h3&gt;
&lt;p&gt;The Backstage new backend architecture (introduced in version 1.x) includes&lt;br&gt;
an internal authentication policy that requires all service-to-service calls&lt;br&gt;
to include a valid Backstage token. This affects how the scaffolder frontend&lt;br&gt;
talks to the scaffolder backend — a call that was unauthenticated in older&lt;br&gt;
versions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For local development only&lt;/strong&gt;, the quickest fix is to use the guest auth&lt;br&gt;
provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:
    appConfig:
      auth:
        providers:
          guest:
            dangerouslyAllowOutsideDevelopment: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps the auth subsystem active and provides a real&lt;br&gt;
&lt;code&gt;user:default/guest&lt;/code&gt; identity to all plugins — which is safer than&lt;br&gt;
disabling auth entirely with &lt;code&gt;dangerouslyDisableDefaultAuthPolicy: true&lt;/code&gt;.&lt;br&gt;
Plugins that assume a user context will behave correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For production&lt;/strong&gt;, use the GitHub OAuth provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# infrastructure/backstage/values-prod.yaml
backstage:
  backstage:
    appConfig:
      auth:
        environment: production
        providers:
          github:
            production:
              clientId: ${GITHUB_CLIENT_ID}
              clientSecret: ${GITHUB_CLIENT_SECRET}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Store &lt;code&gt;GITHUB_CLIENT_ID&lt;/code&gt; and &lt;code&gt;GITHUB_CLIENT_SECRET&lt;/code&gt; as Kubernetes secrets,&lt;br&gt;
not in &lt;code&gt;values.yaml&lt;/code&gt;. The Helm chart's &lt;code&gt;extraEnvVarsSecrets&lt;/code&gt; field handles&lt;br&gt;
this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:
    extraEnvVarsSecrets:
      - backstage-secrets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create the secret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl create secret generic backstage-secrets \
  -n backstage \
  --from-literal=GITHUB_CLIENT_ID=&quot;your-client-id&quot; \
  --from-literal=GITHUB_CLIENT_SECRET=&quot;your-client-secret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How to verify auth is configured correctly:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check the scaffolder actions API directly
curl http://localhost:7010/api/scaffolder/v2/actions

# If you get 401: auth is not configured for your environment
# If you get 200 with a JSON list of actions: auth is working
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you get a 401 with &lt;code&gt;{&quot;error&quot;:{&quot;name&quot;:&quot;AuthenticationError&quot;,&quot;message&quot;:&quot;Missing credentials&quot;}}&lt;/code&gt;,&lt;br&gt;
the scaffolder form will load but render blank — the page returns HTTP 200&lt;br&gt;
but has no data to display. This is only visible in browser developer tools.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;catalog-configuration-registering-templates&quot;&gt;Catalog configuration: registering templates&lt;/h3&gt;
&lt;p&gt;The Backstage catalog applies security rules to what entity kinds are&lt;br&gt;
accepted from each registered location. The default allow list for&lt;br&gt;
repository-based locations does not include &lt;code&gt;Template&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is documented in&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://backstage.io/docs/features/software-catalog/configuration#catalog-rules&quot;&gt;the catalog rules documentation&lt;/a&gt;&lt;br&gt;
and the&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://backstage.io/docs/features/software-templates/adding-templates&quot;&gt;adding templates documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The registration pattern that works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:
    appConfig:
      catalog:
        locations:
          - type: url
            target: https://github.com/your-org/your-repo/blob/main/backstage/templates/your-template/template.yaml
            rules:
              - allow: [Template]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the &lt;code&gt;rules: - allow: [Template]&lt;/code&gt; block, the entity is silently&lt;br&gt;
rejected at ingestion time. The only signal is a warning in the Backstage&lt;br&gt;
server logs — nothing appears in the UI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to diagnose catalog ingestion failures:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl logs -n backstage deploy/backstage --tail=100 \
  | grep -i &quot;warn\|error\|forbidden\|NotAllowedError&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for &lt;code&gt;NotAllowedError: Forbidden: entity of kind Template is not
allowed from that location&lt;/code&gt;. If you see this, your rules block is missing&lt;br&gt;
or at the wrong YAML nesting level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After updating the config, restart Backstage to re-ingest:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl rollout restart deploy/backstage -n backstage
kubectl rollout status deploy/backstage -n backstage --timeout=300s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The template should appear in &lt;code&gt;/create&lt;/code&gt; within 60 seconds of the pod&lt;br&gt;
becoming ready.&lt;/p&gt;
&lt;p&gt;You can validate your &lt;code&gt;app-config.yaml&lt;/code&gt; structure using the Backstage CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @backstage/cli config:check --config app-config.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;github-integration-the-token-secret&quot;&gt;GitHub integration: the token secret&lt;/h3&gt;
&lt;p&gt;The scaffolder requires a GitHub token to open pull requests. The token&lt;br&gt;
must be present as an environment variable in the running Backstage pod.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;backstage:
  backstage:
    appConfig:
      integrations:
        github:
          - host: github.com
            token: ${GITHUB_TOKEN}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Store the token as a Kubernetes secret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create or update the secret
kubectl create secret generic backstage-github-token \
  -n backstage \
  --from-literal=GITHUB_TOKEN=&quot;ghp_your_token_here&quot; \
  --dry-run=client -o yaml | kubectl apply -f -

# Restart to reload the environment variable
kubectl rollout restart deploy/backstage -n backstage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; environment variables from Kubernetes secrets are injected at&lt;br&gt;
pod start time. Updating the secret does not update the running pod. You&lt;br&gt;
must restart the deployment after updating the secret for the new value&lt;br&gt;
to take effect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to verify the token is present without exposing the value:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check character length — a valid GitHub token is 40+ characters
kubectl exec -n backstage deploy/backstage -- \
  sh -c 'echo &quot;Token length: ${#GITHUB_TOKEN}&quot;'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this returns &lt;code&gt;Token length: 0&lt;/code&gt; or &lt;code&gt;Token length: 16&lt;/code&gt; (the length of a&lt;br&gt;
placeholder like &lt;code&gt;&amp;lt;YOUR_TOKEN_HERE&amp;gt;&lt;/code&gt;), the secret was not updated correctly&lt;br&gt;
or the pod was not restarted after the update.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-working-minimal-values-yaml-for-local-development&quot;&gt;A working minimal values.yaml for local development&lt;/h3&gt;
&lt;p&gt;This is the minimum configuration that produces a functioning Backstage&lt;br&gt;
portal on a local Kubernetes cluster with the demo image:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# infrastructure/backstage/values.yaml
backstage:
  backstage:
    image:
      registry: ghcr.io
      repository: backstage/backstage
      tag: latest           # pin to a specific version in production

    appConfig:
      app:
        title: Your Platform
        baseUrl: http://localhost:7010

      backend:
        baseUrl: http://localhost:7010
        cors:
          origin: http://localhost:7010
        database:
          client: better-sqlite3
          connection: ':memory:'

      auth:
        providers:
          guest:
            dangerouslyAllowOutsideDevelopment: true

      integrations:
        github:
          - host: github.com
            token: ${GITHUB_TOKEN}

      catalog:
        locations:
          - type: url
            target: https://github.com/your-org/your-repo/blob/main/backstage/templates/your-template/template.yaml
            rules:
              - allow: [Template]

    startupProbe:
      httpGet:
        path: /healthcheck
        port: 7007
      initialDelaySeconds: 120
      periodSeconds: 10
      failureThreshold: 30

    readinessProbe:
      httpGet:
        path: /healthcheck
        port: 7007
      initialDelaySeconds: 120
      periodSeconds: 10
      failureThreshold: 3

    livenessProbe:
      httpGet:
        path: /healthcheck
        port: 7007
      initialDelaySeconds: 300
      periodSeconds: 30
      failureThreshold: 3

    resources:
      requests:
        cpu: 100m
        memory: 512Mi

    extraEnvVarsSecrets:
      - backstage-github-token

  postgresql:
    enabled: false    # using in-memory SQLite for local dev
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;deploying-and-verifying&quot;&gt;Deploying and verifying&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm repo add backstage https://backstage.github.io/charts
helm repo update

kubectl create namespace backstage

# Create the GitHub token secret first
kubectl create secret generic backstage-github-token \
  -n backstage \
  --from-literal=GITHUB_TOKEN=&quot;your-token&quot;

# Install
helm install backstage backstage/backstage \
  -n backstage \
  -f infrastructure/backstage/values.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Watch the startup:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl get pods -n backstage -w
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expect the pod to stay in &lt;code&gt;Running 0/1&lt;/code&gt; for 60–120 seconds while Node.js&lt;br&gt;
starts. Do not interpret this as a failure. The startup probe will not&lt;br&gt;
pass until the application is ready.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Access the portal:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl -n backstage port-forward svc/backstage 7010:7007
# Open: http://localhost:7010
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Verify the backend is responding:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:7010/healthcheck
# Expected: {&quot;status&quot;:&quot;ok&quot;}

curl http://localhost:7010/api/scaffolder/v2/actions
# Expected: JSON list of available scaffolder actions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Verify catalog ingestion:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl logs -n backstage deploy/backstage --tail=50 \
  | grep -i &quot;processed\|warn\|error&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for &lt;code&gt;Processed N entities&lt;/code&gt; with no &lt;code&gt;NotAllowedError&lt;/code&gt; lines.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-production-values-profile&quot;&gt;The production values profile&lt;/h3&gt;
&lt;p&gt;Separate your dev and prod configuration into two files. The difference is&lt;br&gt;
significant enough that sharing a single file creates dangerous defaults&lt;br&gt;
in production.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# infrastructure/backstage/values-prod.yaml
backstage:
  backstage:
    image:
      registry: ghcr.io
      repository: your-org/your-backstage-app   # your own image
      tag: &quot;1.2.3&quot;                               # pinned, never latest

    replicaCount: 2

    appConfig:
      app:
        baseUrl: https://backstage.your-domain.com
      backend:
        baseUrl: https://backstage.your-domain.com
        database:
          client: pg
          connection:
            host: ${POSTGRES_HOST}
            port: 5432
            user: ${POSTGRES_USER}
            password: ${POSTGRES_PASSWORD}
            database: backstage

      auth:
        environment: production
        providers:
          github:
            production:
              clientId: ${GITHUB_CLIENT_ID}
              clientSecret: ${GITHUB_CLIENT_SECRET}

    startupProbe:
      initialDelaySeconds: 60     # your own image starts faster
      failureThreshold: 18        # 3 minutes maximum

    resources:
      requests:
        cpu: 200m
        memory: 512Mi
      limits:
        cpu: &quot;1&quot;
        memory: 1Gi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply both files together:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm upgrade backstage backstage/backstage \
  -n backstage \
  -f infrastructure/backstage/values.yaml \
  -f infrastructure/backstage/values-prod.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prod values file overrides only what it specifies. Everything else&lt;br&gt;
comes from the base &lt;code&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;diagnostic-command-reference&quot;&gt;Diagnostic command reference&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Pod status and events
kubectl get pods -n backstage
kubec
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15068/deploying-backstage-on-kubernetes-with-the-helm-chart-the-infrastructure-first-guide</guid>
<pubDate>Fri, 17 Apr 2026 11:52:35 +0000</pubDate>
</item>
</channel>
</rss>