Introduction
Platform Engineering has emerged as a key discipline in 2026, focused on building self-service Internal Developer Platforms (IDPs) for developers. Instead of wasting time on infrastructure, devs can focus on business code thanks to abstractions like ready-to-use templates, automated pipelines, and integrated observability.
Why is it crucial? According to Gartner studies, 80% of enterprises will adopt IDPs by 2026 to cut toil (manual tasks) by 50%. Backstage, developed by Spotify and adopted by Netflix/LinkedIn, is the go-to open-source tool: a unified portal for service catalogs, scaffolding, docs, and CI/CD.
This tutorial walks you step-by-step through creating a basic IDP with Backstage: local installation, advanced config with PostgreSQL, a custom TypeScript plugin for Terraform, and Docker deployment. At the end, you'll have a functional, scalable platform ready for Kubernetes. Ideal for intermediate DevOps engineers. (148 words)
Prerequisites
- Node.js 20+ and Yarn 1.22+
- Docker and Docker Compose
- Basic knowledge of TypeScript and YAML
- GitHub account for templates (optional)
- Minimum 4 GB RAM for services
Creating the Backstage Project
npx @backstage/create-app@latest ./platform-idp --type nodejs --force
cd platform-idp
yarn install
yarn tscThis command initializes a complete Backstage project with a Node.js backend, React frontend, and ready packages. The --force option overwrites existing files if needed. Avoid npm since Yarn is optimized for Backstage monorepos; test with yarn dev to verify.
Initial Configuration
Once the project is created, Backstage runs in a sandbox with SQLite. We'll switch to PostgreSQL for a production-like setup and enable essential plugins like the scaffolder (project generation) and TechDocs (auto-generated docs).
app-config.yaml Configuration
app:
baseUrl: http://localhost:3000
bindConfig:
port: 3000
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
cors:
origin: http://localhost:3000
database:
client: pg
connection:
host: localhost
port: 5432
user: backstage
password: backstage
database: backstage
auth:
providers:
github:
clientId: ${GITHUB_CLIENT_ID}
clientSecret: ${GITHUB_CLIENT_SECRET}
proxy:
'/catalog':
target: http://localhost:7007/catalog-provider
'/scaffolder':
target: http://localhost:7007/scaffolder
techdocs:
builder: local
github:
baseUrl: https://github.com
token: ${GITHUB_TOKEN}This file centralizes all config: PostgreSQL for persistence, proxy for internal microservices, GitHub auth, and local TechDocs. Replace env variables (GITHUB_*) with your credentials. Pitfall: without cors.origin, the frontend will block; always restart after changes.
Docker Compose for Services
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: backstage
POSTGRES_PASSWORD: backstage
POSTGRES_DB: backstage
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
backstage:
build: .
ports:
- "3000:3000"
- "7007:7007"
environment:
POSTGRES_HOST: postgres
POSTGRES_USER: backstage
POSTGRES_PASSWORD: backstage
POSTGRES_DATABASE: backstage
GITHUB_TOKEN: ${GITHUB_TOKEN}
depends_on:
- postgres
volumes:
postgres_data:This Compose file orchestrates PostgreSQL and Backstage in containers. The build uses the project's Dockerfile. Run with docker compose up -d; expose env vars. Watch for the persistent volume to avoid data loss on restarts.
Launch and First Tests
Start with yarn dev locally or docker compose up. Access http://localhost:3000. Create a user, import a GitHub repo via Catalog. Test the scaffolder by generating a 'guestbook' project.
Custom TypeScript Plugin for Terraform
import { createRouter } from '@backstage/router';
import { PluginEnvironment } from '@backstage/backend-common';
import express from 'express';
export async function createRouterTerraform(env: PluginEnvironment) {
const router = createRouter();
router.get('/provision', async (req, res) => {
const { clusterName } = req.query;
// Simule appel Terraform CLI ou API
const output = {
status: 'provisioned',
cluster: clusterName,
endpoints: [`https://${clusterName}.example.com`]
};
res.json(output);
});
return router;
}
// Dans index.ts du backend :
// router.use('/api/terraform', await createRouterTerraform(env));This plugin adds a /api/terraform/provision API to simulate IaC provisioning. Integrate it into packages/backend/src/index.ts. It's extensible to real terraform apply execution. Pitfall: handle async errors with try/catch to prevent backend crashes.
Terraform Scaffolder Template
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: terraform-aws-vpc
title: AWS VPC Terraform
description: Déploie une VPC AWS via Terraform
tags:
- terraform
- aws
owner: platform-team
spec:
owner: platform-team
type: service
parameters:
- title: Nom VPC
type: string
ui:widget: text
required: true
validation:
$$js: "((value) => /^[a-zA-Z0-9-]+$/.test(value))"
steps:
- id: terraform-init
name: Init Terraform
action: debug:log
input:
message: "Provisioning VPC ${{ parameters.vpcName }}"
- id: publish
name: Publier template
action: publish:github
input:
repoUrl: github.com?repo=platform-idp&branch=main
defaultBranch: true
output:
links:
- title: Terraform Plan
url: https://console.aws.amazon.com
icon: cloudThis YAML template lets devs generate a Terraform VPC project via the UI. It validates input and publishes to GitHub. Place it in templates/ and import into Catalog. Avoid steps without a valid action to prevent scaffolding failures.
Production Deployment with Helm
helm repo add backstage https://backstage.github.io/charts
helm repo update
helm install backstage backstage/backstage \
--namespace backstage --create-namespace \
--set backend.extraEnv.GITHUB_TOKEN=$GITHUB_TOKEN \
--set postgresql.auth.postgresPassword=backstageThis script deploys Backstage to Kubernetes using the official Helm chart. Ensure your kubectl context is set. Customize with your secrets; monitor pods with kubectl logs. Common pitfall: forgetting the namespace causes port conflicts.
Best Practices
- Secure secrets: Use Vault or Kubernetes Secrets instead of plain env vars.
- Monitor with Prometheus: Add the Backstage plugin for Grafana dashboards.
- GitOps everything: Store YAML configs in Git and apply via ArgoCD.
- Horizontal scaling: Set backend replicas >1 for >50 users.
- Strict RBAC: Limit scaffolding templates to authorized teams.
Common Errors to Avoid
- Non-persistent DB: Without Docker volumes, Catalog data is lost on restart.
- Misconfigured CORS: Frontend/backend on different ports → 401 errors; check
app-config.cors. - Unrecompiled plugins: After TS additions, run
yarn tsc && yarn build. - Templates without validation: Free inputs cause Terraform errors; add
$$jsregex.
Next Steps
Dive deeper with the Backstage docs. Integrate Crossplane for true self-service IaC. Check out our Learni Platform Engineering courses for Kubernetes + Backstage masterclasses in enterprise settings.