Building an Internal Developer Portal with Backstage: Centralize Docs, Tools, and DevOps Workflows

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Why Your Team Needs an Internal Developer Portal

Picture this: a new developer joins your team. On day one, they need to find the API docs for the payment service, figure out which team owns the notification microservice, locate the CI/CD pipeline for the frontend, and understand the deployment process. Without a central place for all this, they spend days pinging people on Slack and digging through scattered wikis.

That’s the exact problem an Internal Developer Portal (IDP) exists to solve. In teams running more than a handful of services, poor discoverability quietly kills productivity — a developer spending 30 minutes tracking down an API owner, multiplied across 20 people, adds up to real engineering hours lost every week.

Backstage is an open-source framework built by Spotify and donated to the CNCF. It gives developers one place to browse the service catalog, read documentation, trigger pipelines, check infrastructure status, and onboard to new services — without bouncing between six different tools.

This guide focuses specifically on getting Backstage running locally, wiring up your first catalog entries, and setting up basic monitoring so you know the portal stays healthy. No fluff about why platform engineering matters — just the setup.

Installation: Spinning Up Backstage Locally

Before anything else, make sure you have these installed:

  • Node.js 18+ (check with node -v)
  • Yarn 1.22+ (check with yarn -v)
  • Git
  • Docker (optional, but you’ll want it for PostgreSQL later)

Create a New Backstage App

Backstage ships a CLI scaffolding tool. Run this in your terminal:

npx @backstage/create-app@latest

You’ll be prompted for an app name — something like my-developer-portal works fine. The CLI scaffolds a monorepo with two packages: packages/app (the React frontend) and packages/backend (the Node.js backend). Expect it to pull down around 800MB of dependencies.

cd my-developer-portal
yarn install
yarn dev

Give it a minute or two, then open http://localhost:3000. You’ll see the Backstage home screen with a sample catalog already loaded.

Understanding the Project Structure

Here’s what actually matters in the scaffolded project:

my-developer-portal/
├── app-config.yaml          # Main config file
├── app-config.local.yaml    # Local overrides (gitignored)
├── packages/
│   ├── app/                 # Frontend (React)
│   └── backend/             # Backend (Node.js + Express)
└── catalog-info.yaml        # Your first catalog entry (sample)

Almost all configuration lives in app-config.yaml — database connections, integrations, auth providers, and catalog locations. You’ll be editing this file a lot.

Configuration: Setting Up the Service Catalog

The service catalog is Backstage’s core feature. Every microservice, library, website, or API your team owns becomes a component in the catalog. Here’s how to register a real service.

Write a catalog-info.yaml for Your Service

Inside your actual service repository (not the Backstage repo), create a catalog-info.yaml at the root:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-service
  description: Handles all payment processing for checkout flow
  annotations:
    github.com/project-slug: my-org/payment-service
    backstage.io/techdocs-ref: dir:.
  tags:
    - nodejs
    - payments
    - api
spec:
  type: service
  lifecycle: production
  owner: payments-team
  system: checkout
  providesApis:
    - payment-api

This file tells Backstage everything it needs: who owns the service, what lifecycle stage it’s in (production, experimental, or deprecated), which APIs it exposes, and where the docs live.

Register the Component in Backstage

Open your portal at http://localhost:3000, go to CatalogRegister Existing Component, and paste the raw GitHub URL to your catalog-info.yaml:

https://github.com/my-org/payment-service/blob/main/catalog-info.yaml

Backstage imports the component immediately. It shows up in the catalog within seconds.

Connect GitHub Integration (for Auto-Discovery)

Manually registering every service doesn’t scale past about 10 repos. Backstage can auto-discover catalog-info.yaml files across your entire GitHub org. Edit app-config.yaml:

integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}

catalog:
  providers:
    github:
      my-org:
        organization: 'my-org'
        catalogPath: '/catalog-info.yaml'
        filters:
          branch: 'main'
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

Set the environment variable before starting:

export GITHUB_TOKEN=ghp_your_personal_access_token
yarn dev

Backstage will now crawl your GitHub org every 30 minutes, automatically picking up any repo with a catalog-info.yaml on the main branch. Your token needs repo and read:org scopes — without both, you’ll hit 403 errors during discovery.

Enable TechDocs (In-Portal Documentation)

TechDocs lets teams write docs in Markdown alongside their code and read them directly inside Backstage — no external wiki required. First, install the CLI tool:

