Frontend & Backend

How to deploy a containerized Frontend application talking to a containerized Backend application exposed via a DNS with score-compose and score-k8s

Overview

In this example we will walk you through how you can deploy a containerized Frontend application talking to a Backend application, and this with both score-compose and score-k8s.

  flowchart TD
    dns[DNS] --> frontend-workload(Frontend)
    dns[DNS] --> backend-workload(Backend)
    subgraph Workloads
        frontend-workload
        backend-workload
    end
    frontend-workload -.-> dns
    dns -.-> backend-workload
    backend-workload-->postgres[(PostgreSQL)]

Score files

We will describe each containerized application with its own Score file for each. The demo code can be found here.

Open your IDE and paste in the following score-frontend.yaml file, which describes the containerized Nginx Frontend application exposed via a DNS that contact a Backend application on each request.

apiVersion: score.dev/v1b1
metadata:
  name: frontend
containers:
  frontend:
    image: .
    variables:
      APP_CONFIG_app_baseUrl: ${resources.dns.url}
      APP_CONFIG_backend_baseUrl: ${resources.dns.url}
    livenessProbe:
      httpGet:
        path: /healthcheck
        port: 8080
    readinessProbe:
      httpGet:
        path: /healthcheck
        port: 8080
service:
  ports:
    tcp:
      port: 3000
      targetPort: 8080
resources:
  backend:
    type: service
  dns:
    type: dns
    id: dns
  route:
    type: route
    params:
      host: ${resources.dns.host}
      path: /
      port: 3000

The following score-backend.yaml file describes the containerized the NodeJS Backend application talking to a PostgreSQL database:

apiVersion: score.dev/v1b1
metadata:
  name: backend
containers:
  backend:
    image: .
    command:
      - "node"
    args:
      - packages/backend
      - "--config"
      - app-config.yaml
      - "--config"
      - app-config.production.yaml
    variables:
      POSTGRES_HOST: ${resources.pg.host}
      POSTGRES_PASSWORD: ${resources.pg.password}
      POSTGRES_PORT: ${resources.pg.port}
      POSTGRES_USER: ${resources.pg.username}
      APP_CONFIG_auth_providers_guest_dangerouslyAllowOutsideDevelopment: "true"
      APP_CONFIG_backend_cors_origin: ${resources.dns.url}
      APP_CONFIG_techRadar_url: https://github.com/mathieu-benoit/humanitec-ref-arch/blob/main/tech-radar.json
    livenessProbe:
      httpGet:
        path: /.backstage/health/v1/liveness
        port: 7007
    readinessProbe:
      httpGet:
        path: /.backstage/health/v1/readiness
        port: 7007
service:
  ports:
    tcp:
      port: 7007
      targetPort: 7007
resources:
  pg:
    type: postgres-instance
  dns:
    type: dns
    id: dns
  route:
    type: route
    params:
      host: ${resources.dns.host}
      path: /api
      port: 7007

The id: dns part under the dns resources in both Score files allow to generate a shared dns between both frontend and backend Workloads.

Deployment with score-compose and score-k8s

From here, we will now see how to deploy these exact same Score files with either with score-compose or with score-k8s:

To begin, follow the installation instructions to install the latest version of score-compose.

init

Initialize your current score-compose workspace, run the following command in your terminal:

score-compose init --no-sample \
    --provisioners https://raw.githubusercontent.com/score-spec/community-provisioners/refs/heads/main/service/score-compose/10-service.provisioners.yaml \
    --provisioners https://raw.githubusercontent.com/score-spec/community-provisioners/refs/heads/main/dns/score-compose/10-dns-with-url.provisioners.yaml

The init command will create the .score-compose directory with the default resource provisioners available. We are also importing two external files to support the dns dependencies: dns provisioner and service dependencies: service provisioner.

You can see the resource provisioners available by running this command:

score-compose provisioners list

The Score file example illustrated uses three resource types: postgres-instance, dns and route.

+-------------------+-------+------------------+--------------------------------+---------------------------------+
|     TYPE          | CLASS |      PARAMS      |            OUTPUTS             |          DESCRIPTION            |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| dns               | (any) |                  | host, url                      | Outputs a *.localhost domain as |
|                   |       |                  |                                | the hostname and associated URL |
|                   |       |                  |                                | in http on port 8080            |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| postgres-instance | (any) |                  | host, password, port, username | Provisions a dedicated          |
|                   |       |                  |                                | PostgreSQL instance             |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| route             | (any) | host, path, port |                                | Provisions an Ingress route on  |
|                   |       |                  |                                | a shared Nginx instance         |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| service           | (any) |                  | name                           | Outputs the name of the         |
|                   |       |                  |                                | Workload dependency if it       |
|                   |       |                  |                                | exists in the list of Workloads |
+-------------------+-------+------------------+--------------------------------+---------------------------------+

generate

Convert the score-frontend.yaml and score-backend.yaml files into a deployable compose.yaml, run the following command in your terminal:

