Backstage is Spotify's open-source developer portal, now a CNCF incubating project adopted by Spotify, Lyft, American Airlines, and hundreds of engineering organizations worldwide. It centralises your software catalog, service docs, scaffolded templates, and TechDocs into a single pane of glass for your entire engineering team. When Backstage goes down, engineers lose access to the software catalog, internal tooling links, and on-call runbooks — and new services can't be scaffolded until the portal recovers.
In this tutorial you'll set up end-to-end monitoring for a self-hosted Backstage deployment using Vigilmon — free tier, no credit card required.
Why Backstage needs external monitoring
Backstage is a Node.js application backed by a PostgreSQL database and optionally several third-party integrations (GitHub, PagerDuty, Kubernetes). A typical production deployment involves:
- Backstage app server — the main Node.js process serving the React frontend and backend APIs on port 7007
- PostgreSQL — stores catalog entities, user sessions, and scaffolder task history
- Catalog ingestion workers — periodically sync entities from GitHub, GitLab, or Bitbucket
- Optional TechDocs builder — generates and serves MkDocs-based documentation
Failure modes that internal health checks cannot catch:
- App server crashes silently — the process exits but the container is marked "running" because the restart policy brings it back; during restart, all API requests return 502
- PostgreSQL connection pool exhaustion — the app is up but the database connection pool is full; entity queries fail with timeout errors
- Catalog sync failure — the GitHub integration's token expires; services disappear from the catalog without any visible error on the homepage
- Memory leak under heavy load — the Node.js heap grows until OOM kill, causing intermittent restarts during peak usage
- Reverse proxy misconfiguration — a TLS renewal or nginx reload leaves the port temporarily unreachable even though the backend is healthy
An external monitor probing Backstage from outside your network is the only reliable way to detect all of these before your engineers report them via Slack.
What you'll need
- A running Backstage deployment (Docker Compose, Kubernetes, or bare metal)
- A free Vigilmon account — takes 30 seconds to create
Step 1: Verify Backstage health endpoints
Backstage ships with a built-in health endpoint provided by the @backstage/backend-defaults package. It's available at:
GET http://<your-host>:7007/healthcheck
A healthy Backstage instance returns:
{"status":"ok"}
Test it from outside your network first:
curl -f https://backstage.yourcompany.com/healthcheck
# Expected: {"status":"ok"}
If the health endpoint returns 404, you may be running an older Backstage version. Add the health check manually in your packages/backend/src/index.ts:
import { createServiceBuilder } from '@backstage/backend-common';
import Router from 'express-promise-router';
const router = Router();
router.get('/healthcheck', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
For Backstage deployed behind a reverse proxy, make sure /healthcheck is proxied through to the backend port:
server {
listen 443 ssl;
server_name backstage.yourcompany.com;
ssl_certificate /etc/letsencrypt/live/backstage.yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/backstage.yourcompany.com/privkey.pem;
location / {
proxy_pass http://localhost:7007;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for Backstage WebSockets (TechDocs live reload)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Step 2: Add a deep health check for catalog and database
The default /healthcheck only tells you the HTTP server is accepting connections. Add a deeper check that verifies the database connection and at minimum one catalog query:
// packages/backend/src/plugins/healthcheck.ts
import { Router } from 'express';
import { DatabaseManager } from '@backstage/backend-common';
export async function createHealthRouter(options: {
database: DatabaseManager;
}): Promise<Router> {
const router = Router();
router.get('/health/deep', async (_req, res) => {
try {
// Verify database connectivity
const client = await options.database.forPlugin('catalog').getClient();
await client.raw('SELECT 1');
res.json({
status: 'ok',
database: 'connected',
timestamp: new Date().toISOString(),
});
} catch (err) {
res.status(503).json({
status: 'error',
database: 'unreachable',
error: err instanceof Error ? err.message : 'unknown',
});
}
});
return router;
}
Register the router in packages/backend/src/index.ts:
import { createHealthRouter } from './plugins/healthcheck';
const health = await createHealthRouter({ database });
apiRouter.use(health);
Now Vigilmon can monitor both the shallow endpoint (process alive) and the deep endpoint (database alive) independently, so you know exactly which layer failed when an alert fires.
Step 3: Set up HTTP monitoring in Vigilmon
With your health endpoints exposed, add them to Vigilmon:
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS as the type
- Set the URL to
https://backstage.yourcompany.com/healthcheck - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok"
- Status code:
- Save the monitor
Repeat for the deep health endpoint:
- Create a second monitor for
https://backstage.yourcompany.com/health/deep - Set expected status:
200 - Set response body:
"database":"connected"
If the shallow check passes but the deep check fails, you know Backstage is responding but the database is down — a much faster diagnosis than tailing logs.
Step 4: Monitor PostgreSQL via TCP
Backstage's PostgreSQL database can fail independently. Add a TCP port monitor to catch database-level outages:
- In Vigilmon, go to Monitors → New Monitor
- Choose TCP Port as the type
- Enter your database host and port:
db.yourcompany.com/5432 - Save the monitor
If your PostgreSQL is running in a private network (recommended), you can monitor it via the same host that Backstage runs on by adding a lightweight TCP proxy or exposing a dedicated monitoring port. Alternatively, monitor the deep health endpoint — when PostgreSQL is unreachable, it will return HTTP 503.
For Kubernetes deployments, monitor the Service ClusterIP via a TCP check from within the cluster or expose the port through your ingress:
# kubernetes/postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
name: backstage-postgres
namespace: backstage
spec:
selector:
app: postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432
type: ClusterIP
Step 5: Configure alerts and a status page
Set up alert channels so your team is notified immediately when Backstage goes down:
Webhook alert (Slack or Discord):
- Go to Alert Channels → Add Channel → Webhook
- Paste your Slack webhook URL
- Assign the channel to all Backstage monitors
Example Slack alert payload Vigilmon sends on an incident:
{
"monitor_name": "Backstage /healthcheck",
"status": "down",
"url": "https://backstage.yourcompany.com/healthcheck",
"started_at": "2024-03-15T09:12:00Z",
"duration_seconds": 120
}
Status page for your engineering team:
- Go to Status Pages → New Status Page
- Name it "Developer Portal Status"
- Add your Backstage monitors (shallow health, deep health, PostgreSQL TCP)
- Publish the page
Share the status page URL in your internal engineering handbook and Slack channel so engineers can check portal status without paging on-call. The page automatically shows uptime history and active incidents.
Putting it all together: Docker Compose example
Here's a complete docker-compose.yml for a production Backstage deployment with health checks that align with Vigilmon's monitoring:
version: "3.9"
services:
backstage:
image: your-registry/backstage:latest
build:
context: .
dockerfile: packages/backend/Dockerfile
ports:
- "7007:7007"
environment:
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_USER: backstage
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
GITHUB_TOKEN: ${GITHUB_TOKEN}
AUTH_GITHUB_CLIENT_ID: ${AUTH_GITHUB_CLIENT_ID}
AUTH_GITHUB_CLIENT_SECRET: ${AUTH_GITHUB_CLIENT_SECRET}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7007/healthcheck"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: backstage_plugin_catalog
POSTGRES_USER: backstage
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U backstage"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
backstage:
condition: service_healthy
restart: unless-stopped
volumes:
postgres_data:
Monitoring checklist
| What to monitor | Type | URL / Host | Alert on |
|---|---|---|---|
| Backstage app | HTTP | /healthcheck | Non-200 or timeout |
| Backstage + DB | HTTP | /health/deep | Non-200 or database≠connected |
| PostgreSQL | TCP | db-host:5432 | Connection refused |
| Nginx / TLS | HTTP | / | Non-200 or cert expiry |
Summary
Backstage is a critical piece of developer infrastructure — when it's down, engineers lose their catalog, docs, and scaffolding tools. A three-layer monitoring setup (shallow HTTP health, deep database health, and TCP port check for PostgreSQL) gives you precise failure attribution in seconds. With Vigilmon's free tier you can add all three monitors, configure Slack alerts, and publish a status page in under five minutes.
Start monitoring your developer portal for free at vigilmon.online.