pip install mkdocs-techdocs-core

In your service repo, create a mkdocs.yml:

site_name: Payment Service
docs_dir: docs
nav:
  - Home: index.md
  - API Reference: api.md
  - Runbooks: runbooks.md

Create a docs/index.md with your content. Open the service in Backstage and you’ll see a Docs tab rendering those Markdown files as a proper documentation site. Teams that adopt this pattern typically see their Slack “where are the docs?” questions drop noticeably within a few weeks.

Switch from SQLite to PostgreSQL (Production-Ready)

By default, Backstage uses an in-memory SQLite database. It resets on every restart, which is fine for local testing but useless for anything beyond that. Switch to PostgreSQL:

docker run -d \
  --name backstage-db \
  -e POSTGRES_USER=backstage \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=backstage \
  -p 5432:5432 \
  postgres:15

Update app-config.yaml:

backend:
  database:
    client: pg
    connection:
      host: localhost
      port: 5432
      user: backstage
      password: secret
      database: backstage

Install the PostgreSQL client for the backend:

yarn --cwd packages/backend add pg

Verification & Monitoring: Keeping the Portal Healthy

Deploying Backstage and walking away is how you end up with a stale portal nobody trusts. Here’s how to verify it’s working and catch problems early.

Check the Catalog API Directly

Backstage exposes a REST API you can query to confirm catalog ingestion is working:

# List all registered components
curl http://localhost:7007/api/catalog/entities?filter=kind=Component | jq '.[].metadata.name'

# Check a specific component
curl http://localhost:7007/api/catalog/entities/by-name/component/default/payment-service | jq '.metadata'

Your service showing up here means the catalog backend ingested it correctly. If it’s missing, check whether the GitHub token has the right scopes.

Monitor Catalog Refresh Logs

When GitHub auto-discovery runs, Backstage logs each refresh cycle. Watch the backend logs for problems:

yarn dev 2>&1 | grep -E '(catalog|error|warn)' | tail -f

Healthy output looks like [catalog] Processed 42 entities. Repeated 403 errors mean your token expired or is missing the repo or read:org scope. Fix the token, restart, and the next 30-minute refresh cycle will pick it up.

Health Check Endpoint

The Backstage backend exposes a health endpoint you can wire into any uptime monitor:

curl http://localhost:7007/healthcheck
# Expected response: {"status":"ok"}

Deploying to Kubernetes? Use this as your readiness probe:

readinessProbe:
  httpGet:
    path: /healthcheck
    port: 7007
  initialDelaySeconds: 10
  periodSeconds: 15

Verify TechDocs Rendering

After registering a component with TechDocs enabled, click the Docs tab. A blank page or “Documentation not found” means something went wrong during the build. Debug it locally:

cd your-service-repo
techdocs-cli generate --no-docker --verbose
techdocs-cli serve

This builds and previews the docs locally. Most failures come down to a missing mkdocs.yml, a wrong docs_dir path, or a broken Markdown file. Fix it here before it surfaces in the portal.

Track Catalog Coverage

Catalog coverage — the percentage of your real services that have a catalog-info.yaml — is the metric that actually tells you whether the portal is useful. Check it with the GitHub CLI:

# Count repos in your org
gh repo list my-org --limit 200 --json name | jq length

# Count repos with catalog-info.yaml
gh search code 'catalog-info.yaml' --owner my-org --json repository | jq '[.[].repository.name] | unique | length'

Target 80%+ coverage. For teams below that threshold, the fastest way to close the gap is adding catalog entries to your sprint definition-of-done — every new service ships with a catalog-info.yaml on day one.

Next Steps After the Basics

Once your catalog is populated and docs are rendering, several high-value features are worth tackling next:

  • Software Templates — Let developers scaffold new services directly in the portal, with your team’s conventions and security defaults baked in from the start
  • Auth integration — Connect GitHub SSO or your company’s identity provider so developers log in with existing credentials instead of managing a separate Backstage account
  • Custom plugins — Backstage has a plugin ecosystem covering Kubernetes, PagerDuty, Grafana, and Argo CD, surfacing relevant data directly on each service’s catalog page
  • Scorecards — Define maturity standards (has docs, has runbook, passes security scan) and track which services meet them across the org

Ten well-documented services in the catalog will already save your team meaningful time on Slack questions and context-switching. The portal gets more valuable as coverage grows — but it needs to be useful on day one to get adoption, so prioritize quality over quantity when you’re getting started.

Share: