Build a Platform Engineering Internal Developer Portal with Backstage

Deploy Spotify's Backstage on Kubernetes, build a software catalog, create golden path templates, and integrate with GitHub and your monitoring stack.

Domains & Technologies

Domains
CI-CD
Technologies
KUBERNETESTERRAFORMAWS

Blueprint Walkthrough

Architecture Overview

This project builds an Internal Developer Portal (IDP) using Backstage — the open-source platform engineering tool originally created by Spotify and now used by Atlassian, Netflix, Airbnb, and CRED. An IDP is the single place where every developer in your organisation goes to understand what services exist, who owns them, how to create new ones, and how to access documentation and runbooks.

Without an IDP, onboarding a new engineer means a 2-week scavenger hunt — finding the right Slack channel to ask about a service, discovering that the documentation is 18 months out of date, and spending days understanding the CI/CD setup. With Backstage, everything is in one place.

◈ DIAGRAM
Developer
|
v
+------------------+
| Backstage IDP | <- Single portal for everything
| |
| Software Catalog| <- What services exist and who owns them
| Tech Docs | <- Documentation for every service
| Templates | <- Golden paths to create new services
| CI/CD View | <- Pipeline status from GitHub Actions
+------------------+
/ | \ \
/ | \ \
GitHub PagerDuty Kubernetes Datadog
(repos) (incidents) (cluster) (metrics)
Problem Solved

As an engineering team grows beyond 10 people, two problems appear simultaneously. First, nobody knows what services exist or who owns them — the payment service, the notification service, the auth service, each built by a different team, each with different deployment processes and documentation styles. Second, creating a new service is a multi-day process of copying configuration from an existing service, setting up CI/CD from scratch, and asking three different people how to do things correctly.

Backstage solves both. The Software Catalog gives every service a home page with owner, documentation, deployment status, and incident history. Golden Path Templates let a developer create a new production-ready service in 5 minutes — the template scaffolds the repository, sets up CI/CD, registers the service in the catalog, and creates the initial documentation structure.

Step-by-Step Implementation Guide

Step 1: Create a Backstage Application

Backstage is a Node.js application that you own and customise. You create your own instance from the official template and deploy it.

Bash
## Prerequisites
node --version # Needs Node.js 18 or 20
npm --version # Needs npm 8+
yarn --version # Backstage uses Yarn
## If yarn is not installed
npm install -g yarn
## Create a new Backstage application
npx @backstage/create-app@latest
## When prompted:
## Enter a name for your app: devops-network-portal
## This takes 3-5 minutes to set up
cd devops-network-portal
## Start the development server to verify it works
yarn dev
## Opens at http://localhost:3000
## You should see the Backstage interface with the example catalog
Remember

Backstage is YOUR application — you own the code and deploy it yourself. Think of it like a Next.js app that you customise. The Backstage team provides the framework and plugins, you provide the configuration and your company's specific integrations.

Step 2: Configure the Software Catalog

The Software Catalog is the heart of Backstage. Every service, website, library, and data pipeline in your organisation gets a catalog-info.yaml file at the root of its repository. Backstage reads these files and builds the catalog.

Create a catalog-info.yaml for each of your existing services:

YAML
## catalog-info.yaml — add this to each service repository
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: order-service
description: Handles order creation, tracking, and lifecycle for the platform
annotations:
# Connect to GitHub for repository info
github.com/project-slug: devops-network/order-service
# Connect to GitHub Actions for CI/CD status
github.com/actions-workflow: deploy.yml
# Documentation location
backstage.io/techdocs-ref: dir:.
tags:
- nodejs
- api
- orders
links:
- url: https://grafana.devopsnetwork.in/d/order-service
title: Grafana Dashboard
icon: dashboard
- url: https://runbooks.devopsnetwork.in/order-service
title: Runbook
icon: help
spec:
type: service
lifecycle: production # or: experimental, deprecated
owner: group:platform-team # Team that owns this service
system: order-management
dependsOn:
- component:payment-service
- component:notification-service
providesApis:
- order-api

Configure Backstage to discover these catalog files from GitHub:

Edit app-config.yaml in your Backstage application:

YAML
## app-config.yaml (in your Backstage app root)
catalog:
providers:
github:
devops-network-org:
organization: 'your-github-org' # Your GitHub organization name
catalogPath: '/catalog-info.yaml' # Where to look in each repo
filters:
branch: 'main'
repository: '.*' # Match all repositories
schedule:
frequency: { minutes: 30 } # Scan GitHub every 30 minutes
timeout: { minutes: 3 }
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN} # Set this as an environment variable

Step 3: Create a Golden Path Template

A Golden Path Template is a Software Template in Backstage that scaffolds a complete new service. When a developer clicks CREATE, they fill in a form with the service name and owner, and Backstage automatically creates the GitHub repository, sets up the directory structure, configures CI/CD, and registers the service in the catalog.

Create templates/nodejs-microservice/template.yaml:

