What Backstage Provides#
Backstage is an open-source developer portal originally built by Spotify, now a CNCF Incubating project. It serves as the single UI layer for an internal developer platform, unifying the service catalog, documentation, scaffolding templates, and plugin-based integrations behind one interface. It does not replace your tools — it provides a consistent frontend for discovering and interacting with them.
The core components:
- Software Catalog: A registry of all services, libraries, APIs, and infrastructure components, populated from YAML descriptor files in your repositories.
- TechDocs: Documentation-as-code powered by MkDocs, rendered directly in the Backstage UI alongside the service it describes.
- Scaffolder: A template engine that creates new projects from predefined templates — repositories, CI pipelines, Kubernetes manifests, and all.
- Plugins: Backstage’s extension mechanism. The community provides plugins for Kubernetes, ArgoCD, PagerDuty, GitHub Actions, Terraform, and hundreds of other tools.
Installation#
Backstage requires Node.js 18+ and Yarn. Create a new Backstage app:
npx @backstage/create-app@latestThis scaffolds a full application with a frontend (packages/app), backend (packages/backend), and configuration (app-config.yaml). The process takes a few minutes as it installs dependencies.
Start the development server:
cd my-backstage-app
yarn devThis runs the frontend on port 3000 and the backend on port 7007. The development mode uses an in-memory SQLite database. For production, switch to PostgreSQL.
PostgreSQL Configuration#
Edit app-config.yaml (or create app-config.production.yaml for production overrides):
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}Backstage manages its own schema migrations. On first startup with the PostgreSQL backend, it creates the required tables automatically.
Configuring the Software Catalog#
The catalog is the heart of Backstage. Every entity — service, API, library, team, system — is defined in a catalog-info.yaml file at the root of its repository.
A minimal service definition:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-service
description: "Handles payment processing and billing"
annotations:
github.com/project-slug: "myorg/payment-service"
backstage.io/techdocs-ref: dir:.
tags:
- python
- payments
links:
- url: https://grafana.internal/d/payment-service
title: Dashboard
spec:
type: service
lifecycle: production
owner: team-payments
system: billing
providesApis:
- payment-api
dependsOn:
- component:user-service
- resource:payments-dbKey fields: spec.owner maps to a Group or User entity (critical for ownership tracking), spec.system groups related components, and metadata.annotations configure plugin behavior.
Register catalog sources in app-config.yaml:
catalog:
locations:
# Scan all repos in the org for catalog-info.yaml
- type: github-discovery
target: https://github.com/myorg/*/blob/main/catalog-info.yaml
# Or register individual locations
- type: url
target: https://github.com/myorg/payment-service/blob/main/catalog-info.yaml
rules:
- allow: [Component, API, System, Domain, Resource, Group, User, Location]The github-discovery provider scans all repositories in an organization and registers any that contain a catalog-info.yaml. This is the preferred approach — adding new services to the catalog requires only committing the YAML file.
Defining Systems, APIs, and Teams#
Beyond components, define the organizational structures:
# team.yaml
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: team-payments
description: "Payment and billing team"
spec:
type: team
children: []
members:
- user:jane.doe
- user:john.smith
---
# system.yaml
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: billing
description: "End-to-end billing and payment processing"
spec:
owner: team-payments
domain: commerce
---
# api.yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: payment-api
description: "Payment processing REST API"
spec:
type: openapi
lifecycle: production
owner: team-payments
system: billing
definition:
$text: ./openapi.yamlStore these in a central repository (e.g., backstage-catalog) or co-locate them with the services they describe. The catalog resolves references across files — dependsOn: component:user-service links to whatever entity has metadata.name: user-service.
TechDocs Setup#
TechDocs renders MkDocs-based documentation inside Backstage. Each service includes a mkdocs.yml and a docs/ directory.
At the service root:
# mkdocs.yml
site_name: Payment Service
nav:
- Home: index.md
- Architecture: architecture.md
- API Reference: api.md
- Runbook: runbook.md
plugins:
- techdocs-coreWith a docs/ directory containing the Markdown files referenced in nav.
Configure TechDocs in app-config.yaml:
techdocs:
builder: local # 'local' for dev, 'external' for production
generator:
runIn: docker # Uses the techdocs-container Docker image
publisher:
type: awsS3 # Or googleGcs, azureBlobStorage
awsS3:
bucketName: myorg-techdocs
region: us-east-1For production, use the external builder — a CI pipeline generates the static docs site on each merge to main and uploads to object storage. Backstage serves the pre-built docs from the bucket. This is faster and more reliable than building docs on demand.
The CI step to generate and publish docs:
npx @techdocs/cli generate --source-dir . --output-dir ./site
npx @techdocs/cli publish --publisher-type awsS3 \
--storage-name myorg-techdocs \
--entity default/component/payment-service \
--directory ./siteScaffolder Templates#
The scaffolder creates new projects from templates. A template defines the UI form, input parameters, and a sequence of actions (create repo, write files, open PR, register in catalog).
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: new-python-service
title: Python Microservice
description: "Create a new Python microservice with FastAPI, Docker, and CI/CD"
tags:
- python
- fastapi
- recommended
spec:
owner: team-platform
type: service
parameters:
- title: Service Details
required:
- name
- owner
- system
properties:
name:
title: Service Name
type: string
pattern: "^[a-z][a-z0-9-]*$"
description:
title: Description
type: string
owner:
title: Owner
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
system:
title: System
type: string
ui:field: EntityPicker
ui:options:
catalogFilter:
kind: System
- title: Infrastructure
properties:
database:
title: Database
type: string
enum: ["none", "postgresql", "mysql"]
default: "none"
cacheLayer:
title: Cache
type: string
enum: ["none", "redis"]
default: "none"
steps:
- id: fetch
name: Fetch Skeleton
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
system: ${{ parameters.system }}
database: ${{ parameters.database }}
cacheLayer: ${{ parameters.cacheLayer }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
repoUrl: github.com?owner=myorg&repo=${{ parameters.name }}
description: ${{ parameters.description }}
defaultBranch: main
protectDefaultBranch: true
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Open in Catalog
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}The ./skeleton directory contains the template files with Nunjucks-style placeholders (${{ values.name }}). When a developer fills in the form and submits, Backstage creates the repository, populates it with the skeleton files, and registers the new service in the catalog — all in one action.
Store templates in a dedicated repository and register them in the catalog:
catalog:
locations:
- type: url
target: https://github.com/myorg/backstage-templates/blob/main/templates/**/template.yamlGitHub Integration#
Configure GitHub authentication and API access in app-config.yaml:
integrations:
github:
- host: github.com
apps:
- appId: ${GITHUB_APP_ID}
privateKey: ${GITHUB_APP_PRIVATE_KEY}
clientId: ${GITHUB_APP_CLIENT_ID}
clientSecret: ${GITHUB_APP_CLIENT_SECRET}
webhookSecret: ${GITHUB_WEBHOOK_SECRET}
auth:
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}Using a GitHub App (rather than a personal access token) is strongly recommended. It provides higher rate limits, granular repository permissions, and organizational visibility. Create the app in your GitHub organization settings with these permissions: Repository contents (read), Pull requests (read/write), Metadata (read), and Members (read).
Adding Plugins#
Plugins extend Backstage with integrations. Install a plugin package and wire it into the frontend or backend.
Example — adding the Kubernetes plugin to see pod status in the service view:
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backendConfigure the cluster connection in app-config.yaml:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- url: https://k8s-api.internal:6443
name: production
authProvider: serviceAccount
serviceAccountToken: ${K8S_SA_TOKEN}
skipTLSVerify: falseThen add the Kubernetes tab to the entity page in packages/app/src/components/catalog/EntityPage.tsx. The exact wiring depends on the plugin, but it follows a consistent pattern: install the package, add configuration, and mount the component on the entity page.
Production Deployment#
For production, build the Docker image:
yarn build:backend
docker build -t backstage -f packages/backend/Dockerfile .Deploy to Kubernetes with a Deployment, Service, and Ingress. Key considerations:
- Run at least two replicas behind a load balancer.
- Use the PostgreSQL backend, not SQLite.
- Externalize all secrets (GitHub tokens, database passwords) into Kubernetes Secrets or Vault.
- Set
app.baseUrlandbackend.baseUrlto your actual domain. - Configure the catalog to refresh on a schedule (
catalog.providers.github.schedulein newer versions) rather than relying solely on webhook-triggered refreshes.
A Helm chart for Backstage is available in the community charts repository, which handles most of the Kubernetes resource configuration.