Here’s how we can use CUE with a registry, including:
- using CUE to validate existing non-CUE data files
- how we can use schemas in the Central Registry to do a better job of that
- how we can use CUE to define/generate configuration
- how we can abstract that into a reusable component (module)
- how we can publish that module to a registry
A lot of this is quite new, so don’t be surprised if you find a few rough edges. That said, it’s reliable enough for us to dogfood it in our own services.
Scenario:
We’re at a company that produces lots of different Docker images.
The maintainers of each image are expected to provide a docker-compose.yaml
file that is used for testing the service.
Here’s an example. Create a new directory, and place this file as
docker-compose.yaml
inside:
service:
frontend:
image: docker.io/library/nginx:latest
ports:
- "80:80"
volume:
- ./html:/usr/share/nginx/html
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
db:
image: docker.ru/library/postgres:17.0
What aspects of this structured data do we care about?
- it’s a valid docker compose file
- it exposes a service named “web” that exports the HTTP interface on port 8080
- if there’s a database service, it must be one of a few acceptable versions of Postgres
- all our images come from our approved registry
Let’s start by just checking the broad outline of what it means to be “a valid docker compose file”, and that there’s a web service specified.
Create this file as schema.cue
alongside the docker-compose.yaml
file:
package splotpolicy
#WebService: {
services!: {
web!: {
ports!: ["8080:80"]
...
}
db?: {
image!: "docker.io/library/postgres:16.4" |
"docker.io/library/postgres:16.5" |
"docker.io/library/postgres:16.6"
...
}
[string]: image!: =~"^docker\\.io/"
}
...
}
Validate your data file:
$ cue vet .:splotpolicy docker-compose.yaml -d '#WebService'
services: field is required but not present:
./schema.cue:4:2
Oh dear! We’ve definitely found at least one problem in our
docker-compose.yaml
file: the top-level services
field is missing. Correct
this in your YAML file, and re-run the cue vet
command from above.
As you re-run cue vet
, you’ll find that there are more problems in your
docker-compose.yaml
file. cue vet
is only satisfied when it’s completely
silent. Try and correct each problem as cue vet
points it out.
Don’t worry if you can’t figure out all the problems in our deliberately broken data file!
If you get stuck, here’s a corrected docker-compose.yaml
file - copying the
contents over from here to your file is absolutely fine …
Here’s the corrected file that cue vet
will have led you towards:
services:
web:
image: docker.io/library/nginx:latest
ports:
- "8080:80"
volume:
- ./html:/usr/share/nginx/html
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
db:
image: docker.io/library/postgres:16.4
There might be some minor differences, such as the specific version of Postgres that you chose, but anything that satisfies the schema is fine.
Check that it satisfies the schema with a final cue vet
:
$ cue vet .:splotpolicy docker-compose.yaml -d '#WebService'
This is great - we can now assert some useful properties of the docker compose files that the company’s developers are handing to us. But we’re not doing as much checking as we could …
Wouldn’t it be nice if someone had done lots of the work for us already, assembling a schema that validates the general shape of a docker compose file?
Enter the Central Registry
The CUE Central Registry is a place where schemas are published that we can
tell the cue
command to use, seamlessly.
Place this CUE code in moreschema.cue
:
package splotpolicy
import (
"github.com/cue-tmp/jsonschema-pub/exp2/dockercompose"
)
#WebService: dockercompose.#Schema & {
...
}
All files in a directory with the same package name are unified together, so this CUE unifies the upstream schema, imported from the registry, with our local #WebService
definition. This combines the company-specific policy validation (such the frontend web service being called web
) with the more general docker compose schema.
You’ll notice that the import path looks a bit temporary - as indeed it is. But one of the important properties of the CUE registry system is that of permanency: once a given version of a module is in the Central Registry, it will remain there indefinitely. When a final location is chosen, there is CUE tooling that allows straightforward refactoring of code to alter import paths
As a one-off, login to the Central Registry:
$ cue login
$ cue mod init cue.example
$ cue mod tidy
After cue mod tidy
completes, check out the contents of cue.mod/module.cue
:
$ cat cue.mod/module.cue
module: "cue.example"
language: {
version: "v0.12.0"
}
deps: {
"github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0": {
v: "v0.0.1"
default: true
}
}
Notice that cue mod tidy
has found and added the latest version
of the dockerconfig
module. The version declared in the module.cue
file acts as a kind of lock file.
Now let’s re-run our cue vet
command, taking advantage of the
more complete schema checking:
$ cue vet .:splotpolicy docker-compose.yaml -d '#WebService'
services.web.volume: field not allowed:
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:8:2
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:8:8
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:18:22
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:19:4
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:19:30
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:272:18
.cache/cue/mod/extract/github.com/cue-tmp/jsonschema-pub/exp2/dockercompose@v0.0.1/schema.cue:475:3
./docker-compose.yaml:6:5
./moreschema.cue:7:14
./schema.cue:3:14
./schema.cue:7:4
./schema.cue:15:13
Whoops - it’s spotted an error in our original YAML file: the web
service’s
volume
key isn’t allowed! Let’s correct it to volumes
so we end up with
this really really fixed data file:
services:
web:
image: docker.io/library/nginx:latest
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
db:
image: docker.io/library/postgres:16.4
A quick cue vet
shows us that we’re – at last! – in possession of some properly formed data:
$ cue vet .:splotpolicy docker-compose.yaml -d '#WebService'
$ cue mod registry 127.0.0.1:55443 &
$ cue mod edit --source self
$ cue mod rename github.com/cue-examples/splotpolicy
$ export CUE_REGISTRY=github.com/cue-examples=127.0.0.1:55443
$ cue mod publish v0.0.1
published github.com/cue-examples/splotpolicy@v0.0.1 to 127.0.0.1:55443/github.com/cue-examples/splotpolicy:v0.0.1
$ cue import -p splotservice1 -l 'content:' docker-compose.yaml
$ cat docker-compose.cue
package splotservice1
content: services: {
web: {
image: "docker.io/library/nginx:latest"
ports: ["8080:80"]
volumes: [
"./html:/usr/share/nginx/html",
"./config/nginx.conf:/etc/nginx/nginx.conf:ro",
]
}
db: image: "docker.io/library/postgres:16.4"
}
package splotservice1
import "github.com/cue-examples/splotpolicy"
content: splotpolicy.#WebService
$ cue vet .:splotservice1
$ sed -i 's/postgres:16.4/postgres:17/' docker-compose.cue
$ cue vet .:splotservice1
content.services.db.image: 3 errors in empty disjunction:
content.services.db.image: conflicting values "docker.io/library/postgres:16.4" and "docker.io/library/postgres:17":
./docker-compose.cue:12:13
./schema.cue:10:12
./service1.cue:5:10
content.services.db.image: conflicting values "docker.io/library/postgres:16.5" and "docker.io/library/postgres:17":
./docker-compose.cue:12:13
./schema.cue:11:5
./service1.cue:5:10
content.services.db.image: conflicting values "docker.io/library/postgres:16.6" and "docker.io/library/postgres:17":
./docker-compose.cue:12:13
./schema.cue:12:5
./service1.cue:5:10
$ sed -i 's/postgres:17/postgres:16.4/' docker-compose.cue
$ cue vet .:splotservice1
This is basically as far as we managed to get during the CfgMgmtCamp 2025 demo - but there’s so much more to CUE + Central Registry! Come and talk to Marcel or Roger at the CUE workshop to get hands-on help with your specific use cases.