Table of Contents
Introduction
Backstage is an open-source developer portal created by Spotify that acts as a centralized hub for infrastructure tooling, documentation, and developer workflows. This guide provides a comprehensive approach to deploying Backstage on Kubernetes using Helm charts, with a focus on production readiness, security, and scalability.
Architecture Overview
The Backstage deployment architecture consists of multiple components working together to provide a unified developer experience:
graph TB    subgraph "External Services"        GIT[Git Provider<br/>GitHub/GitLab]        IDP[Identity Provider<br/>OAuth/SAML]        EXT[External APIs<br/>K8s, AWS, etc.]    end
    subgraph "Kubernetes Cluster"        subgraph "Ingress Layer"            ING[Ingress Controller<br/>NGINX/Traefik]            CERT[Cert Manager<br/>TLS Certificates]        end
        subgraph "Application Layer"            BS[Backstage<br/>Frontend & Backend]            TECH[TechDocs<br/>Service]            SEARCH[Search<br/>Service]        end
        subgraph "Data Layer"            PG[PostgreSQL<br/>Database]            REDIS[Redis<br/>Cache]            S3[Object Storage<br/>TechDocs]        end
        subgraph "Monitoring"            PROM[Prometheus]            GRAF[Grafana]            LOG[Logging<br/>Stack]        end    end
    subgraph "Developer Workstation"        DEV[Developer]    end
    DEV --> ING    ING --> BS    BS --> PG    BS --> REDIS    BS --> GIT    BS --> IDP    BS --> EXT    TECH --> S3    BS --> TECH    BS --> SEARCH    PROM --> BS
    style BS fill:#e3f2fd    style PG fill:#fff9c4    style ING fill:#e8f5e9    style PROM fill:#f3e5f5Deployment Workflow
The deployment process follows a structured approach to ensure proper configuration and security:
sequenceDiagram    participant Admin    participant Helm    participant K8s    participant Backstage    participant Database    participant External
    Admin->>Helm: helm repo add backstage    Admin->>Helm: helm install backstage    Helm->>K8s: Create Namespace    Helm->>K8s: Deploy Secrets    Helm->>K8s: Deploy ConfigMaps    Helm->>K8s: Deploy Database    K8s->>Database: Initialize Schema    Helm->>K8s: Deploy Backstage    K8s->>Backstage: Start Services    Backstage->>Database: Connect    Backstage->>External: Integrate Services    Admin->>Backstage: Access PortalSecurity Architecture
Security is paramount in a Backstage deployment. Here’s the security architecture:
graph TB    subgraph "Security Layers"        subgraph "Network Security"            FW[Firewall Rules]            NP[Network Policies]            TLS[TLS Encryption]        end
        subgraph "Authentication"            OAUTH[OAuth 2.0]            SAML[SAML 2.0]            MFA[Multi-Factor Auth]        end
        subgraph "Authorization"            RBAC[RBAC Policies]            PERM[Permission Model]            API[API Security]        end
        subgraph "Data Security"            ENC[Encryption at Rest]            VAULT[Secret Management]            AUDIT[Audit Logging]        end    end
    subgraph "Backstage Components"        FE[Frontend]        BE[Backend]        DB[Database]        PLUGINS[Plugins]    end
    TLS --> FE    OAUTH --> BE    RBAC --> BE    ENC --> DB    VAULT --> BE    NP --> BE    AUDIT --> BE
    style TLS fill:#ffebee    style OAUTH fill:#e3f2fd    style ENC fill:#e8f5e9    style AUDIT fill:#fff9c4Resource Configuration
Proper resource allocation ensures optimal performance:
graph LR    subgraph "Resource Allocation"        subgraph "Backstage Pod"            CPU1[CPU: 500m-1000m]            MEM1[Memory: 1Gi-2Gi]            DISK1[Disk: 10Gi]        end
        subgraph "PostgreSQL Pod"            CPU2[CPU: 500m-2000m]            MEM2[Memory: 2Gi-4Gi]            DISK2[Disk: 20Gi-100Gi]        end
        subgraph "Redis Pod"            CPU3[CPU: 100m-500m]            MEM3[Memory: 512Mi-1Gi]            DISK3[Disk: 1Gi-5Gi]        end    end
    style CPU1 fill:#e3f2fd    style MEM1 fill:#e8f5e9    style DISK2 fill:#fff9c4Installation Guide
