Build and move container images

Money first. An image registry bills for what it stores, and nothing here ever deletes an image. Every push you make stays until you remove it with the image registry's own tools. Pushing the same image twice costs nothing extra — its layers are already there — but a new build that changes one byte is a new image.

OctoMY™ builds, inspects, pushes, pulls and copies OCI container images itself. There is no Docker installation, no daemon and no docker command involved: the image format and the image registry protocol are implemented directly, in the same C++ as the rest of the tree.


What you need

  • A build of fleet-ctl:

    qbs build -p fleet-ctl
    

    It lands in default/install-root/usr/local/bin/.

  • For a push or a copy, an image registry you can write to, reachable over HTTPS. Plain HTTP is accepted only for an image registry on this machine (localhost, 127.0.0.1, ::1), and no option changes that.

Build an image

Put the files the image should hold in a directory, laid out as they should appear inside the image, then build:

fleet-ctl image build \
  --layout ~/images --ref agent \
  --root /tmp/agent-root \
  --platform linux/arm64 \
  --entrypoint /usr/local/bin/agent

This writes an OCI image layout to ~/images and names the image agent inside it. The digest of the new image is printed on standard output, and nothing else is, so a script can capture it.

Builds are reproducible. The same files give the same digest on any machine on any day: every file is stored with a modification time of zero, owner 0:0, names in a fixed order, and only its executable bit kept from its permissions. That is what lets a second push skip every layer the image registry already holds.

A layer refuses a few things by name rather than storing them wrongly: hard links, sparse files, device nodes, named pipes and sockets. If a build stops on one of them, the message names the file.

To build on top of another image, point at the layout that holds it:

fleet-ctl image build --layout ~/images --ref agent --root /tmp/agent-root \
  --platform linux/arm64 --base-layout ~/base-images --base-ref runtime

A base can come from a public image registry. Pull it into a layout first:

fleet-ctl image pull docker.io/library/debian:trixie-slim --layout ~/base-images --ref debian

A public image is usually an index, with one image for each platform. The build takes the one for its --platform, and stops with the list of platforms the index holds when none matches. A container has no UTF-8 locale unless you give it one, so add --env LANG=C.UTF-8 for a Qt program.

Look inside an image

fleet-ctl image inspect --layout ~/images --ref agent
fleet-ctl image inspect registry.example.com/team/agent:v1

Both print the same JSON: the manifest digest, the platforms, the config, and every layer twice — once as layers, the digests of the compressed bytes the image registry stores, and once as diffIds, the digests of the uncompressed content. When a pull fails with a digest mismatch, the pair is the first thing to compare.

Push, pull and copy

fleet-ctl image push --layout ~/images --ref agent registry.example.com/team/agent:v1
fleet-ctl image pull registry.example.com/team/agent:v1 --layout ~/mirror --ref agent
fleet-ctl image copy registry.example.com/team/agent:v1 eu.registry.example.com/team/agent:v1

A push checks each blob before it sends anything and skips the ones the image registry already has. Large layers go up in chunks, and a dropped connection resumes from where the image registry says it stopped instead of starting over. A push refuses to finish any blob whose bytes do not match its digest.

A copy keeps the source's digest exactly. Within one image registry it moves layers between repositories without transferring them; between two image registries it fetches only what the target is missing.

Every pull is verified. A layer, config or manifest whose bytes do not hash to the digest that named it is rejected and never written.

Credentials

For an image registry that needs a login, give the username and feed the password on standard input, from wherever you keep it:

fleet-ctl image push --username robot --password-stdin \
  --layout ~/images --ref agent registry.example.com/team/agent:v1 < ~/.secrets/registry-password

The password is used for that one command and stored nowhere. fleet-ctl reads no environment variable and no Docker configuration file for credentials.

Measure an image registry with small objects

fleet-ctl image bench registry.example.com/team/bench --count 10000 --bytes 4096 --compare-dir /tmp/objects

The bench pushes each object as one artifact, a blob and a manifest tagged with its name, and then writes the same objects as files in --compare-dir. It prints both wall times as JSON. Use it before you put many small files, such as a compile cache, in an image registry.

From OPAL

The same four operations are OPAL tasks, so a plan, a trigger or the assistant can run them:

Task Does Tier
oci.inspect Read an image's manifest, config and layer digests operational
oci.build Build an image from a directory into a layout. No network operational
oci.push Push an image from a layout to an image registry secured
oci.copy Copy an image between image registries secured

oci.push and oci.copy use the image registry credentials stored for this node, sealed so that only this node can open them.

In this section
Topics
howto development containers oci images fleet