Published
- 5 min read
From Docker Compose to Score: A Platform Engineering Guide
Welcome! You’ve probably seen Platform Engineering everywhere, along with terms like Internal Developer Platform (IDP), Internal Developer Portal, and the shift towards graph-based backends instead of traditional pipelines. It feels like discovering design patterns for the first time or hearing about a new frontend framework—you’re eager to dive in. But where should you begin?
Here’s the context I’m coming from:
- All applications are containerized and deployed to a Kubernetes cluster.
- Developers use Docker Compose or Helm (with intercept tools) for local development. Some require a container registry, while others build images locally.
- Applications are deployed via pipelines or a GitOps approach.
- Infrastructure is managed using Terraform.
With a focus on self-service and an outstanding developer experience (DevEx), I decided to explore the Score specification, aiming to standardize a platform-agnostic specification for developers to use across environments. In future posts, I plan to walk through each layer of the stack, providing a complete platform overview.
This post will guide you on transitioning from Docker Compose to the Score Specification. Once transitioned, Score can generate deployment modes in Docker Compose or other supported options.
What is Score?
Score is a developer-centric, platform-agnostic specification (YAML file) for managing workloads. It ensures consistent configuration across local and remote environments. In Score, you define workloads (which can be mapped to Kubernetes Pods) and other key definitions like containers, services, and resources.
Score supports multiple implementations (CLI tools) that generate platform-specific configuration files, such as Kubernetes or Docker Compose. You can define a Score file and then choose the platform you want to deploy it on. Since we are focusing on Docker Compose, the relevant implementation is score-compose.
For a deeper understanding, the score documentation site provides comprehensive details.
Our Example application
The application we’ll use follows a basic architecture: frontend, backend, and database — known as box-box-cylinder (pictured when you see the architecture diagram). The technologies used are Next.js for the frontend, .NET for the backend, and Microsoft SQL Server for the database. See the source code. Note that this is not production-ready; it’s just a sample for demonstrating Score.
Docker Compose File
The Docker Compose file is straightforward for the three components, but here are a couple of key aspects to note:
- Build: Since this is for local development, the images can be rebuilt after changes, eliminating the need for reliance on a container registry.
- Ports: Ports are exposed to the host, allowing developers to connect directly to the database for debugging purposes, for instance.
name: manually-created
services:
backend:
build:
context: ./backend
environment:
ASPNETCORE_ENVIRONMENT: UAT
depends_on:
- db
ports:
- target: 8080
published: '8082'
frontend:
build:
context: ./frontend
environment:
ENVIRONMENT: UAT
depends_on:
- backend
ports:
- target: 3000
published: '3233'
db:
image: mcr.microsoft.com/mssql/server:2022-latest
env_file:
- ./backend/.env
ports:
- target: 1433
published: '1433'
Defining the Score Specification
For our application, we decided to define the following files for managing our workloads:
- score-frontend.yaml
- score-backend.yaml
- score-database.yaml
Score Frontend File
apiVersion: score.dev/v1b1
metadata:
name: frontend
service:
ports:
frontend:
port: 3233
targetPort: 3000
containers:
frontend:
image: ./frontend
variables:
ENVIRONMENT: UAT
Score Backend File
apiVersion: score.dev/v1b1
metadata:
name: backend
service:
ports:
backend:
port: 8082
targetPort: 8080
containers:
backend:
image: ./backend
variables:
ASPNETCORE_ENVIRONMENT: UAT
Score Database File
apiVersion: score.dev/v1b1
metadata:
name: database
service:
ports:
database:
port: 1433
targetPort: 1433
containers:
database:
image: mcr.microsoft.com/mssql/server:2022-latest
variables:
ENVIRONMENT: UAT
Generate Docker Compose file with score-compose
You would need to follow the instructions to install score-compose first and assuming Docker is already installed.
Initialize
Once installed, initialize score-compose:
score-compose init --no-sample
The flag ‘—no-sample’ ensures init does not create a default score.yaml file as we have our files defined already.
Generate
Run generate on for each workload:
score-compose generate score-frontend.yaml --build=frontend=./aspnetcore -o score-compose.yaml
score-compose generate score-backend.yaml --build=backend=./backend -o score-compose.yaml
score-compose generate score-database.yaml -o score-compose.yaml
It’s important to note that the generate command is accumulative, meaning each run will add to the existing generated state. Multiple runs are currently necessary because the —build parameter does not yet support workload context (though this feature is coming soon).
Here’s a breakdown of the parameters:
- —build: Specifies an optional build context for the container. The format is either —build=container=./dir or —build=container={“context”:“./dir”}. This is especially useful for development as it allows the image to be built when running docker compose up, unlike the database, where we use an existing image.
- -o: Specifies the output file for the generated Docker Compose file. By default, this is compose.yaml.
We run one last generate to expose ports to the host:
score-compose generate score-database.yaml --publish 8002:backend:8002 --publish 5348:frontend:5348 --publish 1433:database:1433 -o score-compose.yaml
Here’s a deeper look into the parameter:
• —publish: This flag makes the defined ports accessible on the Docker host. It follows the format HOST_PORT:/
Generated Docker-Compose file
The resultant file looks mostly like the one that was manually created:
name: src
services:
backend-backend:
annotations:
compose.score.dev/workload-name: backend
build:
context: ./backend
environment:
ENVIRONMENT: UAT
hostname: backend
ports:
- target: 8002
published: '8002'
database-database:
annotations:
compose.score.dev/workload-name: database
environment:
ENVIRONMENT: UAT
hostname: database
image: mcr.microsoft.com/mssql/server:2022-latest
ports:
- target: 1433
published: '1433'
frontend-frontend:
annotations:
compose.score.dev/workload-name: frontend
environment:
ENVIRONMENT: UAT
hostname: frontend
image: ./frontend
ports:
- target: 5348
published: '5348'
The naming convention with the services is ‘workloadName-containerName’ so you could adjust it if you dont like this convention.
Docker-compose up
To get up and running, we execute docker-compose up specifying our generated file:
docker-compose -f score-compose.yaml up
Your application should now be running as expected. The only aspect I haven’t covered yet is the depends_on property, but I will update this as soon as I explore it further.
Conclusion
Just like learning the Docker Compose specification, the Score specification is straightforward and easy to adopt. We’ve walked through a simple example application and created Score files to define our workloads. You could even install a different implementation, such as score-k8s, and generate manifest files to deploy to a Kubernetes cluster. I hope this introduction provided valuable insights into the possibilities. Would you consider transitioning to Score?