PlatformEngr® Backstage course

Module 09 · Ship it · 60 min · You are: Platform team

Ship it: an image, production config and an upgrade

In this module

  • You learn: How the backend serves the built frontend, what goes into the image, how production config layers on top, and how upgrades work.
  • You do: Change the Dockerfile, write app-config.production.yaml, build and run the image next to Postgres, then bump Backstage to 1.56.3 and rebuild.
  • You end with: A backstage image that runs the whole portal on one port, with your catalog, template and docs, on Backstage 1.56.3.

yarn start is two dev servers and hot reload. Production is one Node process that serves the built frontend and the API, configured by environment variables. Anything the portal reads from disk (catalog files, templates) must be in the image, and anything it runs (MkDocs) too. Upgrades are part of shipping: Backstage releases monthly, and small steps are cheap.

Open this module in the interactive course to track progress.

Concepts

Host build
yarn build:backend builds the frontend and backend on your machine; the Dockerfile only copies the result in.
Config layering
The container starts with --config app-config.yaml --config app-config.production.yaml. Objects merge; lists, like catalog.locations, are replaced.
Release
A Backstage release is a set of package versions. versions:bump --release moves every @backstage/* package to it together.

Files to know

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Put MkDocs and your files in the image

Stop the dev portal (Ctrl+C): the container uses port 7007 too.

Change in ~/work/portal/packages/backend/Dockerfile

-     apt-get install -y --no-install-recommends libsqlite3-dev && \
-     rm -rf /var/lib/apt/lists/*
+     apt-get install -y --no-install-recommends libsqlite3-dev python3 python3-venv && \
+     rm -rf /var/lib/apt/lists/*
+ 
+ # TechDocs builds docs with MkDocs inside the container (runIn: local).
+ RUN python3 -m venv /opt/mkdocs && \
+     /opt/mkdocs/bin/pip install --no-cache-dir mkdocs-techdocs-core==1.7.1
+ ENV PATH="/opt/mkdocs/bin:$PATH"

Change in ~/work/portal/packages/backend/Dockerfile

- # This will include the examples, if you don't need these simply remove this line
- COPY --chown=node:node examples ./examples
+ COPY --chown=node:node catalog ./catalog
+ COPY --chown=node:node templates ./templates

Nothing reads the example data any more:

cd ~/work/portal && git rm -rq examples

2. Write the production config

Replace the whole file ~/work/portal/app-config.production.yaml

# Loaded on top of app-config.yaml inside the container (see the Dockerfile CMD).
# Only what differs from your laptop goes here.
app:
  # The backend serves the frontend in production, so both use one URL.
  baseUrl: http://localhost:7007

backend:
  baseUrl: http://localhost:7007
  listen: ':7007'
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}

auth:
  environment: production
  providers:
    github:
      production:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
        signIn:
          resolvers:
            - resolver: usernameMatchingUserEntityName

techdocs:
  generator:
    # No Docker inside the container: mkdocs is installed in the image instead.
    runIn: local

catalog:
  # Lists replace, they do not merge, so every location is repeated here with
  # paths relative to /app in the image.
  locations:
    - type: file
      target: ./catalog/org.yaml
      rules:
        - allow: [Group, User]
    - type: file
      target: ./catalog/systems.yaml
      rules:
        - allow: [Domain, System]
    - type: file
      target: ./templates/node-service/template.yaml
      rules:
        - allow: [Template]

3. Build

Give Docker at least 4 GB of memory: the build runs out with 2 GB.

cd ~/work/portal && yarn install --immutable && yarn tsc && yarn build:backend

Build the image. It is tagged backstage:

yarn build-image

4. Run it next to Postgres

A fresh database on its own network, so you see only what is in the image and in Git.

docker network create portal-net && docker run -d --name portal-prod-db --network portal-net -e POSTGRES_PASSWORD=portal-prod postgres:17.11
cd ~/work/portal && set -a && . ./.env && set +a

Start the portal in the foreground. The variables without = are passed from your shell:

docker run --rm --name portal --network portal-net -p 7007:7007 \
  -e POSTGRES_HOST=portal-prod-db -e POSTGRES_PORT=5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=portal-prod \
  -e PORTAL_API_TOKEN -e GITHUB_TOKEN -e AUTH_GITHUB_CLIENT_ID -e AUTH_GITHUB_CLIENT_SECRET \
  backstage

A warning that backend.baseUrl points at localhost in production is expected here. 1. Open http://localhost:7007 (one port now) and sign in with GitHub. 2. The catalog has your teams, checkout, cart and portal, and the template. 3. checkout → TechDocs builds with MkDocs inside the container.

In your second terminal (with .env loaded). Expect cart, checkout and portal:

curl -s -H "Authorization: Bearer $PORTAL_API_TOKEN" "localhost:7007/api/catalog/entities?filter=kind=component" | jq -r '.[].metadata.name'

5. Upgrade Backstage

Stop the container (Ctrl+C). Bump every Backstage package to one release, then build and test as before.

cd ~/work/portal && yarn backstage-cli versions:bump --release 1.56.3

Expect 1.56.3:

jq -r .version backstage.json

The upgrade helper (link above) shows changes to files create-app generated, such as the Dockerfile; apply any that matter. Then build, test and run the image again:

yarn install && yarn tsc && yarn build:backend && yarn build-image

Backstage upgrade helper ↗

6. Ship it

cd ~/work/portal && git status --short

Propose it as a pull request. Put it on its own branch and push it:

git switch -c ship-image && git add -A && git commit -qm "Ship: production image and Backstage 1.56.3"
git push -qu origin HEAD

Open the pull request. It prints a link:

gh pr create --fill

Open that link in your browser. Files changed shows exactly your diff.

Merge it on GitHub (Squash and merge, then Delete branch), or from the terminal:

gh pr merge --squash --delete-branch

Either way, bring your copy up to date:

git switch -q main && git pull -q

Simplified for the course

The image runs on your laptop with localhost URLs. In production it runs on Kubernetes (or a container service) behind TLS with a real hostname, Postgres is managed, secrets come from a secret store, and CI builds and pushes the image on every merge. The Argo CD course shows how to deploy it from Git.

Break it on purpose: A slimmer image

Someone trims the Dockerfile and removes a COPY line that "only copies YAML". The build passes and the container starts.

You practise: reading a production startup, and connecting a sign-in failure to a file missing from the image.

1. Break it

Delete from ~/work/portal/packages/backend/Dockerfile

- COPY --chown=node:node catalog ./catalog
cd ~/work/portal && yarn build-image

Start with a fresh database, like a new environment would:

docker rm -f portal-prod-db && docker run -d --name portal-prod-db --network portal-net -e POSTGRES_PASSWORD=portal-prod postgres:17.11
cd ~/work/portal && set -a && . ./.env && set +a
docker run --rm --name portal --network portal-net -p 7007:7007 \
  -e POSTGRES_HOST=portal-prod-db -e POSTGRES_PORT=5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=portal-prod \
  -e PORTAL_API_TOKEN -e GITHUB_TOKEN -e AUTH_GITHUB_CLIENT_ID -e AUTH_GITHUB_CLIENT_SECRET \
  backstage

2. What you see

Expected: you sign in. You get: Failed to sign-in, unable to resolve user identity. The catalog has checkout and cart (from discovery) but no teams.

3. Fix it yourself

Find out why the teams are missing in this environment, fix the image, and prove the files are inside it.

Hint 1

Search the container log for org.yaml.

Hint 2

The location is ./catalog/org.yaml, relative to /app in the container. What does the Dockerfile copy into /app?

The fix

The log says file /app/catalog/org.yaml does not exist. Put the line back:

Add to ~/work/portal/packages/backend/Dockerfile before COPY --chown=node:node templates ./templates

COPY --chown=node:node catalog ./catalog
cd ~/work/portal && yarn build-image

Start the container again (same docker run). In the second terminal, expect org.yaml and systems.yaml:

docker exec portal ls /app/catalog
After you fixed it: the five questions
What failed?
The image had no catalog/ folder, so the platform's teams and users never loaded and sign-in found no User.
How was it detected?
By sign-in failing, and a warning in the startup log. An existing database would have hidden it: entities stay in Postgres.
Who responds?
The platform team: they own the image.
How is it corrected?
Copy the folder into the image again and redeploy.
How is it prevented?
A smoke test in CI that starts the image with an empty database and checks the catalog has Users and Groups.

Quiz

  1. You list catalog.locations in app-config.production.yaml. What happens to the ones in app-config.yaml?
    • They are replaced: the production list is the only list
    • Both lists are merged
    • The base file wins for lists
  2. Why did the broken image work fine against the old database?
    • Postgres caches files from the image
    • Entities stay in the database; nothing deleted them, so only a fresh database showed the gap
    • The location warning is only shown with a fresh database
  3. How should you upgrade Backstage?
    • Edit each @backstage/* version by hand
    • Recreate the app with the newest create-app
    • Run versions:bump regularly, check the upgrade helper, then build and test
Answers
  1. They are replaced: the production list is the only list Lists replace. That is why the production file repeats every location.
  2. Entities stay in the database; nothing deleted them, so only a fresh database showed the gap Test images against an empty database.
  3. Run versions:bump regularly, check the upgrade helper, then build and test Small, frequent steps through the release are cheap; big jumps are not.

Field note

The best Backstage teams I know upgrade every month on a fixed day. The worst ones are a year behind and need a project to catch up.

Report a problem with this module