How to Deploy Backstage on Kubernetes Step by Step

Deploying an internal developer portal requires a robust, scalable foundation that separates stateless application logic from persistent data layers. This guide provides a production-ready workflow for migrating from local development to a managed Kubernetes cluster. Before provisioning infrastructure, ensure your team has evaluated the broader Developer Portal Architecture & Frameworks landscape to align container orchestration choices with organizational security and compliance standards.

Context: Architecture & Prerequisites

Backstage operates as a stateless Node.js frontend and API layer backed by persistent storage for catalog metadata, software templates, and authentication sessions. Understanding how plugins, the catalog processor, and external identity providers interact is essential before containerization. A comprehensive review of the Backstage Architecture Deep Dive clarifies how to decouple plugin dependencies and manage database connections in distributed environments.

Prerequisites:

  • Active Kubernetes cluster (v1.24+) with RBAC enabled
  • Helm v3 CLI installed and configured
  • Provisioned PostgreSQL instance (managed or self-hosted)
  • Container registry with authenticated push/pull access
  • kubectl configured with cluster-admin or namespace-scoped privileges

Exact Configuration: Step-by-Step Deployment

1. Containerize the Application

Backstage requires a multi-stage build to strip development dependencies, compile TypeScript, and produce a lean production image.

FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn tsc && yarn build

FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN yarn install --production --frozen-lockfile
EXPOSE 7007
CMD ["node", "dist/packages/backend"]

2. Push Image & Tag

Build the image locally or in CI, then push it to your registry with a semantic version tag.

docker build -t registry.company.io/backstage:v1.2.0 .
docker push registry.company.io/backstage:v1.2.0

3. Define Helm Overrides

Create a values.yaml file to target the image, inject production environment variables, and configure TLS ingress. Reference database credentials via Kubernetes Secrets, never plaintext.

image:
 repository: registry.company.io/backstage
 tag: v1.2.0
 pullPolicy: IfNotPresent
env:
 - name: NODE_ENV
 value: production
 - name: BACKSTAGE_BASE_URL
 value: https://backstage.company.io
ingress:
 enabled: true
 hosts:
 - host: backstage.company.io
 paths: ["/"]
 tls:
 - secretName: backstage-tls
 hosts: ["backstage.company.io"]

4. Execute Deployment

Add the official Backstage Helm repository, create the namespace, and deploy. Ensure the PostgreSQL connection URI is injected via --set or a pre-created Secret.

helm repo add backstage https://backstage.github.io/charts
helm repo update

kubectl create namespace backstage

helm install backstage backstage/backstage \
 -f values.yaml \
 --namespace backstage \
 --set postgresql.url="postgresql://user:pass@postgres-host:5432/backstage"

Validation: Health Checks & Rollout Verification

Immediately verify deployment health and routing before routing production traffic.

# 1. Confirm rolling update completion
kubectl rollout status deployment/backstage -n backstage

# 2. Validate database & catalog initialization
kubectl logs -l app.kubernetes.io/name=backstage -n backstage | grep -E "Database connection established|CatalogClient initialized"

# 3. Test frontend accessibility via ingress
curl -I https://backstage.company.io/health

# 4. Local debugging (if ingress is pending)
kubectl port-forward svc/backstage 7007:7007 -n backstage

# 5. Monitor resource consumption
kubectl top pods -n backstage

Rollback Strategy: If health checks fail or pods enter CrashLoopBackOff, immediately revert to the previous stable release:

helm rollback backstage 1 -n backstage

Edge Cases: Production Hardening & Troubleshooting

Symptom Root Cause Resolution
Plugin compilation fails Missing native build tools in builder stage Add RUN apk add --no-cache python3 make gcc g++ to the builder stage of your Dockerfile.
Catalog processor cannot read cluster resources RBAC misconfiguration Create a dedicated ServiceAccount and bind a minimal ClusterRole with get, list, watch permissions for required API groups.
Pod scheduling fails / PVC pending Missing storageClassName or constrained nodes Explicitly define storageClassName in Helm values and configure volumeMounts with subPath to avoid node-local storage conflicts.
TLS handshake errors Ingress controller lacks cert rotation Deploy cert-manager with a ClusterIssuer, reference the generated secret in ingress.tls.secretName, and never expose raw DB ports externally.
OOMKilled during catalog sync Missing resource limits Define resources.requests and resources.limits in values.yaml. Start with cpu: 500m, memory: 1Gi and scale based on kubectl top pods metrics.

Critical Pitfalls to Avoid:

  • Running with NODE_ENV=development in production causes memory leaks and disables asset optimization.
  • Hardcoding PostgreSQL credentials in values.yaml violates security baselines; always use Secret references.
  • Misconfiguring ingress path routing strips static assets; ensure the ingress controller proxies / and /api correctly to the backend service.

Frequently Asked Questions

Can I deploy Backstage without an external PostgreSQL database? While SQLite works for local development, it is unsupported in production Kubernetes environments. PostgreSQL is required for concurrent catalog processing, template execution, and high-availability scaling.

How do I handle Backstage plugin updates on Kubernetes? Rebuild the container image with updated dependencies, push a new tag, and update the Helm release using helm upgrade. Kubernetes will perform a rolling update with zero downtime if readiness probes are correctly configured.

Why does the Backstage pod crash with ‘EACCES: permission denied’ errors? This typically occurs when the container runs as root but attempts to write to a read-only filesystem or restricted volume. Set securityContext.runAsUser: 1000 in the Helm values and ensure WORKDIR has appropriate write permissions.

Is it necessary to run a separate frontend and backend deployment? No. The official Backstage architecture bundles the frontend static assets and backend API into a single Node.js process. Deploying them together simplifies ingress routing, session management, and scaling.