YAML
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: nodejs-microservice
title: Node.js Microservice
description: Production-ready Node.js microservice with Docker, CI/CD, and Kubernetes manifests
tags:
- nodejs
- microservice
- recommended
spec:
owner: group:platform-team
type: service
parameters:
- title: Service Details
required:
- name
- description
- owner
properties:
name:
title: Service Name
type: string
description: Name of the new microservice (e.g., inventory-service)
pattern: '^[a-z][a-z0-9-]*$' # Enforce lowercase with hyphens
description:
title: Description
type: string
description: What does this service do?
owner:
title: Owner Team
type: string
description: The team that owns this service
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
environment:
title: Target Environment
type: string
description: Which environment to deploy to first
default: staging
enum:
- staging
- production
steps:
# Step 1: Fetch the template skeleton
- id: fetch-base
name: Fetch Base Template
action: fetch:template
input:
url: ./skeleton # The template files live in ./skeleton directory
values:
name: ${{ parameters.name }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
# Step 2: Create the GitHub repository
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: ['github.com']
description: ${{ parameters.description }}
repoUrl: github.com?owner=devops-network&repo=${{ parameters.name }}
defaultBranch: main
repoVisibility: private
# Step 3: Register the service in the Software Catalog
- 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 }}

Create the template skeleton files in templates/nodejs-microservice/skeleton/:

◈ DIAGRAM
skeleton/
+-- catalog-info.yaml # Auto-registered catalog entry
+-- Dockerfile # Production-ready Dockerfile
+-- package.json # Node.js dependencies
+-- src/
| +-- index.js # Express server boilerplate
+-- k8s/
| +-- deployment.yaml # Kubernetes deployment
| +-- service.yaml # Kubernetes service
| +-- ingress.yaml # Kubernetes ingress
+-- .github/
| +-- workflows/
| +-- deploy.yml # GitHub Actions CI/CD pipeline
+-- docs/
+-- index.md # TechDocs starting point

All files in the skeleton use template variables like ${{ values.name }} that get replaced when the template runs.

Step 4: Enable TechDocs for Service Documentation

TechDocs lets every service have its own documentation site generated from Markdown files in the repository. Documentation lives next to code — when the code changes, developers update the docs in the same PR.

Bash
## Install the TechDocs CLI
npm install -g @techdocs/cli
## In each service repository, create a docs folder
mkdir docs
## Create the documentation entry point
cat > docs/index.md << 'EOF'
## Order Service
The Order Service handles the complete lifecycle of customer orders on the platform.
## Architecture
This service is a Node.js Express application that:
* Receives order creation requests from the API gateway
* Validates payment with the Payment Service
* Sends confirmation via the Notification Service
* Stores order state in PostgreSQL
## Running Locally
```bash
npm install
npm run dev

Deployment

Deployments are handled automatically by GitHub Actions on every merge to main. See the CI/CD pipeline for details.

On-Call Runbook

If this service is paging:

  1. Check Grafana dashboard for error rate spike
  2. Check PostgreSQL connection pool saturation
  3. Check Payment Service health (common upstream cause)
  4. Escalate to platform-team Slack channel if not resolved in 15 minutes EOF

Create mkdocs.yml configuration

cat > mkdocs.yml << 'EOF' site_name: Order Service Documentation docs_dir: docs nav:

  • Home: index.md plugins:
  • techdocs-core EOF
YAML
In `app-config.yaml`, configure TechDocs storage:
```yaml
techdocs:
builder: 'local' # Use 'external' and S3 for production
generator:
runIn: 'local'
publisher:
type: 'local'
local:
publishDirectory: '/tmp/techdocs'

Step 5: Deploy Backstage on Kubernetes

Bash
## Build the Backstage Docker image
yarn build:all
docker build -t backstage:latest .
## Push to your container registry
docker tag backstage:latest YOUR_ECR_REGISTRY/backstage:latest
docker push YOUR_ECR_REGISTRY/backstage:latest
## Create the Kubernetes manifests
kubectl create namespace backstage
## Create secret with required environment variables
kubectl create secret generic backstage-secrets \
--namespace backstage \
--from-literal=GITHUB_TOKEN=ghp_YOUR_GITHUB_TOKEN \
--from-literal=POSTGRES_PASSWORD=your_db_password
## Deploy Backstage
kubectl apply -n backstage -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
spec:
replicas: 1 # Backstage is not stateless — use 1 replica with shared DB
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
containers:
* name: backstage
image: YOUR_ECR_REGISTRY/backstage:latest
ports:
* containerPort: 7007
envFrom:
* secretRef:
name: backstage-secrets
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1"
---
apiVersion: v1
kind: Service
metadata:
name: backstage
spec:
selector:
app: backstage
ports:
* port: 80
targetPort: 7007
type: LoadBalancer
EOF
kubectl get svc -n backstage
## Note the EXTERNAL-IP — this is your IDP URL
Validation & Testing
Bash
## 1. Access the Backstage UI
BACKSTAGE_URL=$(kubectl get svc backstage -n backstage \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "Backstage available at: http://$BACKSTAGE_URL"
## 2. Verify the Software Catalog is populated
## Navigate to Catalog -> filter by Kind: Component
## Expected: All services with catalog-info.yaml appear
## 3. Test the Golden Path Template
## Navigate to Create -> Node.js Microservice
## Fill in the form and submit
## Verify: GitHub repo created, catalog entry registered, docs page available
## 4. Verify TechDocs renders for a service
## Click on a service in the catalog -> Docs tab
## Expected: Rendered Markdown documentation from the repository
## 5. Check GitHub integration shows CI/CD status
## Click on a service -> CI/CD tab
## Expected: GitHub Actions workflow runs visible
## 6. Verify dependency graph
## Click on order-service -> Dependencies tab
## Expected: Shows payment-service and notification-service as dependencies
## 7. Test catalog auto-discovery
## Add catalog-info.yaml to a new GitHub repository
## Wait 30 minutes (or trigger manual refresh)
## Verify new service appears in catalog automatically
echo "Internal Developer Portal fully operational"