score-compose generate score-backend.yaml \
    --image ghcr.io/mathieu-benoit/backstage-backend:latest

score-compose generate score-frontend.yaml \
    --image ghcr.io/mathieu-benoit/backstage-frontend:latest

The generate command will add the input score-frontend.yaml and score-backend.yaml workloads with a particular container image to the .score-compose/state.yaml state file and generate the output compose.yaml.

If you want to build the container image when this compose.yaml will be deployed, you can run this generate command with the --build parameter instead:

score-compose generate score-backend.yaml --build 'main={"context":".","tags":["backend:local"]}'

See the generated compose.yaml by running this command:

cat compose.yaml

If you make any modifications to the score-frontend.yaml or score-backend.yaml files, run score-compose generate score.yaml to regenerate the output compose.yaml.

resources

Get the information of the resources dependencies of the workload, run the following command:

score-compose resources list
+--------------------------------------+--------------------------------+
|                 UID                  |            OUTPUTS             |
+--------------------------------------+--------------------------------+
| dns.default#dns                      | host, url                      |
+--------------------------------------+--------------------------------+
| postgres-instance.default#backend.pg | host, password, port, username |
+--------------------------------------+--------------------------------+
| service.default#frontend.backend     | name                           |
+--------------------------------------+--------------------------------+
| route.default#backend.route          |                                |
+--------------------------------------+--------------------------------+
| route.default#frontend.route         |                                |
+--------------------------------------+--------------------------------+

At this stage, we can already see the value of the dns resource generated:

score-compose resources get-outputs dns.default#dns
{
  "host": "dnsjdtv57.localhost",
  "url": "http://dnsjdtv57.localhost:8080"
}

Same for the postgres-instance resource:

score-compose resources get-outputs postgres-instance.default#backend.pg
{
  "host": "pg-OuoTNo",
  "password": "REDACTED",
  "port": 5432,
  "username": "REDACTED"
}

docker compose

Run docker compose up to execute the generated compose.yaml file:

docker compose up -d
[+] Running 5/5
 ✔ Container deploy-backstage-with-score-routing-5QD6z8-1      Running
 ✔ Container deploy-backstage-with-score-pg-glZvLw-1           Healthy
 ✔ Container deploy-backstage-with-score-wait-for-resources-1  Exited
 ✔ Container deploy-backstage-with-score-backend-backend-1     Running
 ✔ Container deploy-backstage-with-score-frontend-frontend-1   Running

docker ps

See the running containers:

docker ps
CONTAINER ID   IMAGE                                              COMMAND                  CREATED          STATUS                   PORTS                                                 NAMES
1712a2002838   ghcr.io/mathieu-benoit/backstage-frontend:latest   "/docker-entrypoint.…"   3 minutes ago    Up 2 minutes             80/tcp                                                deploy-backstage-with-score-frontend-frontend-1
6bf734ab9179   ghcr.io/mathieu-benoit/backstage-backend:latest    "node packages/backe…"   3 minutes ago    Up 2 minutes                                                                   -backend-backend-1
9a126ed0456c   mirror.gcr.io/nginx:1-alpine                       "/docker-entrypoint.…"   3 minutes ago    Up 2 minutes             0.0.0.0:8080->80/tcp, [::]:8080->80/tcp               deploy-backstage-with-score-routing-5QD6z8-1
5ec1732b6695   mirror.gcr.io/postgres:17-alpine                   "docker-entrypoint.s…"   3 minutes ago    Up 3 minutes (healthy)   5432/tcp                                              -pg-glZvLw-1

curl localhost:8080

Test the running container, run the following command:

curl localhost:8080 -H "Host: dnsjdtv57.localhost"
...
<title>Scaffolded Backstage App</title>
...

Congrats! You’ve successfully deploy, with the score-compose implementation, a containerized Frontend application talking to a containerized Backend application exposed via a DNS. You provisioned them through Docker, without writing the Docker Compose file by yourself.

To begin, follow the installation instructions to install the latest version of score-k8s.

init

Initialize your current score-k8s workspace, run the following command in your terminal:

score-k8s init --no-sample \
    --provisioners https://raw.githubusercontent.com/score-spec/community-provisioners/refs/heads/main/service/score-k8s/10-service.provisioners.yaml \
    --provisioners https://raw.githubusercontent.com/score-spec/community-provisioners/refs/heads/main/dns/score-k8s/10-dns-with-url.provisioners.yaml

The init command will create the .score-k8s directory with the default resource provisioners available. We are also importing two external files to support the dns dependencies: dns provisioner and service dependencies: service provisioner.

You can see the resource provisioners available by running this command:

score-k8s provisioners list

The Score file example illustrated uses three resource types: postgres-instance, dns and route.

