Migrate Backstage Docker Deployment from SQLite to PostgreSQL

This guide shows how to run a production-style Backstage container that uses PostgreSQL (instead of SQLite), with secrets provided through a single .env file.

Background

This post is a follow-up to my previous guide:

That post establishes the baseline Docker workflow:

  • Build a single Docker image (backstage:local)
  • Inject secrets via --env-file .env
  • (Optionally) persist SQLite on the host at /var/lib/backstage/db

In this post, the Docker workflow stays the same, but we replace the database layer:

  • SQLite → PostgreSQL

What you will have at the end

  • A local Docker image backstage:local
  • Backstage listening on :7007
  • Backstage storing data in PostgreSQL
  • A private .env (gitignored)

Important note about “migration”: Backstage doesn’t provide a universal one-click “SQLite → Postgres” migration for every plugin’s data. In most homelab setups, the catalog is re-ingested (GitHub/Gerrit/LDAP providers), so you can switch DBs without preserving old SQLite content. If you have plugin-specific data you must keep, treat this as a database cutover and plan plugin-by-plugin migration separately.


Prerequisites

  • Ubuntu 24.04
  • Docker installed
  • A working Backstage monorepo (example: ~/homelab-backstage)
  • PostgreSQL reachable from the machine that runs Docker
  • Your Backstage config files exist in the repo:
    • app-config.yaml
    • app-config.production.yaml

  1. Create the Backstage database and user

On the PostgreSQL server:

sudo -u postgres psql

Run:

CREATE ROLE backstage WITH LOGIN PASSWORD 'change_me_now';
CREATE DATABASE backstage OWNER backstage;

  1. Switch Backstage config from SQLite to PostgreSQL

Because my container loads config in this order:

  • app-config.yaml
  • app-config.production.yaml

…the cleanest approach is:

  • Keep SQLite in app-config.yaml (optional, for local dev)
  • Override to PostgreSQL in app-config.production.yaml (recommended for Docker/prod)

Update app-config.production.yaml

Add (or replace) this section:

backend:
  database:
    client: pg
    pluginDivisionMode: schema
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      database: ${POSTGRES_DB}

Why pluginDivisionMode: schema?

Backstage supports two common ways to separate plugin tables:

  • pluginDivisionMode: schema (recommended for homelab / Docker):
    One PostgreSQL database, but each plugin gets its own schema (e.g., catalog, auth, scaffolder). This avoids table-name collisions while keeping DB management simple.
  • Default (if you remove pluginDivisionMode):
    Backstage typically falls back to database-per-plugin, where each plugin uses its own database. This may require extra PostgreSQL privileges (often the ability to create databases), which many setups don’t want to grant.

If your goal is “single database”, keep pluginDivisionMode: schema.


  1. Add PostgreSQL environment variables to .env

Append these variables to the existing .env file used by Docker --env-file:

vi .env

Add:

POSTGRES_HOST=192.168.0.102
POSTGRES_PORT=5432
POSTGRES_USER=backstage
POSTGRES_PASSWORD=change_me_now
POSTGRES_DB=backstage

Security tip: Keep .env out of Git. If publishing the repo, provide a .env.sample / .env.example instead.


  1. Build the Backstage Docker image
yarn install --immutable
yarn tsc
yarn build:backend
docker build -f packages/backend/Dockerfile -t backstage:local .

  1. Run Backstage using PostgreSQL

Start Backstage with the .env containing PostgreSQL variables:

docker run --rm \
  -p 7007:7007 \
  --env-file .env \
  backstage:local

On first startup with PostgreSQL, logs typically show database migrations and then readiness becomes healthy.

Did this guide save you time?

Support this site

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top