Prerequisites
Before beginning the installation, ensure you have:
- Kubernetes Cluster: Version 1.19 or higher
 - Helm: Version 3.8 or higher
 - kubectl: Configured to access your cluster
 - Storage: Persistent storage provisioner
 - Ingress Controller: NGINX or similar (optional but recommended)
 - DNS: Domain name for accessing Backstage
 
Step 1: Add Helm Repository
# Add the Backstage Helm repositoryhelm repo add backstage https://backstage.github.io/chartshelm repo update
# Verify the repository was addedhelm search repo backstageStep 2: Create Namespace and Secrets
# Create a dedicated namespacekubectl create namespace backstage
# Create secret for PostgreSQLkubectl create secret generic backstage-postgresql \  --from-literal=postgres-password=$(openssl rand -base64 32) \  --namespace backstage
# Create secret for OAuth (if using GitHub)kubectl create secret generic backstage-oauth \  --from-literal=AUTH_GITHUB_CLIENT_ID=your-client-id \  --from-literal=AUTH_GITHUB_CLIENT_SECRET=your-client-secret \  --namespace backstageStep 3: Create Production Values File
Create a comprehensive values-production.yaml file:
global:  # Set your cluster domain  clusterDomain: cluster.local
backstage:  # Number of Backstage replicas for HA  replicas: 2
  image:    registry: ghcr.io    repository: backstage/backstage    tag: latest    pullPolicy: IfNotPresent
  # Resource limits and requests  resources:    requests:      memory: "1Gi"      cpu: "500m"    limits:      memory: "2Gi"      cpu: "1000m"
  # Container configuration  containerPorts:    backend: 7007
  # Extra environment variables  extraEnvVars:    - name: NODE_ENV      value: "production"    - name: LOG_LEVEL      value: "info"    - name: APP_CONFIG_backend_baseUrl      value: "https://backstage.example.com"    - name: APP_CONFIG_backend_cors_origin      value: "https://backstage.example.com"
  # Extra environment variables from secrets  extraEnvVarsSecrets:    - backstage-oauth
  # App configuration  appConfig:    app:      title: "My Company Backstage"      baseUrl: https://backstage.example.com
    organization:      name: "My Company"
    backend:      baseUrl: https://backstage.example.com      listen:        port: 7007      cors:        origin: https://backstage.example.com        methods: [GET, POST, PUT, DELETE]        credentials: true      database:        client: pg        connection:          host: ${POSTGRES_HOST}          port: ${POSTGRES_PORT}          user: ${POSTGRES_USER}          password: ${POSTGRES_PASSWORD}          database: backstage          ssl:            require: true            rejectUnauthorized: false      cache:        store: redis        connection: redis://backstage-redis:6379
    integrations:      github:        - host: github.com          token: ${GITHUB_TOKEN}
    auth:      environment: production      providers:        github:          production:            clientId: ${AUTH_GITHUB_CLIENT_ID}            clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
    catalog:      import:        entityFilename: catalog-info.yaml        pullRequestBranchName: backstage-integration      rules:        - allow: [Component, System, API, Resource, Location]      locations:        - type: url          target: https://github.com/myorg/software-catalog/blob/main/catalog-info.yaml
    techdocs:      builder: "external"      generator:        runIn: "docker"      publisher:        type: "awsS3"        awsS3:          bucketName: my-techdocs-bucket          region: us-east-1
    search:      elasticsearch:        nodes:          - http://elasticsearch:9200
  # Service Monitor for Prometheus  serviceMonitor:    enabled: true    path: /metrics    interval: 30s    labels:      prometheus: kube-prometheus
  # Pod Disruption Budget  podDisruptionBudget:    enabled: true    minAvailable: 1
  # Autoscaling configuration  autoscaling:    enabled: true    minReplicas: 2    maxReplicas: 5    targetCPU: 70    targetMemory: 80
# PostgreSQL configurationpostgresql:  enabled: true
  auth:    database: backstage    existingSecret: backstage-postgresql    secretKeys:      adminPasswordKey: postgres-password      userPasswordKey: postgres-password
  primary:    persistence:      enabled: true      size: 20Gi      storageClass: fast-ssd
    resources:      requests:        cpu: 500m        memory: 2Gi      limits:        cpu: 2000m        memory: 4Gi
    configuration: |      shared_buffers = 256MB      max_connections = 200      effective_cache_size = 1GB
    pgHbaConfiguration: |      local all all trust      host all all 0.0.0.0/0 md5      host all all ::1/128 md5
  metrics:    enabled: true    serviceMonitor:      enabled: true
