Building an internal developer platform sounds like a huge enterprise program. It can be, but your first version does not need to be huge. The first useful version needs one paved road that helps developers find a service, understand who owns it, and start a standard workflow without asking five different teams for links.
Backstage is a practical way to build that first version because it gives you the building blocks for a developer portal, a software catalog, and software templates. In this tutorial, you will build the shape of an Internal Developer Platform (IDP) with Backstage, Kubernetes deployment manifests, and a first self-service template.
By the end, you will have a realistic starter path for an IDP: a Backstage app, a catalog entry, a golden-path template, and deployment files you can adapt for a real cluster.
Prerequisites
To follow along, you will need the following:
-
A Unix-like workstation such as Linux, macOS, or Windows Subsystem for Linux.
-
Node.js Active LTS installed. Backstage currently recommends Node 22 or 24 for a standalone app.
-
Yarn available through Corepack.
-
Git and access to a Git hosting provider such as GitHub, GitLab, or Azure DevOps.
-
Basic Kubernetes knowledge if you plan to adapt the deployment examples.
-
A container registry and Kubernetes cluster for production use.
[!NOTE]The local Backstage app is a learning environment. The official Backstage getting-started guide notes that the standalone installation uses an in-memory SQLite database and demo content, so treat it as a starting point rather than a production deployment.
Choosing the First Platform Capability
Before you install anything, decide what your first IDP capability will do. A platform without a clear workflow becomes another portal full of stale links. A small, opinionated platform gives developers a useful path right away.
For a first build, use this target workflow:
-
A developer opens the platform.
-
The developer finds a service in the catalog.
-
The catalog shows ownership, lifecycle, system, and source location.
-
The developer starts a template for a new service or environment request.
-
The template hands off to source control and CI/CD.
-
Platform engineers review, improve, and automate the next bottleneck.
That workflow is intentionally modest. You are not trying to automate every deployment on day one. You are building a durable home for service metadata and one repeatable golden path.
[!TIP]Start with one paved road. A narrow IDP that creates a working service template beats a broad portal that only documents manual handoffs.
Creating a Backstage App
Start by creating a standalone Backstage app. The generated project gives you an app-config.yaml file, a root package.json, and separate frontend and backend packages. Those are the core pieces you will customize as your platform grows.
Run the app creation command from the directory where you keep platform projects.
npx @backstage/create-app@latest
When prompted, name the app something clear, such as internal-developer-platform.
Move into the new directory and start the app.
cd internal-developer-platform yarn dev
Open the local URL shown in your terminal, commonly http://localhost:3000. At this point, you have a developer portal shell. The real value arrives when you connect it to services, owners, documentation, and workflows.
Adding Your First Software Catalog Entry
The Backstage Software Catalog is a centralized system for software ownership and metadata. Backstage expects catalog metadata to live with the code, commonly in a catalog-info.yaml file stored in source control.
Create a catalog-info.yaml file in the root of a service repository. The following example describes a simple API service.
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-api
description: Handles payment authorization requests for internal applications
annotations:
github.com/project-slug: example-org/payments-api
spec:
type: service
lifecycle: experimental
owner: platform-team
system: commerce-platform
This file gives Backstage enough information to show the service in the catalog. The owner field is especially important because ownership is one of the biggest benefits of a developer portal. Once teams trust the catalog, they stop asking, “Who owns this service?” in chat.
Backstage can register catalog locations manually, through integrations, or through templates. For a first test, use the catalog registration flow in the Backstage UI and point it at the raw URL for your catalog-info.yaml file.
If your repositories live in GitHub or Azure DevOps, review the Backstage integration docs for GitHub locations or Azure DevOps locations before scaling beyond a single manual registration.
Turning a Repeatable Workflow Into a Software Template
A catalog helps developers find existing software. A template helps them create new software the right way. Backstage Software Templates can load a code skeleton, ask for inputs, and publish the output to a source control location.
Create a template file named template.yaml in a repository you use for platform templates.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: node-service-template
title: Node.js Service
description: Create a starter Node.js service with catalog metadata
spec:
owner: platform-team
type: service
parameters:
- title: Service details
required:
- component_id
- owner
properties:
component_id:
title: Component ID
type: string
description: Unique service name, such as orders-api
owner:
title: Owner
type: string
description: Backstage owner group, such as platform-team
steps:
- id: fetchBase
name: Fetch the base skeleton
action: fetch:template
input:
url: ./skeleton
values:
component_id: ${{ parameters.component_id }}
owner: ${{ parameters.owner }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
repoUrl: github.com?owner=example-org&repo=${{ parameters.component_id }}
description: Generated service from the internal developer platform
- id: register
name: Register in the catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Catalog entity
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}
This template shows the pattern, not a complete enterprise implementation. The fetch:template action reads a skeleton, publish:github publishes the generated repository, and catalog:register registers the new component.
Notice the step ID fetchBase. Backstage documentation warns that custom scaffolder action IDs should use camelCase instead of kebab-case because dashes can be interpreted as subtraction in template expressions. Using simple camelCase IDs keeps expressions predictable.
Adding a Catalog File to the Template Skeleton
A golden-path template should register its own output. Add a catalog-info.yaml file inside the template skeleton so every generated service starts with catalog metadata.
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.component_id }}
description: Generated by the internal developer platform
spec:
type: service
lifecycle: experimental
owner: ${{ values.owner }}
You can add more files to the skeleton over time: a Dockerfile, CI workflow, README, test structure, deployment chart, or service-level objective template. Keep the first version boring and reliable. Developers will trust a simple template that works more than a fancy template that fails halfway through.
Connecting Templates to CI/CD
Backstage does not magically deploy infrastructure by itself. The template starts a workflow, and the workflow must call the systems that actually build, test, provision, and deploy.
A common pattern is:
-
Backstage creates a repository from a standard skeleton.
-
The generated repository includes a CI/CD workflow file.
-
The CI/CD system builds and tests the service.
-
A deployment tool applies Kubernetes manifests, Helm charts, or GitOps changes.
-
Backstage links back to the repository, documentation, and runtime views.
For GitHub Actions, your skeleton might include a starter workflow like this.
name: service-ci
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
For Azure DevOps, the same idea can be expressed in azure-pipelines.yml.
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '22.x'
- script: npm ci
displayName: Install dependencies
- script: npm test
displayName: Run tests
These workflows are intentionally small. The first win is consistency: every generated service starts with the same test and build shape.
Preparing Backstage for Kubernetes
The official Backstage Kubernetes deployment guide describes Backstage as a stateless application that should use an external PostgreSQL database in production. That model fits Kubernetes well: Backstage runs as an application deployment, PostgreSQL stores state, and services provide stable network endpoints.
Start with a namespace.
apiVersion: v1 kind: Namespace metadata: name: backstage
Create a deployment for Backstage. The image value below is a placeholder for your own built Backstage image.
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
namespace: backstage
spec:
replicas: 2
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
containers:
- name: backstage
image: registry.example.com/platform/backstage:latest
ports:
- containerPort: 7007
env:
- name: POSTGRES_HOST
value: postgres.backstage.svc.cluster.local
- name: POSTGRES_PORT
value: '5432'
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: backstage-postgres
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: backstage-postgres
key: password
Expose Backstage with a Kubernetes Service.
apiVersion: v1
kind: Service
metadata:
name: backstage
namespace: backstage
spec:
selector:
app: backstage
ports:
- name: http
port: 80
targetPort: 7007
Kubernetes services matter because pods are temporary. The Kubernetes Service abstraction gives other workloads a stable way to reach the application even as pods are replaced.
Handling Secrets Safely
Your Backstage deployment will need tokens for source control integrations, authentication providers, and plugin backends. Kubernetes Secrets are the basic Kubernetes object for sensitive values, but base64 encoding is not encryption.
A demo secret looks like this.
apiVersion: v1 kind: Secret metadata: name: backstage-postgres namespace: backstage type: Opaque stringData: username: backstage password: replace-me-with-a-real-secret-manager-value
Use this shape for local learning only. For production, connect to your organization’s secret-management approach, enable cluster encryption at rest, restrict who can read secrets, and avoid committing real credentials to Git.
[!WARNING]Never commit real platform tokens to a template repository. Keep examples as placeholders and inject secrets through your approved secret manager or CI/CD secret store.
Adding Authentication and Permissions
A useful internal platform needs identity. Backstage has dedicated documentation for authentication and permissions. Do not treat auth as a final polish item. Even a small portal can expose repository links, ownership data, CI/CD entry points, and operational details.
For a first production path, define these rules before opening the platform broadly:
-
Which identity provider signs users in?
-
Which groups map to platform admins?
-
Who can run scaffolder templates?
-
Which templates require approval?
-
Who can register new catalog locations?
-
Which plugins expose operational or sensitive data?
Start strict and loosen access as the platform matures. This prevents the IDP from becoming a self-service foot-gun.
Validating the First Platform Slice
Once the app, catalog entry, and template exist, validate the platform as a developer would use it.
-
Open Backstage and find the sample service in the catalog.
-
Confirm the owner, lifecycle, and source annotations are visible.
-
Run the software template in a non-production organization or test project.
-
Confirm the generated repository includes a catalog descriptor.
-
Confirm CI starts from the generated workflow.
-
Confirm the new component can be registered in the catalog.
-
Document every manual step the developer still had to perform.
The last step is where your platform roadmap comes from. If every developer still has to request a namespace manually, build that automation next. If every service owner has to copy monitoring links manually, add those links to the skeleton or a plugin.
Common Mistakes to Avoid
Avoid these early IDP mistakes:
-
Starting with too many templates. One reliable template teaches more than ten half-finished ones.
-
Ignoring ownership data. The catalog is only useful when teams trust who owns what.
-
Treating Kubernetes Secrets as encrypted storage. Base64 is encoding, not encryption.
-
Skipping authentication and permissions. A developer portal can become a powerful control plane.
-
Automating an unclear process. Standardize the workflow before encoding it into a template.
-
Letting metadata rot. Make catalog metadata part of the pull-request review process.
Your first Internal Developer Platform does not need to solve every platform engineering problem. It needs to make one workflow easier and more consistent than it was yesterday.
Start with Backstage, register one real service, create one software template, and deploy the portal with production-minded defaults. Then watch where developers still get stuck. Each friction point becomes the next platform capability.
That is how a portal turns into a platform: one paved road at a time.