Backstage is an open-source developer portal framework built by Spotify and donated to the CNCF. It started as Spotify’s internal tool for managing thousands of microservices — a single place where any engineer could find what services existed, who owned them, where their documentation lived, and how to create a new one. Backstage was open-sourced in 2020 and is now the de facto standard for building internal developer portals.
At its core, Backstage solves the “where does this live?” problem that plagues growing engineering organizations. Services scatter across GitHub repos, documentation hides in Confluence, runbooks live in Notion, and nobody knows what depends on what. Backstage pulls all of this into a single, searchable, maintained catalog — and builds self-service tooling on top of it.
This guide covers the four pillars of a production Backstage deployment: the Software Catalog, Software Templates (scaffolding), TechDocs (documentation as code), and Plugins (extending the platform).
Architecture Overview
Backstage is a React frontend with a Node.js backend. Its key concepts:
┌─────────────────────────────────────────────────────────┐
│ Backstage App │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Catalog │ │ Scaffolder │ │ TechDocs │ │
│ │ (registry) │ │ (templates) │ │ (docs site) │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Plugins │ │ Auth / RBAC │ │ Search │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
│ │
│ Backend API ←→ Database (PostgreSQL) │
│ ←→ External integrations (GitHub, PD, etc) │
└─────────────────────────────────────────────────────────┘
Entities are the objects in Backstage’s catalog: services (Component), APIs, teams (Group), people (User), systems, domains, and resources. Entities are defined in catalog-info.yaml files that live alongside the code they describe.
Plugins extend the frontend and backend. There are 200+ community plugins for Kubernetes, PagerDuty, GitHub, SonarQube, Datadog, ArgoCD, Vault, and dozens more.
Getting Started
Creating a Backstage App
1
2
3
4
5
6
7
8
9
10
11
|
# Requires Node.js 18+ and yarn
npx @backstage/create-app@latest
# Prompts for app name (e.g., "lunarops-portal")
# Creates a fully functional Backstage app in ./lunarops-portal/
cd lunarops-portal
# Start in development mode
yarn dev
# Opens http://localhost:3000
|
The generated structure:
lunarops-portal/
├── app-config.yaml # main configuration
├── app-config.local.yaml # local overrides (gitignored)
├── packages/
│ ├── app/ # React frontend
│ │ ├── src/
│ │ │ ├── App.tsx # plugin registration, routes
│ │ │ └── components/ # custom components
│ │ └── package.json
│ └── backend/ # Node.js backend
│ ├── src/
│ │ └── index.ts # backend plugin registration
│ └── package.json
└── plugins/ # local custom plugins
Core Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
# app-config.yaml
app:
title: LunarOps Developer Portal
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7007
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
database: backstage
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
auth:
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
catalog:
# Auto-discover catalog-info.yaml files from GitHub
providers:
github:
myorg:
organization: 'lunarops'
catalogPath: '/catalog-info.yaml'
filters:
branch: 'main'
repository: '.*' # all repos
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 3 }
rules:
- allow: [Component, API, Group, User, Resource, System, Domain, Template, Location]
techdocs:
builder: 'local'
generator:
runIn: 'local'
publisher:
type: 'local'
# For production: store TechDocs in object storage
# techdocs:
# builder: 'external'
# publisher:
# type: 'googleGcs'
# googleGcs:
# bucketName: lunarops-techdocs
# credentials: ${GOOGLE_APPLICATION_CREDENTIALS}
|
The Software Catalog
The catalog is Backstage’s core value. Every service, API, library, website, team, and person has an entity. Entities are described in catalog-info.yaml files that live in the repository they describe — the catalog is always in sync with the code.
Entity Kinds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
# catalog-info.yaml — a Component (a service, library, or website)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-api
title: My API Service
description: REST API for order management
annotations:
github.com/project-slug: lunarops/my-api
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: P1234567
argocd/app-name: my-api-production
sonarqube.org/project-key: my-api
tags:
- go
- rest-api
- orders
links:
- url: https://my-api.lunarops.io
title: Production
icon: web
- url: https://grafana.internal/d/my-api
title: Dashboard
icon: dashboard
- url: https://wiki.internal/runbooks/my-api
title: Runbook
icon: help
spec:
type: service # service | library | website | documentation
lifecycle: production # experimental | production | deprecated
owner: orders-team
system: order-management
dependsOn:
- component:default/postgres-orders
- component:default/redis-cache
providesApis:
- my-api-v1
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# An API entity — describes the API contract
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: my-api-v1
description: Order Management API v1
spec:
type: openapi
lifecycle: production
owner: orders-team
system: order-management
definition:
$text: ./openapi.yaml # reference to OpenAPI spec file
|
1
2
3
4
5
6
7
8
9
|
# A System — groups related components
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: order-management
description: Everything related to order processing
spec:
owner: orders-team
domain: commerce
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# A Group — a team
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: orders-team
description: The order management engineering team
spec:
type: team
profile:
displayName: Orders Team
email: orders-team@lunarops.io
picture: https://avatars.githubusercontent.com/t/12345
parent: engineering
children: []
members:
- alice
- bob
- carol
|
Auto-Discovery with GitHub Provider
Rather than registering each service manually, the GitHub provider scans your organization:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# app-config.yaml
catalog:
providers:
github:
lunarops:
organization: 'lunarops'
catalogPath: '/catalog-info.yaml' # look for this file in every repo
filters:
branch: 'main'
repository: '^(?!\.github).*' # exclude .github repo
schedule:
frequency: { minutes: 30 }
timeout: { minutes: 5 }
initialDelay: { seconds: 15 }
|
Any repo that adds a catalog-info.yaml file to main is automatically discovered and added to the catalog within 30 minutes.
Dependency Visualization
The catalog’s real power: once services declare their dependsOn relationships, Backstage renders a dependency graph. You can see at a glance what depends on a database you’re planning to migrate, or trace the upstream impact of a service going down.
1
2
3
4
5
6
7
8
|
# In component's catalog-info.yaml
spec:
dependsOn:
- component:default/payments-service
- resource:default/orders-postgres
- resource:default/orders-redis
consumesApis:
- payments-api-v2
|
Navigate to the component in Backstage → “Dependencies” tab → interactive graph showing the full dependency tree.
Software Templates: Self-Service Scaffolding
Software Templates are the self-service engine of Backstage. A template defines a form that developers fill in, and a set of actions that run to scaffold code, create repositories, provision resources, and register the new entity in the catalog.
A Production-Ready Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
# templates/go-http-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: go-http-service
title: Go HTTP Service
description: |
Production-ready Go HTTP service with:
- GitHub Actions CI/CD
- Kubernetes manifests (Helm chart)
- Prometheus metrics and Grafana dashboard
- Structured logging (zerolog)
- Health check endpoints
- OpenAPI spec
tags:
- go
- http
- recommended
annotations:
backstage.io/techdocs-ref: dir:.
spec:
owner: platform-team
type: service
parameters:
# Step 1: Service information
- title: Service Information
required:
- name
- description
- owner
properties:
name:
title: Service Name
type: string
description: Lowercase, hyphen-separated (e.g., order-processor)
pattern: '^[a-z][a-z0-9-]{2,50}[a-z0-9]$'
ui:autofocus: true
description:
title: Description
type: string
description: What does this service do? (shown in catalog)
owner:
title: Owner
type: string
description: Team responsible for this service
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
system:
title: System
type: string
description: Which system does this service belong to?
ui:field: EntityPicker
ui:options:
catalogFilter:
kind: System
# Step 2: Infrastructure options
- title: Infrastructure
properties:
includeDatabase:
title: Include PostgreSQL database?
type: boolean
default: false
includeRedis:
title: Include Redis cache?
type: boolean
default: false
includeQueue:
title: Include message queue (NATS)?
type: boolean
default: false
repoVisibility:
title: Repository Visibility
type: string
default: private
enum:
- private
- internal
- public
# Step 3: Deployment configuration
- title: Deployment
properties:
productionApproval:
title: Require manual approval for production deployments?
type: boolean
default: true
minReplicas:
title: Minimum Replicas
type: integer
default: 2
minimum: 1
maximum: 10
cpuRequest:
title: CPU Request (millicores)
type: integer
default: 100
memoryRequest:
title: Memory Request (Mi)
type: integer
default: 128
steps:
# 1. Fetch and render the skeleton template
- id: fetch-skeleton
name: Generate Code
action: fetch:template
input:
url: ./skeleton
copyWithoutRender:
- .github/workflows/*.yml # don't template YAML actions syntax
values:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
system: ${{ parameters.system }}
includeDatabase: ${{ parameters.includeDatabase }}
includeRedis: ${{ parameters.includeRedis }}
cpuRequest: ${{ parameters.cpuRequest }}
memoryRequest: ${{ parameters.memoryRequest }}
minReplicas: ${{ parameters.minReplicas }}
productionApproval: ${{ parameters.productionApproval }}
year: ${{ '' | now('YYYY') }}
# 2. Create the GitHub repository
- id: create-repo
name: Create GitHub Repository
action: github:repo:create
input:
repoUrl: github.com?owner=lunarops&repo=${{ parameters.name }}
description: ${{ parameters.description }}
defaultBranch: main
repoVisibility: ${{ parameters.repoVisibility }}
collaborators:
- team: ${{ parameters.owner }}
access: admin
# 3. Push the generated code
- id: push-code
name: Push Code to GitHub
action: publish:github
input:
repoUrl: github.com?owner=lunarops&repo=${{ parameters.name }}
defaultBranch: main
commitMessage: "feat: initial service scaffold from platform template"
# 4. Create branch protection rules
- id: branch-protection
name: Configure Branch Protection
action: github:repo:push
input:
repoUrl: github.com?owner=lunarops&repo=${{ parameters.name }}
branchProtection:
- pattern: main
requirePullRequestReviews: true
requiredApprovingReviewCount: 1
requireStatusChecks: true
statusChecks: ['ci', 'security-scan']
# 5. Provision Vault secrets path
- id: provision-vault
name: Provision Vault Secrets Path
action: http:backstage:request
input:
method: POST
path: /api/proxy/vault/v1/secret/data/${{ parameters.name }}/config
body:
data:
placeholder: "Replace this with real secrets"
# 6. Register ArgoCD application
- id: create-argocd-app
name: Register with ArgoCD
action: argocd:create-resources
input:
appName: ${{ parameters.name }}-staging
argoInstance: lunarops-production
namespace: ${{ parameters.name }}-staging
repoUrl: https://github.com/lunarops/${{ parameters.name }}
path: deploy/helm
# 7. Register in the Backstage catalog
- id: register-catalog
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['push-code'].output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
# Show useful links when the template completes
output:
links:
- title: Repository
url: ${{ steps['push-code'].output.remoteUrl }}
icon: github
- title: Open in catalog
icon: catalog
entityRef: ${{ steps['register-catalog'].output.entityRef }}
- title: ArgoCD
url: https://argocd.internal/applications/${{ parameters.name }}-staging
icon: dashboard
text:
- title: Next Steps
content: |
Your service **${{ parameters.name }}** has been created! Here's what to do next:
1. Clone the repo: `git clone git@github.com:lunarops/${{ parameters.name }}.git`
2. Add real secrets to Vault: `vault kv put secret/${{ parameters.name }}/config key=value`
3. Push your first commit — CI will run automatically
4. Your service will auto-deploy to staging on merge to `main`
|
The Template Skeleton
The skeleton/ directory contains the files to render using Nunjucks templating:
skeleton/
├── catalog-info.yaml
├── README.md
├── go.mod
├── go.sum
├── main.go
├── internal/
│ ├── server/
│ │ └── server.go
│ └── health/
│ └── health.go
├── deploy/
│ └── helm/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── ingress.yaml
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── deploy.yml
└── docs/
├── index.md
└── mkdocs.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# skeleton/catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.name }}
title: ${{ values.name | title }}
description: ${{ values.description }}
annotations:
github.com/project-slug: lunarops/${{ values.name }}
backstage.io/techdocs-ref: dir:.
spec:
type: service
lifecycle: experimental
owner: ${{ values.owner }}
system: ${{ values.system }}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// skeleton/main.go
package main
import (
"log"
"net/http"
"os"
"github.com/lunarops/${{ values.name }}/internal/server"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
srv := server.New()
log.Printf("Starting ${{ values.name }} on :%s", port)
if err := http.ListenAndServe(":"+port, srv); err != nil {
log.Fatal(err)
}
}
|
TechDocs: Documentation as Code
TechDocs transforms Markdown files in your repo into a beautiful documentation site hosted inside Backstage. Documentation lives next to the code, gets reviewed in PRs, and never goes stale because it’s coupled to the service it documents.
Setting Up TechDocs for a Service
Add a docs/ directory and mkdocs.yml to any repository:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# mkdocs.yml (at the repo root)
site_name: My API Service
site_description: Order management REST API
docs_dir: docs/
nav:
- Home: index.md
- Architecture:
- Overview: architecture/overview.md
- Data Model: architecture/data-model.md
- API: architecture/api.md
- Operations:
- Deployment: operations/deployment.md
- Runbook: operations/runbook.md
- Alerting: operations/alerting.md
- Development:
- Getting Started: development/getting-started.md
- Local Setup: development/local-setup.md
- Testing: development/testing.md
plugins:
- techdocs-core # required Backstage plugin
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<!-- docs/index.md -->
# My API Service
The Order Management API handles all order lifecycle operations — creation,
payment processing, fulfillment, and cancellation.
## Quick Links
| Resource | Link |
|----------|------|
| GitHub | [lunarops/my-api](https://github.com/lunarops/my-api) |
| Production | [my-api.lunarops.io](https://my-api.lunarops.io) |
| Dashboard | [Grafana](https://grafana.internal/d/my-api) |
| Alerts | [PagerDuty](https://lunarops.pagerduty.com/services/P1234567) |
## Ownership
**Team:** [Orders Team](/catalog/groups/default/orders-team)
**On-call:** Rotating — see PagerDuty schedule
**SLO:** 99.5% availability, P99 < 500ms
## Architecture
```mermaid
graph LR
Client --> API[my-api]
API --> DB[(PostgreSQL)]
API --> Cache[(Redis)]
API --> Payments[payments-service]
API --> Notify[notification-service]
|
```markdown
<!-- docs/operations/runbook.md -->
# Runbook: My API Service
## High Error Rate
**Alert:** `MyApiHighErrorRate`
### Step 1: Check pod health
```bash
kubectl get pods -n my-api
kubectl describe pod <failing-pod> -n my-api
Step 2: Check recent deployments
1
|
kubectl rollout history deployment/my-api -n my-api
|
If recent deployment: kubectl rollout undo deployment/my-api -n my-api
Step 3: Check database connectivity
1
2
|
kubectl exec -it deployment/my-api -n my-api -- \
/bin/sh -c "pg_isready -h $DB_HOST -p $DB_PORT"
|
- Primary on-call: PagerDuty escalation policy “orders-team”
- Escalation: #orders-team-incidents in Slack
```yaml
# catalog-info.yaml — point to your docs
metadata:
annotations:
backstage.io/techdocs-ref: dir:. # mkdocs.yml is at the repo root
TechDocs renders this in Backstage under the component’s “Docs” tab — styled, searchable, and always reflecting the current state of the main branch.
Building TechDocs in CI
For production deployments, build TechDocs in CI and publish to object storage (GCS, S3, Azure Blob):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# .github/workflows/techdocs.yml
name: Publish TechDocs
on:
push:
branches: [main]
paths:
- 'docs/**'
- 'mkdocs.yml'
jobs:
publish-techdocs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install TechDocs CLI
run: pip install mkdocs-techdocs-core
- name: Generate TechDocs
run: techdocs-cli generate --no-docker --source-dir . --output-dir ./site
- name: Publish TechDocs to GCS
run: |
techdocs-cli publish \
--publisher-type googleGcs \
--storage-name lunarops-techdocs \
--entity default/Component/my-api \
--directory ./site
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GCS_SA_KEY }}
|
Plugins: Extending Backstage
Plugins are what make Backstage genuinely useful beyond the catalog. They surface external system data directly on entity pages — no tab-switching required.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# Kubernetes plugin — show pod status on component pages
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend
# ArgoCD plugin — show deployment status
yarn --cwd packages/app add @roadiehq/backstage-plugin-argo-cd
# PagerDuty plugin — show on-call and incidents
yarn --cwd packages/app add @pagerduty/backstage-plugin
# GitHub Actions plugin — show CI/CD runs
yarn --cwd packages/app add @backstage/plugin-github-actions
# SonarQube plugin — show code quality metrics
yarn --cwd packages/app add @backstage/plugin-sonarqube
# Grafana plugin — embed dashboards
yarn --cwd packages/app add @k-phoen/backstage-plugin-grafana
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// packages/app/src/components/catalog/EntityPage.tsx
// Add plugin cards to the service entity page
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
import { EntityArgoCDOverviewCard } from '@roadiehq/backstage-plugin-argo-cd';
import { EntityPagerDutyCard } from '@pagerduty/backstage-plugin';
import { EntityGithubActionsContent } from '@backstage/plugin-github-actions';
import { EntitySonarQubeCard } from '@backstage/plugin-sonarqube';
const serviceEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityLinksCard />
</Grid>
{/* Show ArgoCD deployment status */}
<Grid item md={6}>
<EntityArgoCDOverviewCard />
</Grid>
{/* Show PagerDuty on-call and recent incidents */}
<Grid item md={6}>
<EntityPagerDutyCard />
</Grid>
{/* Show SonarQube code quality */}
<Grid item md={6}>
<EntitySonarQubeCard />
</Grid>
</Grid>
</EntityLayout.Route>
{/* Full Kubernetes tab */}
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
{/* GitHub Actions CI/CD tab */}
<EntityLayout.Route path="/ci-cd" title="CI/CD">
<EntityGithubActionsContent />
</EntityLayout.Route>
{/* TechDocs tab */}
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
{/* API definition tab */}
<EntityLayout.Route path="/api" title="API">
<EntityApiDefinitionCard />
</EntityLayout.Route>
{/* Dependency graph */}
<EntityLayout.Route path="/dependencies" title="Dependencies">
<EntityDependencyOfComponentsCard />
<EntityDependsOnComponentsCard />
<EntityDependsOnResourcesCard />
</EntityLayout.Route>
</EntityLayout>
);
|
Writing a Custom Plugin
When no community plugin exists, write your own. Backstage’s plugin system is well-designed for this:
1
2
3
4
5
|
# Scaffold a new plugin
yarn backstage-cli new --select plugin
# Prompts for plugin ID (e.g., "deployment-tracker")
# Creates packages/app/src/plugins/deployment-tracker/
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
// A simple custom plugin that shows deployment history from your API
// packages/app/src/plugins/deployment-tracker/src/components/DeploymentCard.tsx
import React, { useEffect, useState } from 'react';
import { useApi, discoveryApiRef } from '@backstage/core-plugin-api';
import { useEntity } from '@backstage/plugin-catalog-react';
import { InfoCard, Table } from '@backstage/core-components';
interface Deployment {
version: string;
environment: string;
deployedAt: string;
deployedBy: string;
status: 'success' | 'failed' | 'in-progress';
}
export const DeploymentHistoryCard = () => {
const { entity } = useEntity();
const discoveryApi = useApi(discoveryApiRef);
const [deployments, setDeployments] = useState<Deployment[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDeployments = async () => {
const baseUrl = await discoveryApi.getBaseUrl('deployment-tracker');
const serviceName = entity.metadata.name;
const response = await fetch(
`${baseUrl}/deployments/${serviceName}?limit=10`
);
const data = await response.json();
setDeployments(data.deployments);
setLoading(false);
};
fetchDeployments();
}, [entity.metadata.name, discoveryApi]);
const columns = [
{ title: 'Version', field: 'version' },
{ title: 'Environment', field: 'environment' },
{ title: 'Deployed By', field: 'deployedBy' },
{ title: 'Time', field: 'deployedAt' },
{
title: 'Status',
field: 'status',
render: (row: Deployment) => (
<span style={{ color: row.status === 'success' ? 'green' : 'red' }}>
{row.status}
</span>
),
},
];
return (
<InfoCard title="Deployment History">
<Table
isLoading={loading}
data={deployments}
columns={columns}
options={{ paging: false, search: false }}
/>
</InfoCard>
);
};
|
Deploying Backstage to Kubernetes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
# k8s/backstage-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
namespace: backstage
spec:
replicas: 2
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
serviceAccountName: backstage
containers:
- name: backstage
image: ghcr.io/lunarops/backstage:latest
ports:
- containerPort: 7007
env:
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: backstage-db
key: host
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: backstage-db
key: password
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: backstage-secrets
key: github-token
- name: AUTH_GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: backstage-secrets
key: github-client-id
- name: AUTH_GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: backstage-secrets
key: github-client-secret
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
readinessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: backstage
namespace: backstage
spec:
selector:
app: backstage
ports:
- port: 80
targetPort: 7007
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backstage
namespace: backstage
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
rules:
- host: backstage.lunarops.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backstage
port:
number: 80
tls:
- hosts:
- backstage.lunarops.io
secretName: backstage-tls
|
Build the Docker image as part of CI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
# Dockerfile
FROM node:18-bookworm-slim AS base
WORKDIR /app
RUN apt-get update && apt-get install -y python3 g++ make && rm -rf /var/lib/apt/lists/*
FROM base AS deps
COPY package.json yarn.lock ./
COPY packages/app/package.json packages/app/
COPY packages/backend/package.json packages/backend/
RUN yarn install --frozen-lockfile --network-timeout 600000
FROM deps AS build
COPY . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
RUN yarn --cwd packages/app build
FROM node:18-bookworm-slim AS runtime
WORKDIR /app
RUN apt-get update && apt-get install -y libsqlite3-dev python3 && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/packages/backend/dist ./packages/backend/dist
COPY --from=build /app/packages/backend/node_modules ./packages/backend/node_modules
COPY --from=build /app/packages/app/dist ./packages/app/dist
COPY app-config.yaml ./
USER node
EXPOSE 7007
CMD ["node", "packages/backend/dist/index.js"]
|
Making Backstage Succeed: Common Pitfalls
The empty catalog problem. A catalog with 3 entries is useless. Run a “catalog migration sprint” where every team adds catalog-info.yaml to their repos in one week. Make it a requirement for all new services from day one.
Stale data. Backstage is only as useful as the data it shows. Auto-discovery via the GitHub provider solves this for entity discovery. For plugin data (PagerDuty, Grafana, Kubernetes), the integrations pull live data — but only if annotations are correct. Audit annotations quarterly.
Plugin overload. It’s tempting to install every plugin. Start with five that address real pain: Kubernetes status, CI/CD history, on-call (PagerDuty), code quality (SonarQube), and TechDocs. Add more when developers ask for them.
Treating it as a build-once project. Backstage requires ongoing maintenance — plugin updates, configuration changes, new template additions. Assign someone to own it as a product, not as infrastructure. Schedule quarterly “Backstage days” where the platform team improves templates and documentation based on developer feedback.
Missing authentication. The demo setup uses no auth. Production requires GitHub OAuth or OIDC. Users seeing each other’s data or being unable to identify component owners breaks the trust model.
Backstage works when it becomes the place developers actually go — not because they’re required to, but because it’s genuinely useful. A service page with live Kubernetes pod status, recent deploys, on-call schedule, code coverage, and documentation all in one tab is more useful than seven separate tools. The catalog makes onboarding a new engineer faster. Templates make creating a new service a form rather than a multi-day process. TechDocs makes documentation reviewable and maintainable.
The investment is real — Backstage takes time to set up and maintain. But the return is an organization where “where does this live?” has an answer, and “how do I create a new service?” is a 10-minute task rather than a week-long project.
Comments