Skip to main content
All posts
DevSecOps11 min read

Building an Internal Developer Portal with Backstage on Azure: A Practical Guide

A practical guide to deploying Spotify Backstage on Azure, covering AKS and Container Apps hosting, catalog setup, software templates, TechDocs, Azure DevOps integration, Entra ID authentication, and custom plugins.

Published

The average enterprise developer spends 60 minutes per day searching for information: which team owns a service, where the API docs live, how to create a new microservice that meets compliance requirements. Multiply that across 200 engineers and you lose 200 hours of engineering productivity every single day.

Backstage, the open-source developer portal created by Spotify, solves this by providing a single pane of glass for your entire software ecosystem. But deploying it on Azure in an enterprise context requires more than running npx @backstage/create-app. This guide covers the architecture decisions, deployment patterns, and integration points that matter in production.

Backstage Architecture Overview

Before deploying anything, you need to understand what Backstage actually is under the hood.

Loading diagram...

Backstage consists of three layers:

Core — The app shell, plugin system, and built-in features (catalog, scaffolder, TechDocs). This is what you get out of the box.

Plugins — Community and custom plugins that extend functionality. Everything from Kubernetes dashboards to cost management views. Plugins have frontend components (React) and optional backend components (Express).

App — Your specific Backstage instance with configuration, theming, and the set of plugins you have chosen to install.

The backend runs as a Node.js application with a PostgreSQL database. The frontend is a React single-page application served either by the backend or through a CDN. In production, you deploy them together as a single container or split them for independent scaling.

Choosing Your Azure Hosting: AKS vs Container Apps

This is the first decision that shapes everything downstream.

Azure Kubernetes Service (AKS)

Use AKS when:

  • You already operate AKS clusters and have a platform team comfortable with Kubernetes
  • You need fine-grained control over networking (private clusters, custom DNS, service mesh)
  • Your Backstage plugins require persistent volumes or complex sidecar patterns
  • You anticipate heavy plugin customization with multiple backend services
YAML
# 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-sa
      containers:
        - name: backstage
          image: myacr.azurecr.io/backstage:1.24.0
          ports:
            - containerPort: 7007
          envFrom:
            - secretRef:
                name: backstage-secrets
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "1000m"
          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
  type: ClusterIP

Pair this with an NGINX Ingress Controller or Azure Application Gateway Ingress Controller for TLS termination and external access.

Azure Container Apps

Use Container Apps when:

  • You want minimal operational overhead and your team does not manage Kubernetes
  • Your Backstage instance serves fewer than 500 developers
  • You need quick setup with built-in TLS, scaling, and revision management
Bicep
resource backstageApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: 'backstage'
  location: location
  properties: {
    managedEnvironmentId: containerAppEnv.id
    configuration: {
      ingress: {
        external: true
        targetPort: 7007
        transport: 'http'
      }
      secrets: [
        {
          name: 'postgres-connection'
          value: postgresConnectionString
        }
        {
          name: 'azure-client-secret'
          value: entraIdClientSecret
        }
      ]
    }
    template: {
      containers: [
        {
          name: 'backstage'
          image: '${acrName}.azurecr.io/backstage:1.24.0'
          resources: {
            cpu: json('1.0')
            memory: '2Gi'
          }
          env: [
            { name: 'POSTGRES_HOST', value: postgresHost }
            { name: 'POSTGRES_PORT', value: '5432' }
            { name: 'POSTGRES_USER', value: postgresUser }
            {
              name: 'POSTGRES_PASSWORD'
              secretRef: 'postgres-connection'
            }
          ]
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 3
        rules: [
          {
            name: 'http-scaling'
            http: { metadata: { concurrentRequests: '50' } }
          }
        ]
      }
    }
  }
}

For both options, use Azure Database for PostgreSQL Flexible Server as the backend database. Enable private endpoint access and disable public network access.

Setting Up the Software Catalog

The catalog is the heart of Backstage. Without a well-maintained catalog, you have an expensive empty shell.

Catalog Structure

Backstage entities follow a standard YAML format. Every repository should contain a catalog-info.yaml at its root:

YAML
# catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-service
  description: Handles payment processing and transaction management
  annotations:
    dev.azure.com/project-repo: MyOrg/payment-service
    backstage.io/techdocs-ref: dir:.
    sonarqube.org/project-key: payment-service
  tags:
    - dotnet
    - payments
    - tier-1
  links:
    - url: https://dev.azure.com/MyOrg/Payments/_dashboards
      title: Azure DevOps Dashboard
      icon: dashboard
spec:
  type: service
  lifecycle: production
  owner: team-payments
  system: payment-platform
  providesApis:
    - payment-api
  consumesApis:
    - customer-api
    - notification-api
  dependsOn:
    - resource:payment-database
    - resource:payment-servicebus

Auto-Discovery with Azure DevOps

Instead of manually registering every repository, configure the Azure DevOps provider to auto-discover catalog-info.yaml files:

YAML
# app-config.production.yaml
catalog:
  providers:
    azureDevOps:
      yourOrgName:
        organization: https://dev.azure.com/YourOrg
        project: '*'
        repository: '*'
        path: /catalog-info.yaml
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }
  rules:
    - allow:
        - Component
        - System
        - API
        - Resource
        - Group
        - User

This scans all projects and repositories every 30 minutes. New services appear in Backstage automatically once they add a catalog-info.yaml.

Software Templates: Scaffolding That Enforces Standards

Software templates are where Backstage pays for itself. Instead of developers copying an old project and stripping out business logic, they fill in a form and get a new repository that meets all organizational standards from day one.

YAML
# templates/dotnet-microservice/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: dotnet-microservice
  title: .NET Microservice
  description: Creates a new .NET 9 microservice with CI/CD, SonarQube, and infrastructure
  tags:
    - dotnet
    - microservice
    - recommended
spec:
  owner: team-platform
  type: service
  parameters:
    - title: Service Information
      required: [name, description, owner]
      properties:
        name:
          title: Service Name
          type: string
          pattern: '^[a-z][a-z0-9-]*$'
          ui:help: 'Lowercase with hyphens. Example: payment-processor'
        description:
          title: Description
          type: string
          maxLength: 200
        owner:
          title: Owning Team
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
    - title: Technical Options
      properties:
        database:
          title: Database
          type: string
          enum: ['none', 'postgresql', 'cosmos-db']
          default: 'none'
        messaging:
          title: Messaging
          type: string
          enum: ['none', 'servicebus', 'eventhub']
          default: 'none'
  steps:
    - id: fetch
      name: Fetch Skeleton
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          database: ${{ parameters.database }}
          messaging: ${{ parameters.messaging }}
    - id: publish
      name: Create Repository
      action: publish:azure
      input:
        allowedHosts: ['dev.azure.com']
        repoUrl: >-
          dev.azure.com?organization=YourOrg&project=Services&repo=${{ parameters.name }}
        description: ${{ parameters.description }}
    - 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 Backstage
        icon: catalog
        entityRef: ${{ steps['register'].output.entityRef }}

The skeleton directory contains a complete project template with Dockerfile, CI/CD pipeline, Terraform modules, and pre-configured SonarQube analysis. Every new service starts correct.

TechDocs Integration

TechDocs turns Markdown files in your repositories into a searchable documentation site inside Backstage. No separate documentation platform needed.

Configuration for Azure Blob Storage

In production, use the "recommended" deployment where TechDocs are built in CI/CD and published to Azure Blob Storage:

YAML
# app-config.production.yaml
techdocs:
  builder: 'external'
  generator:
    runIn: 'local'
  publisher:
    type: 'azureBlobStorage'
    azureBlobStorage:
      containerName: 'techdocs'
      credentials:
        accountName: ${TECHDOCS_STORAGE_ACCOUNT}
        accountKey: ${TECHDOCS_STORAGE_KEY}

CI/CD Pipeline for TechDocs

Add this to every repository's pipeline to publish docs on merge:

YAML
# azure-pipelines.yml (TechDocs stage)
- stage: PublishDocs
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
    - job: TechDocs
      pool:
        vmImage: 'ubuntu-latest'
      steps:
        - task: UsePythonVersion@0
          inputs:
            versionSpec: '3.11'
        - script: |
            pip install mkdocs-techdocs-core
            npx @techdocs/cli generate --no-docker
            npx @techdocs/cli publish \
              --publisher-type azureBlobStorage \
              --storage-name $(TECHDOCS_STORAGE_ACCOUNT) \
              --azureAccountKey $(TECHDOCS_STORAGE_KEY) \
              --entity default/component/$(SERVICE_NAME)
          displayName: 'Generate and Publish TechDocs'

Authentication with Microsoft Entra ID

Enterprise Backstage must integrate with your identity provider. For Azure environments, that means Entra ID (formerly Azure AD).

YAML
# app-config.production.yaml
auth:
  environment: production
  providers:
    microsoft:
      production:
        clientId: ${AZURE_CLIENT_ID}
        clientSecret: ${AZURE_CLIENT_SECRET}
        tenantId: ${AZURE_TENANT_ID}
        domainHint: yourcompany.com
        additionalScopes:
          - Mail.Read

Configure the sign-in page to use Microsoft as the sole provider:

Typescript
// packages/app/src/App.tsx
import { microsoftAuthApiRef } from '@backstage/core-plugin-api';

const app = createApp({
  // ...
  components: {
    SignInPage: props => (
      <SignInPage
        {...props}
        auto
        provider={{
          id: 'microsoft',
          title: 'Microsoft',
          message: 'Sign in with your corporate account',
          apiRef: microsoftAuthApiRef,
        }}
      />
    ),
  },
});

Map Entra ID groups to Backstage teams using the Microsoft Graph org provider:

YAML
catalog:
  providers:
    microsoftGraphOrg:
      default:
        tenantId: ${AZURE_TENANT_ID}
        clientId: ${AZURE_CLIENT_ID}
        clientSecret: ${AZURE_CLIENT_SECRET}
        groupFilter: startswith(displayName, 'team-')
        userFilter: accountEnabled eq true
        schedule:
          frequency: { hours: 1 }
          timeout: { minutes: 5 }

