Platform Engineering with Backstage 2026: Build an Internal Developer Platform from Scratch
Platform Engineering vs DevOps
DevOps solved a real problem: it tore down the wall between development and operations. But as organizations scaled to hundreds of engineers and dozens of microservices, a new problem emerged — cognitive overload. Every team reinvented the same wheels: CI pipelines, Kubernetes manifests, observability stacks, secret management patterns, and on-call runbooks. Developers spent more time navigating infrastructure than shipping features.
Platform Engineering is the response. Rather than asking every team to become infrastructure experts, it establishes golden paths — opinionated, well-maintained routes that let product engineers focus on business logic. The platform team builds and operates the Internal Developer Platform (IDP); product teams consume it through self-service interfaces.
The difference in day-to-day experience is stark. In a pure DevOps model, spinning up a new service means reading three wikis, copying a colleague's pipeline YAML, asking in Slack who owns the staging cluster, and hoping the Terraform module still works. On a mature IDP, you fill out a form, click submit, and receive a new GitHub repository pre-wired with CI/CD, monitoring, and Kubernetes deployment in under two minutes.
Backstage is the open-source framework that most platform teams reach for when building that self-service portal.
What Backstage Is
Backstage was created by Spotify to manage thousands of internal services. Spotify open-sourced it in 2020 and donated it to the Cloud Native Computing Foundation (CNCF), where it graduated as an incubating project. Today it is the de-facto standard for building developer portals.
At its core, Backstage is a React/Node.js web application built around three primitives:
- Software Catalog — a live inventory of every service, library, website, API, and infrastructure component in your organization.
- Software Templates (Scaffolder) — form-driven workflows that create new components and wire up boilerplate automatically.
- TechDocs — a "docs as code" system that renders Markdown documentation from each repo directly inside the portal.
Everything else — Kubernetes dashboards, cost tracking, incident management, GitHub Actions views — is a plugin. The plugin architecture means you can add first-party or community plugins, or write your own, without forking the core. The plugin marketplace lists over 200 community contributions.
Prerequisites and Installation
You need Node.js 20+, Yarn, Docker (for local TechDocs rendering), and a GitHub account with a personal access token scoped to repo and read:org.
Create a new Backstage app:
npx @backstage/create-app@latest
# Prompted for app name — use: my-idp
cd my-idp
yarn dev
Backstage starts on http://localhost:3000. The default setup uses an in-memory SQLite database and a static catalog. Before you go further, switch to PostgreSQL by installing the backend plugin and updating app-config.yaml:
# app-config.yaml (excerpt)
backend:
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: 5432
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
database: backstage
For production, deploy the backend as a Docker container and the frontend as a static site behind a CDN. The official Backstage Helm chart (backstage/backstage on Artifact Hub) handles this with minimal configuration.
The Software Catalog
The catalog is the heart of every internal developer platform tutorial. It answers the question every engineer eventually asks: "What services exist, who owns them, and where is the runbook?"
catalog-info.yaml for a Microservice
Every component registers itself by committing a catalog-info.yaml file at the repository root:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-service
description: Handles payment processing and refunds
annotations:
github.com/project-slug: acme-corp/payments-service
backstage.io/techdocs-ref: dir:.
kubernetes.io/label-selector: app=payments-service
tags:
- python
- fastapi
- payments
spec:
type: service
lifecycle: production
owner: team-payments
system: commerce
dependsOn:
- component:orders-service
- resource:postgres-payments
providesApis:
- payments-api
The dependsOn and providesApis fields make the catalog a live dependency graph. Backstage renders these as an interactive diagram, so an on-call engineer can immediately see upstream and downstream blast radius.
GitHub Org Discovery
Manually adding every repo's catalog-info.yaml to app-config.yaml does not scale. The GitHub discovery provider scans your entire organization automatically:
# app-config.yaml
catalog:
providers:
github:
acme-org:
organization: acme-corp
catalogPath: /catalog-info.yaml
filters:
branch: main
repository: '.*'
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
Install the provider package:
yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github
Register it in packages/backend/src/index.ts:
backend.add(import('@backstage/plugin-catalog-backend-module-github'));
Backstage will now index every repository that contains a catalog-info.yaml on a 30-minute cycle. New services appear automatically once the file is merged.
Software Templates (Scaffolder)
The Scaffolder is the self-service engine. It lets developers fill in a form — service name, team owner, description — and receive a fully bootstrapped repository with all your organization's standards baked in.
Creating a Python FastAPI Template
Templates live in your catalog. Create a templates/fastapi-service/template.yaml:
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: python-fastapi-service
title: Python FastAPI Microservice
description: Creates a new FastAPI service with CI/CD, Dockerfile, and Kubernetes manifests
tags:
- python
- fastapi
- recommended
spec:
owner: team-platform
type: service
parameters:
- title: Service details
required: [name, description, owner]
properties:
name:
title: Service name
type: string
pattern: '^[a-z][a-z0-9-]*$'
description: Lowercase, hyphens only (e.g. invoice-processor)
description:
title: Short description
type: string
owner:
title: Owning team
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
steps:
- id: fetch-template
name: Fetch template skeleton
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: [github.com]
description: ${{ parameters.description }}
repoUrl: github.com?owner=acme-corp&repo=${{ parameters.name }}
defaultBranch: main
repoVisibility: private
- 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
url: ${{ steps.register.output.catalogEntityUrl }}
The skeleton/ directory next to template.yaml contains the actual file templates with ${{ values.name }} placeholders. Include a Dockerfile, GitHub Actions workflow, catalog-info.yaml, mkdocs.yml, and a bare-bones FastAPI main.py. When a developer submits the form, Backstage materializes those templates, creates the GitHub repository, pushes the code, and registers the new service in the catalog — all in one atomic workflow.
This is developer self-service at its best: a product engineer has a production-ready, standards-compliant service skeleton in under two minutes without filing a ticket or pinging the platform team on Slack.
Kubernetes Plugin
The Kubernetes plugin surfaces pod health, resource consumption, and recent rollout status directly on each service's catalog page. Engineers never need to leave the portal to check whether their latest deployment is healthy.
Install the frontend and backend plugins:
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
Configure cluster access in app-config.yaml:
kubernetes:
serviceLocatorMethod:
type: multiTenant
clusterLocatorMethods:
- type: config
clusters:
- name: production-us-east
url: ${K8S_CLUSTER_URL}
authProvider: serviceAccount
serviceAccountToken: ${K8S_SERVICE_ACCOUNT_TOKEN}
caData: ${K8S_CA_DATA}
The plugin links a catalog component to its Kubernetes workloads via the annotation in catalog-info.yaml:
annotations:
kubernetes.io/label-selector: app=payments-service,env=production
Backstage then displays live pod counts, CPU and memory graphs, and the status of any active rollout — giving every on-call engineer immediate situational awareness without kubectl access.
TechDocs: Auto-Generated Documentation
TechDocs converts the docs/ directory in each repository into a searchable, versioned documentation site rendered inside Backstage. There is no separate doc hosting to manage.
Add mkdocs.yml to your service skeleton:
site_name: Payments Service
docs_dir: docs
plugins:
- techdocs-core
nav:
- Home: index.md
- Architecture: architecture.md
- Runbook: runbook.md
The annotation in catalog-info.yaml (backstage.io/techdocs-ref: dir:.) tells Backstage where to find the docs. In CI, build and publish the static site to object storage (S3, GCS) using the @techdocs/cli tool. In production mode, Backstage reads from the object store and skips on-the-fly generation, which is much faster for large organizations.
GitHub Actions Plugin
The GitHub Actions plugin embeds pipeline run history directly on a service's catalog page. Developers see their latest build status, test results, and deployment pipeline without opening a second browser tab.
yarn --cwd packages/app add @backstage-community/plugin-github-actions
Add the required annotation to catalog-info.yaml:
annotations:
github.com/project-slug: acme-corp/payments-service
The plugin picks up the GITHUB_TOKEN from app-config.yaml and renders a paginated table of workflow runs with status badges, timestamps, and direct links to individual job logs.
Measuring IDP Success
Shipping an IDP is an investment. Justify it with data. The four DORA metrics map directly to the value a mature platform delivers:
| Metric | What to measure | Target (Elite) |
|---|---|---|
| Deployment frequency | Deployments per team per day | On demand (multiple/day) |
| Lead time for changes | Commit to production | Less than one hour |
| Change failure rate | % of deployments causing incidents | 0 – 15% |
| Time to restore service | Mean time to recovery | Less than one hour |
Track these in your observability platform and surface them in Backstage via a custom plugin or the community @backstage-community/plugin-dora plugin. After six months of IDP adoption, the clearest signal is a drop in "how do I deploy?" Slack messages and a rise in self-service template usage — both easy to measure.
FAQ
Do I need Kubernetes to run Backstage? No. You can run Backstage as a Node.js process on any VM or container platform. Kubernetes is only required if you want to display workload health with the Kubernetes plugin.
Can I use Backstage without GitHub? Yes. There are catalog discovery providers for GitLab, Bitbucket, Azure DevOps, and Gitea. The Scaffolder has publish actions for all major Git hosts.
How long does an initial Backstage rollout take? A minimal portal with a catalog and one template typically takes two to four weeks for a two-person platform team. Full plugin integration (Kubernetes, TechDocs, GitHub Actions) adds another two to three weeks.
Is Backstage suitable for small teams? Backstage pays off at around 20+ engineers or 30+ microservices. Below that, the maintenance overhead may outweigh the self-service benefits. Consider the simpler port.io or OpsLevel SaaS alternatives for smaller organizations.
How do I keep the catalog accurate over time? Enable auto-discovery so that new repos register themselves when they add a catalog-info.yaml. Automate the file's creation via your service template. Add a linting step in CI that validates the schema. Mark components as lifecycle: deprecated when services are decommissioned rather than deleting the entity — historical context is valuable.