Backstage - Street-Level Ops¶
Quick Diagnosis Commands¶
# Check Backstage app status (Kubernetes deployment)
kubectl get pods -n backstage
kubectl logs -n backstage deployment/backstage --tail=50
# Check database connectivity (Backstage requires PostgreSQL in prod)
kubectl exec -n backstage deployment/backstage -- \
node -e "require('pg').Pool({connectionString: process.env.POSTGRES_URL}).query('SELECT 1')"
# Validate a catalog-info.yaml file locally
npx @backstage/cli catalog:validate catalog-info.yaml
# Run Backstage locally for development
yarn dev
# Build production bundle
yarn build
# Check Backstage CLI version
npx @backstage/cli --version
Gotcha: Backstage auto-discovers entities on a polling interval (default: every 100-200 seconds). After registering a new location, the entity may take several minutes to appear in the catalog. Hitting refresh in the UI does nothing -- use the
/api/catalog/refreshendpoint to force an immediate ingestion cycle.
# List installed plugins
cat package.json | jq '.dependencies | keys[] | select(startswith("@backstage"))'
# Backstage CLI commands
npx @backstage/cli new # scaffold new app or plugin
npx @backstage/cli versions:check # check for outdated Backstage packages
npx @backstage/cli versions:bump # bump all Backstage packages to latest
# App config validation
node packages/backend/dist/index.js --config app-config.yaml --dry-run
# Check entity ingestion status via API
curl -s http://localhost:7007/api/catalog/entities | jq 'length'
curl -s "http://localhost:7007/api/catalog/entities?filter=kind=component" | jq '.[].metadata.name'
# Force catalog refresh (admin API)
curl -X POST http://localhost:7007/api/catalog/refresh \
-H 'Content-Type: application/json' \
-d '{"entityRef": "component:default/my-service"}'
# Check catalog entity validation errors
curl -s "http://localhost:7007/api/catalog/locations" | jq '.[] | select(.data.error != null)'
# Get specific entity details
curl -s "http://localhost:7007/api/catalog/entities/by-name/component/default/my-service" | jq .
# List all locations (sources of entities)
curl -s "http://localhost:7007/api/catalog/locations" | jq '.[] | {type: .data.type, target: .data.target}'
# Check TechDocs build status
curl -s "http://localhost:7007/api/techdocs/metadata/docs/default/component/my-service" | jq .
Common Scenarios¶
Scenario 1: Entity Not Appearing in Catalog¶
A team added a catalog-info.yaml but their service doesn't show up in Backstage.
# Step 1: Check if the location is registered
curl -s "http://localhost:7007/api/catalog/locations" | \
jq '.[] | select(.data.target | contains("my-repo"))'
# Step 2: If not registered, add the location
curl -X POST http://localhost:7007/api/catalog/locations \
-H 'Content-Type: application/json' \
-d '{
"type": "url",
"target": "https://github.com/myorg/my-service/blob/main/catalog-info.yaml"
}'
# Step 3: Check for validation errors in the entity
curl -s "http://localhost:7007/api/catalog/entities/by-name/component/default/my-service" \
| jq '.metadata.annotations'
# Step 4: Validate the YAML format manually
cat catalog-info.yaml
# Required fields:
# apiVersion: backstage.io/v1alpha1
# kind: Component
# metadata:
# name: my-service
# spec:
# type: service
# lifecycle: production
# owner: team-name
# Step 5: Force refresh
curl -X POST http://localhost:7007/api/catalog/refresh \
-H 'Content-Type: application/json' \
-d '{"entityRef": "component:default/my-service"}'
Scenario 2: TechDocs Not Rendering¶
Service has mkdocs.yml and docs folder but TechDocs shows an error.
# Check TechDocs plugin configuration in app-config.yaml
grep -A 10 "techdocs:" app-config.yaml
# For local builds (development)
# app-config.yaml should have:
# techdocs:
# builder: local
# generator:
# runIn: local
# publisher:
# type: local
# For production (S3/GCS storage)
# techdocs:
# builder: external # built in CI, not by Backstage
# publisher:
# type: awsS3
# awsS3:
# bucketName: my-techdocs-bucket
# Build docs locally to test
cd path/to/service
npx @techdocs/cli generate --no-docker
npx @techdocs/cli serve # preview at http://localhost:3000
# Check mkdocs.yml has required Backstage config
cat mkdocs.yml
# site_name must exist
# plugins must include techdocs-core
Scenario 3: Auth Provider Not Working¶
Users get authentication errors when logging in via GitHub/Google OAuth.
# Check auth configuration in app-config.yaml
grep -A 20 "auth:" app-config.yaml
# Verify environment variables are set
kubectl exec -n backstage deployment/backstage -- env | grep -i "AUTH_\|CLIENT_ID\|CLIENT_SECRET"
# Common GitHub OAuth config:
# auth:
# environment: production
# providers:
# github:
# production:
# clientId: ${AUTH_GITHUB_CLIENT_ID}
# clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
# Check callback URL matches GitHub OAuth App settings
# Callback URL must be: https://backstage.example.com/api/auth/github/handler/frame
# Check Backstage logs for auth errors
kubectl logs -n backstage deployment/backstage --tail=100 | grep -i "auth\|oauth\|token"
Scenario 4: Database Migration Failures on Upgrade¶
Backstage fails to start after version upgrade due to schema migration issues.
# Check Backstage startup logs
kubectl logs -n backstage deployment/backstage --tail=200 | grep -i "migration\|database\|error"
# Check current database schema version
kubectl exec -n backstage deployment/backstage -- \
node -e "
const knex = require('knex')({ client: 'pg', connection: process.env.POSTGRES_URL });
knex('knex_migrations').select().then(r => { console.log(r); knex.destroy(); });
"
# Backstage runs migrations on startup automatically
# If stuck: check for long-running transactions or advisory locks
# Connect to PostgreSQL directly:
psql $POSTGRES_URL -c "SELECT pid, query, state FROM pg_stat_activity WHERE state != 'idle';"
# Kill blocking connections if needed
psql $POSTGRES_URL -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state != 'idle' AND pid != pg_backend_pid();"
# Rollback to previous Backstage version in emergency
kubectl set image deployment/backstage backstage=backstage:previous-version -n backstage
Key Patterns¶
catalog-info.yaml for Common Entity Types¶
# Component (a deployable service, library, or website)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-service
description: "Order processing service"
annotations:
github.com/project-slug: myorg/my-service
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: PXXXXXX
tags:
- java
- orders
spec:
type: service
lifecycle: production
owner: group:team-platform
system: order-management
dependsOn:
- component:default/order-database
providesApis:
- order-api
---
# API definition
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: order-api
spec:
type: openapi
lifecycle: production
owner: group:team-platform
definition:
$text: ./openapi.yaml
---
# Resource (database, S3 bucket, etc.)
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: order-database
spec:
type: database
owner: group:team-platform
system: order-management
app-config.yaml Structure¶
app:
title: My Company Portal
baseUrl: https://backstage.example.com
backend:
baseUrl: https://backstage.example.com
listen:
port: 7007
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: 5432
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
database: backstage
catalog:
rules:
- allow: [Component, System, API, Resource, Location, Group, User]
locations:
# Auto-discover catalog-info.yaml files in GitHub org
- type: github-discovery
target: https://github.com/myorg
techdocs:
builder: external
generator:
runIn: local
publisher:
type: awsS3
awsS3:
bucketName: ${TECHDOCS_S3_BUCKET}
region: us-east-1
Checking Entity Relationships¶
# Get all dependencies of a component
curl -s "http://localhost:7007/api/catalog/entities/by-name/component/default/my-service" | \
jq '.relations[] | {type: .type, target: .target.name}'
# Find all components owned by a team
curl -s "http://localhost:7007/api/catalog/entities?filter=kind=component,spec.owner=group:default/team-platform" | \
jq '.[].metadata.name'
# List all APIs in production lifecycle
curl -s "http://localhost:7007/api/catalog/entities?filter=kind=api,spec.lifecycle=production" | \
jq '.[].metadata.name'
Software Templates (Scaffolder)¶
# template.yaml — creates a new service from a template
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: service-template
title: New Service Template
spec:
owner: platform-team
type: service
parameters:
- title: Service Details
required: [name, owner]
properties:
name:
type: string
title: Service Name
owner:
type: string
title: Owner Team
steps:
- id: fetch-template
name: Fetch Template
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
- id: create-repo
name: Create Repository
action: publish:github
input:
repoUrl: github.com?repo=${{ parameters.name }}&owner=myorg
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.create-repo.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
Default trap: The Backstage scaffolder
publish:githubaction creates repos with the default branch name from your GitHub org settings (usuallymain). If your template referencesblob/master/catalog-info.yamlin the registration step, the catalog location silently fails to resolve. Always useblob/main/or parametrize the branch name.