---
id: k8s
title: Deploying with Kubernetes
sidebar_label: Kubernetes
description: How to deploy Backstage to a Kubernetes cluster
---

[Kubernetes](https://kubernetes.io/) is a system for deploying, scaling and
managing containerized applications. Backstage is designed to fit this model and
run as a stateless application with an external PostgreSQL database.

There are many different tools and patterns for Kubernetes clusters, so the best
way to deploy to an existing Kubernetes setup is _the same way_ you deploy
everything else.

This guide covers basic Kubernetes definitions needed to get Backstage up and
running in a typical cluster. The object definitions might look familiar, since
the Backstage software catalog
[also uses](../features/software-catalog/descriptor-format.md) the Kubernetes
object format for its entity definition files!

## Testing locally

To test out these concepts locally before deploying to a production Kubernetes
cluster, first install [kubectl](https://kubernetes.io/docs/tasks/tools/), the
Kubernetes command-line tool.

Next, install [minikube](https://minikube.sigs.k8s.io/docs/start/). This creates
a single-node Kubernetes cluster on your local machine:

```shell
# Assumes Mac + Homebrew; see the minikube site for other installations
$ brew install minikube
$ minikube start

...
Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default.
```

Now you can run `kubectl` commands and have changes applied to the minikube
cluster. You should be able to see the `kube-system` Kubernetes pods running:

```shell
$ kubectl get pods -A
```

When you're done with the tutorial, use `minikube stop` to halt the cluster and
free up resources.

## Creating a namespace

Deployments in Kubernetes are commonly assigned to their own
[namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
to isolate services in a multi-tenant environment.

This can be done through `kubectl` directly:

```shell
$ kubectl create namespace backstage
namespace/backstage created
```

Alternatively, create and apply a Namespace definition:

```yaml
# kubernetes/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: backstage
```

```shell
$ kubectl apply -f kubernetes/namespace.yaml
namespace/backstage created
```

## Creating the PostgreSQL database

Backstage in production uses PostgreSQL as a database. To isolate the database
from Backstage app deployments, we can create a separate Kubernetes deployment
for PostgreSQL.

### Creating a PostgreSQL secret

First, create a Kubernetes Secret for the PostgreSQL username and password. This
will be used by both the PostgreSQL database and Backstage deployments:

```yaml
# kubernetes/postgres-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secrets
  namespace: backstage
type: Opaque
data:
  POSTGRES_USER: YmFja3N0YWdl
  POSTGRES_PASSWORD: aHVudGVyMg==
```

The data in Kubernetes secrets are base64-encoded. The values can be generated
on the command line:

```shell
$ echo -n "backstage" | base64
YmFja3N0YWdl
```

:::note Note

Secrets are base64-encoded, but not encrypted. Be sure to enable
[Encryption at Rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/)
for the cluster. For storing secrets in Git, consider
[SealedSecrets or other solutions](https://learnkube.com/kubernetes-secrets-in-git)

:::

The secrets can now be applied to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/postgres-secrets.yaml
secret/postgres-secrets created
```

### Creating a PostgreSQL persistent volume

PostgreSQL needs a persistent volume to store data; we'll create one along with
a `PersistentVolumeClaim`. In this case, we're claiming the whole volume - but
claims can ask for only part of a volume as well.

```yaml
# kubernetes/postgres-storage.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-storage
  namespace: backstage
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 2G
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: '/mnt/data'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-storage-claim
  namespace: backstage
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2G
```

This file contains definitions for two different kinds, separated by a line with
a triple dash. This syntax is helpful if you want to consolidate related
Kubernetes definitions in a single file and apply them at the same time.

Note the volume `type: local`; this creates a volume using local disk on
Kubernetes nodes. More likely in a production scenario, you'd want to use a more
highly available
[types of PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes).

Apply the storage volume and claim to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/postgres-storage.yaml
persistentvolume/postgres-storage created
persistentvolumeclaim/postgres-storage-claim created
```

### Creating a PostgreSQL deployment

Now we can create a Kubernetes Deployment descriptor for the PostgreSQL database
deployment itself:

```yaml
# kubernetes/postgres.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: backstage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:13.2-alpine
          imagePullPolicy: 'IfNotPresent'
          ports:
            - containerPort: 5432
          envFrom:
            - secretRef:
                name: postgres-secrets
          env:
            - name: POSTGRES_HOST
              value: postgres.backstage
            - name: POSTGRES_PORT
              value: '5432'
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: postgresdb
              subPath: data
      volumes:
        - name: postgresdb
          persistentVolumeClaim:
            claimName: postgres-storage-claim
```

If you're not used to Kubernetes, this is a lot to take in. We're describing a
Deployment (one or more instances of an application) that we'd like Kubernetes
to know about in the `metadata` block.

The `spec` block describes the desired state. Here we've requested Kubernetes
create 1 replica (running instance of PostgreSQL), and to create the replica
with the given pod `template`, which again contains Kubernetes metadata and a
desired state. The template `spec` shows one container, created from the
[published](https://hub.docker.com/_/postgres) `postgres:13.2-alpine` Docker
image.

Note the `envFrom` and `secretRef` - this tells Kubernetes to fill environment
variables in the container with values from the Secret we created. We've also
referenced the volume created for the deployment, and given it the mount path
expected by PostgreSQL.

Apply the PostgreSQL deployment to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/postgres.yaml
deployment.apps/postgres created

$ kubectl get pods --namespace=backstage
NAME                        READY   STATUS    RESTARTS   AGE
postgres-56c86b8bbc-66pt2   1/1     Running   0          21s
```

Verify the deployment by connecting to the pod:

```shell
$ kubectl exec -it --namespace=backstage postgres-56c86b8bbc-66pt2 -- /bin/bash
bash-5.1# psql -U $POSTGRES_USER
psql (13.2)
backstage=# \q
bash-5.1# exit
```

### Creating a PostgreSQL service

The database pod is running, but how does another pod connect to it?

Kubernetes pods are transient - they can be stopped, restarted, or created
dynamically. Therefore we don't want to try to connect to pods directly, but
rather create a Kubernetes Service. Services keep track of pods and direct
traffic to the right place.

The final step for our database is to create the service descriptor:

```yaml
# kubernetes/postgres-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: backstage
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
```

Apply the service to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/postgres-service.yaml
service/postgres created

$ kubectl get services --namespace=backstage
NAME       TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
postgres   ClusterIP   10.96.5.103   <none>        5432/TCP   29s
```

## Creating the Backstage instance

Now that we have PostgreSQL up and ready to store data, we can create the
Backstage instance. This follows similar steps as the PostgreSQL deployment.

### Creating a Backstage secret

For any Backstage configuration secrets, such as authorization tokens, we can
create a similar Kubernetes Secret as we did
[for PostgreSQL](#creating-a-postgresql-secret), remembering to base64 encode
the values:

```yaml
# kubernetes/backstage-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: backstage-secrets
  namespace: backstage
type: Opaque
data:
  GITHUB_TOKEN: VG9rZW5Ub2tlblRva2VuVG9rZW5NYWxrb3ZpY2hUb2tlbg==
```

Apply the secret to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/backstage-secrets.yaml
secret/backstage-secrets created
```

### Creating a Backstage deployment

To create the Backstage deployment, first create a [Docker image](docker.md).
We'll use this image to create a Kubernetes deployment. For this example, we'll
use the standard host build with the frontend bundled and served from the
backend.

First, create a Kubernetes Deployment descriptor:

```yaml
# kubernetes/backstage.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backstage
  namespace: backstage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backstage
  template:
    metadata:
      labels:
        app: backstage
    spec:
      containers:
        - name: backstage
          image: backstage:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 7007
          envFrom:
            - secretRef:
                name: postgres-secrets
            - secretRef:
                name: backstage-secrets
# Uncomment if health checks are enabled in your app:
# https://backstage.io/docs/plugins/observability#health-checks
#          readinessProbe:
#            httpGet:
#              port: 7007
#              path: /healthcheck
#          livenessProbe:
#            httpGet:
#              port: 7007
#              path: /healthcheck
```

For production deployments, the `image` reference will usually be a full URL to
a repository on a container registry (for example, ECR on AWS).

For testing locally with `minikube`, you can point the local Docker daemon to
the `minikube` internal Docker registry and then rebuild the image to install
it:

```shell
$ eval $(minikube docker-env)
$ yarn build-image --tag backstage:1.0.0
```

There is no special wiring needed to access the PostgreSQL service. Since it's
running on the same cluster, Kubernetes will inject `POSTGRES_HOST` and
`POSTGRES_PORT` environment variables into our Backstage container.
These can be used in the Backstage `app-config.yaml` along with the secrets. Apply this to `app-config.production.yaml` as well if you have one:

```yaml
backend:
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
```

Make sure to rebuild the Docker image after applying `app-config.yaml` changes.

Apply this Deployment to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/backstage.yaml
deployment.apps/backstage created

$ kubectl get deployments --namespace=backstage
NAME        READY   UP-TO-DATE   AVAILABLE   AGE
backstage   1/1     1            1           1m
postgres    1/1     1            1           10m

$ kubectl get pods --namespace=backstage
NAME                                 READY   STATUS    RESTARTS   AGE
backstage-54bfcd6476-n2jkm           1/1     Running   0          58s
postgres-56c86b8bbc-66pt2            1/1     Running   0          9m
```

Beautiful! 🎉 The deployment and pod are running in the cluster. If you run into
any trouble, check the container logs from the pod:

```shell
# -f to tail, <pod> -c <container>
$ kubectl logs --namespace=backstage -f backstage-54bfcd6476-n2jkm -c backstage
```

### Creating a Backstage service

Like the [PostgreSQL service](#creating-a-postgresql-service) above, we need to
create a Kubernetes Service for Backstage to handle connecting requests to the
correct pods.

Create the Kubernetes Service descriptor:

```yaml
# kubernetes/backstage-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: backstage
  namespace: backstage
spec:
  selector:
    app: backstage
  ports:
    - name: http
      port: 80
      targetPort: http
```

The `selector` here is telling the Service which pods to target, and the port
mapping translates normal HTTP port 80 to the backend http port (7007) on the
pod.

Apply this Service to the Kubernetes cluster:

```shell
$ kubectl apply -f kubernetes/backstage-service.yaml
service/backstage created
```

Now we have a fully operational Backstage deployment! 🎉 For a _**grand
reveal**_, you can forward a local port to the service:

```shell
$ sudo kubectl port-forward --namespace=backstage svc/backstage 80:80
Forwarding from 127.0.0.1:80 -> 7007
```

This shows port 7007 since `port-forward` doesn't _really_ support services, so
it cheats by looking up the first pod for a service and connecting to the mapped
pod port.

Note that `app.baseUrl` and `backend.baseUrl` in your `app-config.yaml` should
match what we're forwarding here (port omitted in this example since we're using
the default HTTP port 80):

```yaml
# app-config.yaml
app:
  baseUrl: http://localhost

organization:
  name: Spotify

backend:
  baseUrl: http://localhost
  listen:
    port: 7007
  cors:
    origin: http://localhost
```

If you're using an [auth provider](../auth/index.md), it should also have this
address configured for the authentication pop-up to work properly.

Now you can open a browser on your machine to [localhost](http://localhost) and
browse your Kubernetes-deployed Backstage instance. 🚢🚢🚢

## Further steps

This is most of the way to a full production deployment of Backstage on
Kubernetes. There's a few additional steps to that will likely be needed beyond
the scope of this document.

### Set up a more reliable volume

The `PersistentVolume` configured above uses `local` Kubernetes node storage.
This should be replaced with a cloud volume, network attached storage, or
something more persistent beyond a Kubernetes node.

### Expose the Backstage service

The Kubernetes Service is not exposed for external connections from outside the
cluster. This is generally done with a Kubernetes
[ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) or
an
[external load balancer](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/).

### Update the Deployment image

To update the Kubernetes deployment to a newly published version of your
Backstage Docker image, update the image tag reference in `backstage.yaml` and
then apply the changes with `kubectl apply -f kubernetes/backstage.yaml`.

For production purposes, this image tag will generally be a full-fledged URL
pointing to a container registry where built Docker images are hosted. This can
be hosted internally in your infrastructure, or a managed one offered by a cloud
provider.