Custom Plugins: Extending Backstage for Your Needs

Out-of-the-box plugins cover many use cases, but enterprise environments always have custom requirements. Here is the structure of a simple plugin that displays Azure cost data for each service:

Typescript
// plugins/azure-costs/src/components/CostOverview.tsx
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import {
  InfoCard,
  Progress,
  ResponseErrorPanel,
} from '@backstage/core-components';

export const CostOverview = () => {
  const { entity } = useEntity();
  const config = useApi(configApiRef);
  const resourceGroup =
    entity.metadata.annotations?.['azure.com/resource-group'];

  const { data, loading, error } = useCostData(resourceGroup);

  if (loading) return <Progress />;
  if (error) return <ResponseErrorPanel error={error} />;

  return (
    <InfoCard title="Azure Costs (Last 30 Days)">
      <Typography variant="h4">
        {data.currency} {data.totalCost.toFixed(2)}
      </Typography>
      <CostBreakdownChart data={data.breakdown} />
    </InfoCard>
  );
};

Register it as an entity tab so it appears on every service page that has the azure.com/resource-group annotation.

Adoption Strategy: Making Backstage Stick

Loading diagram...

Deploying Backstage is the easy part. Getting 200 developers to actually use it is hard.

Phase 1: Seed the catalog (Weeks 1-4) — Use auto-discovery to populate the catalog with every repository. Even without catalog-info.yaml files, import repositories as unowned components. Visibility alone provides value.

Phase 2: Software templates (Weeks 4-8) — Build templates for your two or three most common project types. Every new service must go through Backstage. This creates a forcing function for adoption.

Phase 3: TechDocs migration (Weeks 8-12) — Migrate documentation from Confluence or SharePoint into TechDocs. Engineers search in Backstage first, find answers, and form the habit.

Phase 4: Custom plugins (Weeks 12-20) — Build plugins that surface data engineers already need: deployment status, cost dashboards, security findings. Make Backstage the single pane of glass.

Phase 5: Deprecate alternatives (Weeks 20+) — Retire standalone dashboards, wikis, and portals that Backstage replaces. If the old tools still exist, developers will use them out of habit.

When Backstage Is Overkill

Backstage is not always the right answer.

Small organizations (fewer than 50 engineers) — The overhead of maintaining a Backstage instance, writing plugins, and keeping the catalog current exceeds the navigation cost it eliminates. A well-organized wiki and a CI/CD dashboard cover most needs.

Single-product teams — If everyone works on the same product with the same tech stack, the "discovery" problem Backstage solves does not exist. You already know what is there.

No platform team — Backstage requires ongoing maintenance. Plugin updates, Backstage version upgrades, catalog curation. Without a dedicated platform team (even 1-2 engineers), Backstage becomes stale and abandoned.

Compliance-heavy environments with no customization budget — Backstage out of the box does not meet most enterprise compliance requirements. Custom auth, audit logging, and access control plugins require development effort.

Production Checklist

Before going live, verify:

  • PostgreSQL uses private endpoints with TLS enforced
  • Backstage runs behind Azure Application Gateway or Front Door with WAF
  • Entra ID authentication is the only sign-in method
  • Container images are scanned and pulled from a private ACR
  • TechDocs storage uses managed identity for access (not storage keys)
  • Catalog auto-discovery runs on schedule and catches new repositories
  • At least three software templates are ready for developer use
  • Monitoring is configured (Application Insights for the Node.js backend)
  • Backup strategy for the PostgreSQL database is tested

Conclusion

Backstage transforms developer experience when it is deployed thoughtfully and maintained actively. On Azure, the combination of AKS or Container Apps, PostgreSQL Flexible Server, Blob Storage for TechDocs, and Entra ID for authentication provides a solid foundation. The real work is in catalog curation, template creation, and driving adoption.

Start with the catalog. Make discovery easy. Then layer on templates, TechDocs, and custom plugins as adoption grows.

If you are evaluating Backstage for your organization or need help deploying it on Azure with enterprise-grade security and governance, reach out at mbrahim@conceptualise.de. We help enterprise teams build internal developer platforms that developers actually use.

Topics

Backstage internal developer portalBackstage Azure deploymentdeveloper portal AKSsoftware catalog BackstageAzure DevOps Backstage plugin

Frequently Asked Questions

Backstage is an open-source developer portal originally created by Spotify. It provides a centralized catalog of all software, APIs, and infrastructure in an organization, along with software templates for scaffolding new services and TechDocs for documentation. Enterprises need it to reduce cognitive load on developers who otherwise have to navigate dozens of tools, wikis, and tribal knowledge to understand what exists and how to create new things.

Expert engagement

Need expert guidance?

Our team specializes in cloud architecture, security, AI platforms, and DevSecOps. Let's discuss how we can help your organization.

Get in touchNo commitment · No sales pressure

Related articles

All posts