docker-create describes a system as a Docker Compose service: the OS itself,
with systemd as PID 1 and its own Docker inside, on the Docker you already have.
It builds nothing and starts nothing — it writes two files, and starting,
stopping and inspecting the container is plain docker compose.
It is meant for trying a configuration out locally. A container shares the host's kernel and has no disk, so it behaves like CuOS in an LXC container, not like a system on hardware.
- Docker with Compose 2.30 or newer (for
post_start, see below) jq- An x86_64 host for the default image. Elsewhere, see Choosing the image.
From the directory the configuration is in:
./cuos-release/tool.sh docker-create my-system.json
docker compose -f CuOS-my-system.compose.yml up -d
docker compose -f CuOS-my-system.compose.yml exec cuos-my-system bashNAME is the artefact name, as everywhere else (./cuos-release/tool.sh name my-system.json). Two files appear in the current directory:
| File | What it is | Commit it? |
|---|---|---|
NAME.compose.yml |
The service, its volume and its networks | Yes — it holds no secrets |
NAME.system.json |
The merged configuration, mounted as /system_init.json |
No — it is git-ignored for you |
The configuration is the merged one, and "#include" pulls secrets into it
(system_file_password, registry passwords), so docker-create adds it to
.gitignore — together with ./.docker/, which it writes too. Both are
regenerated by running docker-create again, which is also what a fresh clone
needs before up.
Running docker-create again overwrites NAME.compose.yml. Put services of
your own — a test client, a fake device — in a file that includes it:
# compose.yml
include:
- CuOS-my-system.compose.yml
services:
tester:
image: alpine
command: sleep infinity
networks: [lan]Two systems included this way may share a network by giving it the same name.
| Why | |
|---|---|
entrypoint: /sbin/init |
The images' own entrypoint only sleeps. |
privileged: true |
systemd, and the Docker daemon inside. |
/data as a named volume |
State, and /var/lib/docker, which the image links to /data/docker. |
NAME.system.json → /system_init.json |
CuOS in a container reads its configuration from there, once. |
hostname |
From system.json. |
dns |
Every dns-server of every network entry, each once. |
The configuration is read on the first start only. CuOS copies it to
/data and never looks at /system_init.json again, so after changing
system.json:
./cuos-release/tool.sh docker-create my-system.json
docker compose -f CuOS-my-system.compose.yml down -v # -v: forget /data
docker compose -f CuOS-my-system.compose.yml up -dWithout -v the old configuration stays. An update run inside the container
lives in the container's own layer: down or a recreated container is back on
the image the compose file names.
Without --platform, lxc_image is used, and os_image if there is none.
--platform P selects P_image instead — the same keys as for image:
./cuos-release/tool.sh docker-create --platform rpi-arm64 my-system.jsonThat is the way to a native container on an arm64 host, an Apple Silicon Mac
or a Raspberry Pi: the default images are amd64, and run emulated anywhere
else — slowly, if systemd starts at all. A board's image never falls back to
os_image, for the same reason as in image.
The image is pinned by tag and digest (name:version@sha256:...). It is
pulled while the file is written, with the configuration's registry login, so
up needs no login. To pull it again later from a registry that needs one:
DOCKER_CONFIG="${PWD}/.docker" docker compose -f CuOS-my-system.compose.yml pullAn update compares against /etc/image, which first-run.sh writes. A
container started from the image has never run it, so the compose file passes
the image in CUOS_IMAGE and CUOS_IMAGE_DIGEST, which CuOS reads on its first
start.
Images released before that read nothing from the environment, and would write
unknown@unknown. For them the service carries a post_start hook that runs
first-run.sh with the right values — but only while /etc/image is missing or
unknown@unknown, so that it never undoes an update made inside the container.
CuOS configures no network inside a container; the compose file does. Each
entry of network becomes one Compose network, net<i> for network[i],
attached in that order. What only Docker needs to know is in docker.nets[i] —
the same pairing as proxmox.nets for Proxmox:
{
"network": [
{ "dhcp": true },
{ "ip-address": "10.20.0.5", "network-mask": "255.255.255.0", "gateway": "10.20.0.1" },
{ "ip-address": "192.168.50.7", "network-mask": "255.255.255.0" }
],
"docker": {
"nets": [
"lan",
{ "name": "iot", "static": true },
{ "name": "field", "driver": "macvlan", "parent": "eth1" }
]
}
}Key of docker.nets[i] |
Default | |
|---|---|---|
| (a string) | Shorthand for name. null keeps the element's place. |
|
name |
net<i> |
The Compose network. Lets other services — and other systems — join it. |
driver |
bridge |
bridge, macvlan or ipvlan. |
parent |
The host interface, for macvlan and ipvlan. Required there. |
|
static |
false |
Use ip-address, network-mask and gateway of network[i]: the network gets that subnet, the container that address. Always on for macvlan and ipvlan, which have no DHCP. |
Three ways, then:
- A bridge with an address from Docker — the default, and what works
everywhere, Docker Desktop included. The addresses in
system.jsonare ignored. - A bridge with the address from
system.json("static": true). Thegatewaybecomes the host's address on that bridge, and traffic leaves through the host as usual. Do not use it for the subnet the development machine itself is in: the bridge would take that subnet over on the host. Two compose projects using the same subnet cannot both start. - macvlan or ipvlan, to put the container on a real network next to the host. Linux only — Docker Desktop has no host interfaces to attach to. The host itself cannot reach a macvlan container directly; that is how macvlan works.
A mac-address in network[i] is set on that interface. Interface names
from network[i].name are not: inside the container they are eth0, eth1, …
in the order above.
docker is a key of the tooling, like proxmox: CuOS ignores it, and it is not
part of the configuration schema.
| Symptom | Look at |
|---|---|
post_start is rejected |
Compose older than 2.30. Update it. |
exec format error, or the container exits at once on an arm64 host |
The amd64 image without emulation. Use --platform rpi-arm64. |
Pool overlaps with other one on this address space |
A static bridge whose subnet another network on this host already has. |
up fails: the bind source path does not exist |
NAME.system.json is missing beside the compose file, as after a fresh clone — run docker-create again. |
A changed system.json has no effect |
/data keeps the first configuration: down -v. |
- CuOS in an LXC container — how CuOS behaves without its own kernel and disk
- Deploying to Proxmox VE — the same system as a VM or an LXC container