+-------------------+-------+------------------+--------------------------------+---------------------------------+
|     TYPE          | CLASS |      PARAMS      |            OUTPUTS             |          DESCRIPTION            |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| dns               | (any) |                  | host, url                      | Outputs a *.localhost domain as |
|                   |       |                  |                                | the hostname and associated URL |
|                   |       |                  |                                | in http on port 80              |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| postgres-instance | (any) |                  | host, password, port, username | Provisions a dedicated          |
|                   |       |                  |                                | PostgreSQL instance             |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| route             | (any) | host, path, port |                                | Provisions an Ingress route on  |
|                   |       |                  |                                | a shared Nginx instance         |
+-------------------+-------+------------------+--------------------------------+---------------------------------+
| service           | (any) |                  | name                           | Outputs the name of the         |
|                   |       |                  |                                | Workload dependency if it       |
|                   |       |                  |                                | exists in the list of Workloads |
+-------------------+-------+------------------+--------------------------------+---------------------------------+

generate

Convert the score-frontend.yaml and score-backend.yaml files into a deployable manifests.yaml, run the following command in your terminal:

score-k8s generate score-backend.yaml \
    --image ghcr.io/mathieu-benoit/backstage-backend:latest

score-k8s generate score-frontend.yaml \
    --image ghcr.io/mathieu-benoit/backstage-frontend:latest

The generate command will add the input score-frontend.yaml and score-backend.yaml workloads with a particular container image to the .score-k8s/state.yaml state file and generate the output manifests.yaml.

See the generated manifests.yaml by running this command:

cat manifests.yaml

If you make any modifications to the score-frontend.yaml or score-backend.yaml files, run score-k8s generate score.yaml to regenerate the output manifests.yaml.

resources

Get the information of the resources dependencies of the workload, run the following command:

score-k8s resources list
+--------------------------------------+--------------------------------+
|                 UID                  |            OUTPUTS             |
+--------------------------------------+--------------------------------+
| dns.default#dns                      | host, url                      |
+--------------------------------------+--------------------------------+
| postgres-instance.default#backend.pg | host, password, port, username |
+--------------------------------------+--------------------------------+
| service.default#frontend.backend     | name                           |
+--------------------------------------+--------------------------------+
| route.default#backend.route          |                                |
+--------------------------------------+--------------------------------+
| route.default#frontend.route         |                                |
+--------------------------------------+--------------------------------+

At this stage, we can already see the value of the dns resource generated:

score-k8s resources get-outputs dns.default#dns
{
  "host": "dnsnocrke.localhost",
  "url": "http://dnsnocrke.localhost:80"
}

Same for the postgres-instance resource:

score-k8s resources get-outputs postgres-instance.default#backend.pg
{
  "host": "pg-backstage-d7058793",
  "password": "REDACTED",
  "port": 5432,
  "username": "REDACTED"
}

kubectl apply

Here you will need to have access to a Kubernetes cluster to execute the following commands. You can follow these instructions if you want to set up a Kind cluster.

Run kubectl apply to execute the generated manifests.yaml file:

kubectl apply -f manifests.yaml
secret/pg-backend-61f0e7c1 created
statefulset.apps/pg-backend-61f0e7c1 created
service/pg-backend-61f0e7c1 created
httproute.gateway.networking.k8s.io/route-backend-711726ce created
httproute.gateway.networking.k8s.io/route-frontend-61f9e1b8 created
service/backend created
deployment.apps/backend created
service/frontend created
deployment.apps/frontend created

kubectl get all

See the running containers:

kubectl get all,httproute
NAME                           READY   STATUS    RESTARTS   AGE
pod/backend-57fc5664d-4hg7l    1/1     Running   0          22s
pod/frontend-db644cf86-n9cjd   1/1     Running   0          22s
pod/pg-backend-61f0e7c1-0      1/1     Running   0          60m

NAME                          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/backend               ClusterIP   10.96.183.51   <none>        7007/TCP   60m
service/frontend              ClusterIP   10.96.90.135   <none>        3000/TCP   60m
service/pg-backend-61f0e7c1   ClusterIP   10.96.45.108   <none>        5432/TCP   60m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend    1/1     1            1           60m
deployment.apps/frontend   1/1     1            1           60m

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-57fc5664d     1         1         1       22s
replicaset.apps/backend-64fc859b9d    0         0         0       60m
replicaset.apps/frontend-68ccb68884   0         0         0       60m
replicaset.apps/frontend-db644cf86    1         1         1       22s

NAME                                   READY   AGE
statefulset.apps/pg-backend-61f0e7c1   1/1     60m

NAME                                                          HOSTNAMES                 AGE
httproute.gateway.networking.k8s.io/route-backend-711726ce    ["dnsxmfazk.localhost"]   60m
httproute.gateway.networking.k8s.io/route-frontend-61f9e1b8   ["dnsxmfazk.localhost"]   60m

curl localhost

Test the running container, run the following command:

curl localhost -H "Host: dnsnocrke.localhost"
...
<title>Scaffolded Backstage App</title>
...

Congrats! You’ve successfully deploy, with the score-k8s implementation, a containerized Frontend application talking to a containerized Backend application exposed via a DNS. You provisioned them through kubectl, without writing the Kubernetes manifests file by yourself.

Next steps

Last modified June 27, 2025: Frontend & Backend example (#194) (bf6265d)