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@latest

This 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 dev

This 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-db

Key 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.yaml

Store 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-core

With 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-1

For 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 ./site

Scaffolder 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.yaml

GitHub 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-backend

Configure 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: false

Then 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.baseUrl and backend.baseUrl to your actual domain.
  • Configure the catalog to refresh on a schedule (catalog.providers.github.schedule in 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.