# Redis configurationredis:  enabled: true
  architecture: replication
  auth:    enabled: true    existingSecret: backstage-redis    existingSecretPasswordKey: redis-password
  master:    persistence:      enabled: true      size: 5Gi
    resources:      requests:        cpu: 100m        memory: 512Mi      limits:        cpu: 500m        memory: 1Gi
  replica:    replicaCount: 2    persistence:      enabled: true      size: 5Gi
  metrics:    enabled: true    serviceMonitor:      enabled: true
# Ingress configurationingress:  enabled: true  className: nginx
  annotations:    cert-manager.io/cluster-issuer: letsencrypt-prod    nginx.ingress.kubernetes.io/backend-protocol: HTTP    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"    nginx.ingress.kubernetes.io/proxy-body-size: "10m"    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
  hosts:    - host: backstage.example.com      paths:        - path: /          pathType: Prefix
  tls:    - secretName: backstage-tls      hosts:        - backstage.example.com
# Network PolicynetworkPolicy:  enabled: true  ingress:    - from:        - namespaceSelector:            matchLabels:              name: ingress-nginx      ports:        - protocol: TCP          port: 7007Step 4: Install Backstage
# Perform a dry run firsthelm upgrade --install backstage backstage/backstage \  --namespace backstage \  --values values-production.yaml \  --dry-run
# Install Backstagehelm upgrade --install backstage backstage/backstage \  --namespace backstage \  --values values-production.yaml \  --wait \  --timeout 10m
# Verify the installationkubectl get pods -n backstagekubectl get svc -n backstagekubectl get ingress -n backstageStep 5: Post-Installation Configuration
# Check Backstage logskubectl logs -n backstage -l app.kubernetes.io/name=backstage -f
# Access Backstage locally (for testing)kubectl port-forward -n backstage svc/backstage 7007:7007
# Get the initial admin password (if configured)kubectl get secret -n backstage backstage-admin -o jsonpath="{.data.password}" | base64 -dSecurity Considerations
Authentication and Authorization
- OAuth/OIDC Integration:
 
auth:  providers:    oidc:      production:        metadataUrl: https://your-idp.com/.well-known/openid-configuration        clientId: ${AUTH_OIDC_CLIENT_ID}        clientSecret: ${AUTH_OIDC_CLIENT_SECRET}        scope: "openid profile email groups"        prompt: auto- RBAC Configuration:
 
permission:  enabled: true  rbac:    policies:      - effect: allow        principals:          - group:platform-team        permissions:          - catalog.entity.create          - catalog.entity.delete- API Token Security:
 
# Generate secure API tokenskubectl create secret generic backstage-api-tokens \  --from-literal=token1=$(openssl rand -hex 32) \  --namespace backstageNetwork Security
- Network Policies:
 
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata:  name: backstage-network-policy  namespace: backstagespec:  podSelector:    matchLabels:      app.kubernetes.io/name: backstage  policyTypes:    - Ingress    - Egress  ingress:    - from:        - namespaceSelector:            matchLabels:              name: ingress-nginx      ports:        - protocol: TCP          port: 7007  egress:    - to:        - namespaceSelector: {}      ports:        - protocol: TCP          port: 5432 # PostgreSQL        - protocol: TCP          port: 6379 # Redis    - to:        - namespaceSelector: {}          podSelector:            matchLabels:              k8s-app: kube-dns      ports:        - protocol: UDP          port: 53    - to: # Allow HTTPS egress        - ipBlock:            cidr: 0.0.0.0/0      ports:        - protocol: TCP          port: 443- TLS Configuration:
 
# Configure TLS for all internal communicationsbackstage:  appConfig:    backend:      https:        certificate:          key: /etc/backstage/tls/tls.key          cert: /etc/backstage/tls/tls.crtSecrets Management
- External Secrets Operator:
 
apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata:  name: backstage-secrets  namespace: backstagespec:  refreshInterval: 1h  secretStoreRef:    name: vault-backend    kind: SecretStore  target:    name: backstage-secrets  data:    - secretKey: github-token      remoteRef:        key: backstage/github        property: token    - secretKey: postgres-password      remoteRef:        key: backstage/database        property: password- Sealed Secrets:
 
# Install Sealed Secrets controllerkubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.18.0/controller.yaml
# Create a sealed secretecho -n "mypassword" | kubectl create secret generic backstage-db \  --dry-run=client \  --from-file=password=/dev/stdin \  -o yaml | kubeseal -o yaml > sealed-secret.yamlContainer Security
- Security Context:
 
backstage:  podSecurityContext:    runAsNonRoot: true    runAsUser: 1000    fsGroup: 1000    seccompProfile:      type: RuntimeDefault
  containerSecurityContext:    allowPrivilegeEscalation: false    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 1000    capabilities:      drop:        - ALL- Pod Security Standards:
 
apiVersion: v1kind: Namespacemetadata:  name: backstage  labels:    pod-security.kubernetes.io/enforce: restricted    pod-security.kubernetes.io/audit: restricted    pod-security.kubernetes.io/warn: restrictedTroubleshooting
Common Issues and Solutions
Database Connection Issues
# Check PostgreSQL pod statuskubectl get pods -n backstage -l app.kubernetes.io/name=postgresql
# Check PostgreSQL logskubectl logs -n backstage -l app.kubernetes.io/name=postgresql
# Test database connectionkubectl run -it --rm --restart=Never postgres-client \  --image=postgres:14 \  --namespace=backstage \  -- psql -h backstage-postgresql -U postgres -d backstage
# Check connection from Backstage podkubectl exec -it -n backstage deployment/backstage -- \  nc -zv backstage-postgresql 5432Authentication Issues
# Check OAuth configurationkubectl get secret -n backstage backstage-oauth -o yaml
# Verify environment variableskubectl exec -n backstage deployment/backstage -- env | grep AUTH
# Check Backstage auth logskubectl logs -n backstage -l app.kubernetes.io/name=backstage | grep -i authPerformance Issues
# Check resource usagekubectl top pods -n backstage
# Check horizontal pod autoscalerkubectl get hpa -n backstage
# Analyze slow queries in PostgreSQLkubectl exec -it -n backstage backstage-postgresql-0 -- \  psql -U postgres -d backstage -c "SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10;"Certificate Issues
# Check certificate statuskubectl get certificate -n backstage
# Describe certificatekubectl describe certificate backstage-tls -n backstage
# Check cert-manager logskubectl logs -n cert-manager deployment/cert-managerMaintenance and Upgrades
Regular Maintenance Tasks
- Database Maintenance:
 
# Backup databasekubectl exec -n backstage backstage-postgresql-0 -- \  pg_dump -U postgres backstage > backstage-backup-$(date +%Y%m%d).sql
# Vacuum databasekubectl exec -n backstage backstage-postgresql-0 -- \  psql -U postgres -d backstage -c "VACUUM ANALYZE;"- Log Rotation:
 
# Configure log rotation in values.yamlbackstage:  extraEnvVars:    - name: LOG_FILE_MAX_SIZE      value: "100MB"    - name: LOG_FILE_MAX_FILES      value: "5"- Monitoring Setup:
 
# ServiceMonitor for PrometheusapiVersion: monitoring.coreos.com/v1kind: ServiceMonitormetadata:  name: backstage  namespace: backstagespec:  selector:    matchLabels:      app.kubernetes.io/name: backstage  endpoints:    - port: backend      path: /metrics      interval: 30sUpgrade Process
- Pre-upgrade Checklist:
 
# Backup current installationhelm get values backstage -n backstage > backstage-values-backup.yaml
# Backup databasekubectl exec -n backstage backstage-postgresql-0 -- \  pg_dump -U postgres backstage > pre-upgrade-backup.sql
# Check for breaking changeshelm show readme backstage/backstage --version <new-version>- Perform Upgrade:
 
# Update Helm repositoryhelm repo update
# Check changeshelm diff upgrade backstage backstage/backstage \  --namespace backstage \  --values values-production.yaml
# Perform upgradehelm upgrade backstage backstage/backstage \  --namespace backstage \  --values values-production.yaml \  --wait \  --timeout 10m- Post-upgrade Validation:
 
