peel is a minimal init (PID 1) for LXD containers that unpeels an OCI image into a running LXD container: it pulls the image from a registry, unpacks its layers directly onto the container's root filesystem, and execs the image's entrypoint.
It is meant to be installed as /sbin/init inside an otherwise-empty LXD
image (see image/). The LXD image itself carries no application
code; peel downloads that at container start time, based on a handful of LXD
instance configuration keys.
Warning
peel is a prototype. It should not be used in a production environment.
When the container first starts, peel reads its configuration from
/peel/config.json (rendered by an LXD image template from the instance
configuration keys listed below), brings up networking. Peel only ever
unpeels once: if the OCI is already unpeeled, it goes straight to starting the
entrypoint.
Otherwise, it pulls the image's manifest, config and layers from the
registry and extracts every layer directly onto /, honouring whiteout
files as defined by the OCI image spec.
peel never deletes anything on its own initiative: only whatever the image's own
layers overwrite or explicitly whiteout changes on disk. Layers are never
permitted to touch /sbin/init or anything under /peel/, so an image can't
replace peel or tamper with peel's own state.
/peel/config.json is deleted once peel has unpacked the OCI image, so registry
credentials don't linger in the rootfs.
Finally, peel resolves the command to run (the image's entrypoint/cmd/env/
working directory/user, together with any of peel's own overrides),
execs it, and supervises the container from then on: reaping every exited
process (including re-parented orphans), and turning SIGINT/SIGPWR (or
the entrypoint exiting on its own) into a reboot(2) syscall, which LXD
intercepts to actually restart or stop the container.
LXD hands the container a bare network interface with no address and no
/etc/resolv.conf. Since peel's own rootfs starts out essentially empty,
peel is also its own minimal IPv4/IPv6 configurator: on every boot it brings
up every non-loopback interface via DHCPv4 and IPv6 SLAAC, and writes the
results to /etc/resolv.conf. This runs before peel ever talks to a
registry, and is best-effort: a failure on any interface or protocol is
logged but never blocks boot.
Peel embeds a copy of Mozilla's CA bundle as distributed by curl for its own OCI registry connections.
If /peel/lo exists inside the container (typically a disk device the
operator has attached identically to every container that should
participate), peel listens on a unix socket at /peel/lo/<hostname> and
uses it to share its loopback (and wildcard, i.e. "all interfaces") TCP/UDP
listeners with every other container that also has a socket there, and to
pick up theirs in return. Concretely: if container a has something
listening on 127.0.0.1:8080, and container b also participates, a
process inside b can reach it at its own 127.0.0.1:8080, as if the two
containers shared a network namespace (much like containers within the
same pod in Kubernetes or Podman). Peel discovers its own listeners by
polling /proc/net/{tcp,tcp6,udp,udp6} (every second for the first minute
after it starts, then every 5 seconds), and discovers peers by polling
/peel/lo itself on that same schedule; listeners a container only has
because it's proxying for a peer are never re-advertised to others.
The exact address a listener is bound to matters: a listener on a specific
loopback address (e.g. 127.0.0.2, as opposed to 127.0.0.1) is proxied
on that same specific address, and a listener bound to a wildcard address
(0.0.0.0/::) is proxied on that same wildcard address in every other
container too. A wildcard-bound proxy still only forwards connections that
are themselves local (loopback), though: it never turns into an open relay
for traffic arriving on a container's real network interfaces.
This is entirely peer-to-peer and best-effort: it has no effect unless
/peel/lo is attached, and a container that can't reach a peer's socket
(e.g. because that container hasn't started yet) just keeps retrying.
Set these with lxc config set <instance> <key> <value> before starting the
container (or as part of lxc launch/init ... -c key=value). Only
user.oci.image is required.
| Key | Effect |
|---|---|
user.oci.image |
Required. The OCI image reference, e.g. docker.io/library/nginx:1.27. |
user.oci.platform |
os/arch[/variant] to pull, e.g. linux/arm64. Defaults to the host's platform. |
user.oci.insecure |
true to allow talking to the registry over plain HTTP. |
user.oci.username |
Registry username (basic auth). |
user.oci.password |
Registry password (basic auth). |
user.oci.auth |
base64(username:password), as an alternative to username/password. |
user.oci.identity_token |
Registry identity token (as in a Docker config.json auth entry). |
user.oci.registry_token |
Registry bearer token (as in a Docker config.json auth entry). |
user.oci.entrypoint |
JSON array overriding the image's Entrypoint, e.g. ["/bin/myapp"]. |
user.oci.cmd |
JSON array overriding the image's Cmd, e.g. ["--flag","value"]. |
user.oci.env |
JSON array of "KEY=VALUE" strings, merged on top of the image's Env. |
user.oci.working_dir |
Overrides the image's working directory. |
user.oci.user |
Overrides the image's user, as uid[:gid] or name[:group]. |
It is recommended to set security.devlxd=false to prevent a container process
reading any configuration.
image/build.sh # host architecture -> image/dist/peel-<arch>.tar.xz
image/build.sh -a arm64 # cross-compiled for arm64
lxc image import image/dist/peel-amd64.tar.xz --alias peel
lxc init peel my-container -c user.oci.image=docker.io/library/nginx:1.27
lxc start my-containerSee image/build.sh -h for all options. The script cross-compiles peel with
CGO_ENABLED=0, so building for arm64/ppc64le/etc. from an amd64 host
works without a cross toolchain.
Every tagged release (see .github/workflows/release.yml)
publishes a peel image for each supported architecture as a release asset,
and republishes a small simplestreams
index/images document as assets on a separate streams/v1 release. That
means this repository doubles as an LXD image server:
lxc remote add peel https://github.com/canonical/peel/releases/download --protocol simplestreams
lxc launch peel:peel my-container -c user.oci.image=docker.io/library/nginx:1.27lxc image list peel: will show the latest published image for your local
architecture; older versions remain available and can be selected by
fingerprint.