# Check pod statuskubectl get pods -n backstage
# Verify application healthcurl -s https://backstage.example.com/api/health | jq .
# Check for errors in logskubectl logs -n backstage -l app.kubernetes.io/name=backstage --tail=100Best Practices
Development Workflow
- GitOps Integration:
 
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata:  name: backstage  namespace: argocdspec:  project: default  source:    repoURL: https://github.com/myorg/backstage-config    targetRevision: main    path: overlays/production  destination:    server: https://kubernetes.default.svc    namespace: backstage  syncPolicy:    automated:      prune: true      selfHeal: true- Multi-Environment Setup:
 
# Directory structurebackstage-config/├── base/│   ├── kustomization.yaml│   └── values.yaml├── overlays/│   ├── development/│   │   ├── kustomization.yaml│   │   └── values.yaml│   ├── staging/│   │   ├── kustomization.yaml│   │   └── values.yaml│   └── production/│       ├── kustomization.yaml│       └── values.yamlPlugin Management
- Custom Plugin Deployment:
 
# Dockerfile for custom Backstage imageFROM node:16-bullseye-slim as build
WORKDIR /app
# Copy and install dependenciesCOPY package.json yarn.lock ./COPY packages ./packagesRUN yarn install --frozen-lockfile
# Build the applicationRUN yarn build
# Runtime stageFROM node:16-bullseye-slim
WORKDIR /app
# Copy built applicationCOPY --from=build /app/packages/backend/dist ./COPY --from=build /app/packages/app/dist ./app
# Install production dependenciesCOPY package.json yarn.lock ./RUN yarn install --production --frozen-lockfile
EXPOSE 7007CMD ["node", "packages/backend"]- Plugin Configuration:
 
# values.yaml plugin configurationbackstage:  appConfig:    app:      plugins:        - id: "@backstage/plugin-kubernetes"          config:            clusters:              - name: production                url: https://k8s-prod.example.com                authProvider: serviceAccount        - id: "@backstage/plugin-sonarqube"          config:            baseUrl: https://sonarqube.example.com            apiKey: ${SONARQUBE_API_KEY}Monitoring and Observability
- Grafana Dashboard:
 
{  "dashboard": {    "title": "Backstage Monitoring",    "panels": [      {        "title": "Request Rate",        "targets": [          {            "expr": "rate(http_request_duration_seconds_count{job=\"backstage\"}[5m])"          }        ]      },      {        "title": "Error Rate",        "targets": [          {            "expr": "rate(http_request_duration_seconds_count{job=\"backstage\",status=~\"5..\"}[5m])"          }        ]      },      {        "title": "Response Time",        "targets": [          {            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job=\"backstage\"}[5m]))"          }        ]      }    ]  }}- Alert Rules:
 
apiVersion: monitoring.coreos.com/v1kind: PrometheusRulemetadata:  name: backstage-alerts  namespace: backstagespec:  groups:    - name: backstage      rules:        - alert: BackstageDown          expr: up{job="backstage"} == 0          for: 5m          annotations:            summary: "Backstage is down"            description: "Backstage has been down for more than 5 minutes"
        - alert: BackstageHighErrorRate          expr: rate(http_request_duration_seconds_count{job="backstage",status=~"5.."}[5m]) > 0.05          for: 10m          annotations:            summary: "High error rate in Backstage"            description: "Error rate is above 5% for 10 minutes"
        - alert: BackstageHighMemoryUsage          expr: container_memory_usage_bytes{pod=~"backstage-.*"} / container_spec_memory_limit_bytes > 0.9          for: 10m          annotations:            summary: "High memory usage in Backstage"            description: "Memory usage is above 90% for 10 minutes"References
Conclusion
Deploying Backstage on Kubernetes with Helm provides a scalable, secure, and maintainable developer portal solution. By following this guide and implementing the security best practices, you can create a production-ready Backstage deployment that serves as a central hub for your engineering organization.
Remember to:
- Regularly update Backstage and its dependencies
 - Monitor performance and security metrics
 - Backup your data regularly
 - Test disaster recovery procedures
 - Keep documentation updated
 - Engage with the Backstage community for support and contributions
 
The flexibility of Backstage, combined with Kubernetes’ orchestration capabilities and Helm’s package management, creates a powerful platform for improving developer experience and productivity.