|
| 1 | +# EROFS Output Format (experimental) |
| 2 | + |
| 3 | +apko can emit image layers as [EROFS](https://erofs.docs.kernel.org/) filesystem images instead of the default gzip-compressed tar. |
| 4 | +The format tracks the draft [erofs/erofs-image-spec](https://github.com/erofs/erofs-image-spec) (PR [#1](https://github.com/erofs/erofs-image-spec/pull/1)). |
| 5 | +Until the spec reaches a stable release, the media types, annotations, and layer layout used here may change. |
| 6 | + |
| 7 | +## Why EROFS? |
| 8 | + |
| 9 | +- **Mount, don't unpack.** A layer blob is a complete, kernel-mountable read-only filesystem. |
| 10 | + You can `mount -t erofs` the layer directly and look at it, without extracting a tarball. |
| 11 | +- **Random access.** Container runtimes that consume EROFS images can seek into a layer rather than streaming the whole tar. |
| 12 | +- **Designed for sharing.** The spec defines `overlay-lower` and `overlay-data` roles that compose via the kernel's `overlayfs` exactly the way OCI tar layers do. |
| 13 | + |
| 14 | +This document focuses on producing EROFS images and verifying they look legit using widely available tools. |
| 15 | + |
| 16 | +## Prerequisites |
| 17 | + |
| 18 | +To build and inspect EROFS images you need: |
| 19 | + |
| 20 | +- apko built from a revision that contains EROFS support. |
| 21 | +- The `erofs-utils` package, which provides `mkfs.erofs`, `fsck.erofs`, and `dump.erofs`. |
| 22 | + apko ships a pure-Go writer (no CGO), so `mkfs.erofs` is not required for *producing* images — but `fsck.erofs` and `dump.erofs` are the easiest way to inspect what apko produced. |
| 23 | +- To mount an EROFS layer: either the kernel `erofs` module (present in modern Linux distros) plus root for `mount(8)`, or the unprivileged `erofsfuse` binary from `erofs-utils-fuse`. |
| 24 | + |
| 25 | +Install on Wolfi / Chainguard / Alpine: |
| 26 | + |
| 27 | +```sh |
| 28 | +sudo apk add erofs-utils # fsck.erofs, dump.erofs, mkfs.erofs |
| 29 | +sudo apk add erofs-utils-fuse # erofsfuse (optional, for unprivileged mount) |
| 30 | +``` |
| 31 | + |
| 32 | +Install on Debian / Ubuntu: |
| 33 | + |
| 34 | +```sh |
| 35 | +sudo apt install erofs-utils erofsfuse |
| 36 | +``` |
| 37 | + |
| 38 | +## Single-layer build |
| 39 | + |
| 40 | +The simplest case: opt into EROFS via the `--format=erofs` flag or `format: erofs` in apko.yaml. |
| 41 | + |
| 42 | +`erofs-demo.yaml`: |
| 43 | + |
| 44 | +```yaml |
| 45 | +contents: |
| 46 | + keyring: |
| 47 | + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub |
| 48 | + repositories: |
| 49 | + - https://packages.wolfi.dev/os |
| 50 | + packages: |
| 51 | + - wolfi-base |
| 52 | + |
| 53 | +cmd: /bin/sh -l |
| 54 | +archs: |
| 55 | + - host |
| 56 | +``` |
| 57 | +
|
| 58 | +Build into an OCI image layout directory: |
| 59 | +
|
| 60 | +```sh |
| 61 | +mkdir -p out |
| 62 | +apko build erofs-demo.yaml apko-erofs-demo:latest out/ --format=erofs --arch=$(uname -m) |
| 63 | +``` |
| 64 | + |
| 65 | +The OCI layout under `out/` is a regular OCI image directory — the layer blob just happens to be an EROFS filesystem: |
| 66 | + |
| 67 | +``` |
| 68 | +out/ |
| 69 | +├── blobs/sha256/ |
| 70 | +│ ├── <config-digest> # JSON image config |
| 71 | +│ ├── <manifest-digest> # JSON image manifest |
| 72 | +│ └── <layer-digest> # raw EROFS filesystem image |
| 73 | +├── index.json |
| 74 | +└── oci-layout |
| 75 | +``` |
| 76 | + |
| 77 | +### Verify the manifest references EROFS |
| 78 | + |
| 79 | +```sh |
| 80 | +MANIFEST=$(jq -r '.manifests[0].digest | split(":")[1]' out/index.json) |
| 81 | +jq . out/blobs/sha256/$MANIFEST |
| 82 | +``` |
| 83 | + |
| 84 | +Expected (excerpt): |
| 85 | + |
| 86 | +```json |
| 87 | +{ |
| 88 | + "layers": [ |
| 89 | + { |
| 90 | + "mediaType": "application/vnd.erofs", |
| 91 | + "size": 16207872, |
| 92 | + "digest": "sha256:8a2205cc..." |
| 93 | + } |
| 94 | + ] |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +The image config records `erofs` in `os.features` per spec §5.4, signalling to tools that don't implement the spec that they should not attempt to apply the layer as a tar: |
| 99 | + |
| 100 | +```sh |
| 101 | +CONFIG=$(jq -r '.config.digest | split(":")[1]' out/blobs/sha256/$MANIFEST) |
| 102 | +jq '.["os.features"]' out/blobs/sha256/$CONFIG |
| 103 | +# → ["erofs"] |
| 104 | +``` |
| 105 | + |
| 106 | +## Inspect the layer (no mount required) |
| 107 | + |
| 108 | +The layer blob is a complete EROFS filesystem. You can validate and inspect it without mounting anything. |
| 109 | + |
| 110 | +### Identify the file |
| 111 | + |
| 112 | +```sh |
| 113 | +LAYER=$(jq -r '.layers[0].digest | split(":")[1]' out/blobs/sha256/$MANIFEST) |
| 114 | +file out/blobs/sha256/$LAYER |
| 115 | +# → out/blobs/sha256/...: EROFS filesystem, blocksize=12, exslots=0, ... |
| 116 | +``` |
| 117 | + |
| 118 | +### Integrity check |
| 119 | + |
| 120 | +```sh |
| 121 | +fsck.erofs -d3 out/blobs/sha256/$LAYER |
| 122 | +# <I> erofs: No errors found |
| 123 | +``` |
| 124 | + |
| 125 | +### Dump the superblock |
| 126 | + |
| 127 | +```sh |
| 128 | +dump.erofs out/blobs/sha256/$LAYER | head -15 |
| 129 | +``` |
| 130 | + |
| 131 | +This prints the on-disk metadata: block size, inode count, build time, UUID, feature flags. |
| 132 | + |
| 133 | +### Extract without root |
| 134 | + |
| 135 | +`fsck.erofs --extract` reads every inode and writes the resulting tree to a directory. |
| 136 | +This is the strongest unprivileged validation you can run: if the image is malformed, extraction fails; if it succeeds, the file tree on disk is exactly what a kernel mount would expose. |
| 137 | + |
| 138 | +```sh |
| 139 | +mkdir extracted |
| 140 | +fsck.erofs --extract=extracted --xattrs --force out/blobs/sha256/$LAYER |
| 141 | +ls extracted/ |
| 142 | +# bin dev etc home lib ... |
| 143 | +cat extracted/etc/os-release |
| 144 | +``` |
| 145 | + |
| 146 | +## Mount the layer |
| 147 | + |
| 148 | +### Kernel mount (root) |
| 149 | + |
| 150 | +```sh |
| 151 | +sudo mkdir -p /mnt/apko-erofs |
| 152 | +sudo mount -t erofs -o loop out/blobs/sha256/$LAYER /mnt/apko-erofs |
| 153 | +ls /mnt/apko-erofs/ |
| 154 | +file /mnt/apko-erofs/bin/sh |
| 155 | +sudo umount /mnt/apko-erofs |
| 156 | +``` |
| 157 | + |
| 158 | +The kernel mount is read-only, zero-copy, and exposes xattrs. |
| 159 | +If `mount` reports "unknown filesystem type 'erofs'", the kernel module is missing on your system; install it (e.g. `linux-modules-extra-$(uname -r)` on Ubuntu) or use the FUSE path below. |
| 160 | + |
| 161 | +### FUSE mount (unprivileged) |
| 162 | + |
| 163 | +```sh |
| 164 | +mkdir -p mnt |
| 165 | +erofsfuse out/blobs/sha256/$LAYER mnt/ |
| 166 | +ls mnt/ |
| 167 | +fusermount -u mnt/ # or `fusermount3 -u mnt/` |
| 168 | +``` |
| 169 | + |
| 170 | +`erofsfuse` does not require root, which makes it convenient on dev machines and inside CI containers that lack the kernel module. |
| 171 | + |
| 172 | +## Pulling from a registry |
| 173 | + |
| 174 | +If you push the image with `apko publish` or `crane push`, the registry stores each blob unchanged — including the EROFS layer blob. |
| 175 | +Most registry clients can extract layers by digest: |
| 176 | + |
| 177 | +```sh |
| 178 | +# Read the manifest and pull layer blobs. |
| 179 | +crane manifest registry.example.com/apko-erofs-demo:latest > manifest.json |
| 180 | +LAYER_DIGEST=$(jq -r '.layers[0].digest' manifest.json) |
| 181 | +crane blob registry.example.com/apko-erofs-demo:latest@$LAYER_DIGEST > layer.erofs |
| 182 | + |
| 183 | +file layer.erofs # EROFS filesystem... |
| 184 | +fsck.erofs -d3 layer.erofs # <I> erofs: No errors found |
| 185 | +``` |
| 186 | + |
| 187 | +Once you have the blob on disk you can inspect or mount it exactly as in the previous sections. |
| 188 | + |
| 189 | +## Multi-layer builds |
| 190 | + |
| 191 | +Combine `format: erofs` with apko's [layering](layering.md) configuration to get one EROFS layer per package group plus a top layer for unowned files. |
| 192 | + |
| 193 | +`erofs-layered.yaml`: |
| 194 | + |
| 195 | +```yaml |
| 196 | +contents: |
| 197 | + keyring: |
| 198 | + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub |
| 199 | + repositories: |
| 200 | + - https://packages.wolfi.dev/os |
| 201 | + packages: |
| 202 | + - wolfi-base |
| 203 | + |
| 204 | +cmd: /bin/sh -l |
| 205 | +archs: |
| 206 | + - host |
| 207 | + |
| 208 | +layering: |
| 209 | + strategy: origin |
| 210 | + budget: 4 |
| 211 | + |
| 212 | +format: erofs |
| 213 | +``` |
| 214 | +
|
| 215 | +```sh |
| 216 | +mkdir -p out-layered |
| 217 | +apko build erofs-layered.yaml apko-erofs-layered:latest out-layered/ --arch=$(uname -m) |
| 218 | +``` |
| 219 | + |
| 220 | +Inspect the manifest: |
| 221 | + |
| 222 | +```sh |
| 223 | +MANIFEST=$(jq -r '.manifests[0].digest | split(":")[1]' out-layered/index.json) |
| 224 | +jq '.layers[] | {mediaType, role: .annotations["org.erofs.role"]}' out-layered/blobs/sha256/$MANIFEST |
| 225 | +``` |
| 226 | + |
| 227 | +Expected (last layer carries no role per spec §3.8 rule 1): |
| 228 | + |
| 229 | +```json |
| 230 | +{ "mediaType": "application/vnd.erofs", "role": "overlay-lower" } |
| 231 | +{ "mediaType": "application/vnd.erofs", "role": "overlay-lower" } |
| 232 | +{ "mediaType": "application/vnd.erofs", "role": "overlay-lower" } |
| 233 | +{ "mediaType": "application/vnd.erofs", "role": "overlay-lower" } |
| 234 | +{ "mediaType": "application/vnd.erofs", "role": null } |
| 235 | +``` |
| 236 | + |
| 237 | +Each layer is independently mountable as an EROFS filesystem, and each carries its own partial `usr/lib/apk/db/installed` so per-layer scanners (Trivy, Snyk, Grype) can identify the packages it contributes. |
| 238 | + |
| 239 | +### Assemble the full rootfs with overlayfs |
| 240 | + |
| 241 | +The OCI spec composes layers with `overlayfs`-style semantics; for EROFS layers the composition is straightforward. |
| 242 | +Mount each layer separately, then stack them with `mount -t overlay`: |
| 243 | + |
| 244 | +```sh |
| 245 | +# Pull each layer blob out of the OCI layout. |
| 246 | +ROOT=$(pwd)/out-layered/blobs/sha256 |
| 247 | +mkdir -p mnt/{lower0,lower1,lower2,lower3,top,merged,work,upper} |
| 248 | + |
| 249 | +LAYERS=$(jq -r '.layers[].digest | split(":")[1]' $ROOT/../../blobs/sha256/$MANIFEST) |
| 250 | +i=0 |
| 251 | +for d in $LAYERS; do |
| 252 | + sudo mount -t erofs -o loop "$ROOT/$d" "mnt/lower$i" 2>/dev/null || \ |
| 253 | + erofsfuse "$ROOT/$d" "mnt/lower$i" |
| 254 | + i=$((i+1)) |
| 255 | +done |
| 256 | + |
| 257 | +# In overlayfs, lowerdirs are listed top-down (highest priority first). |
| 258 | +# OCI orders layers bottom-up (index 0 is the base), so reverse the order. |
| 259 | +sudo mount -t overlay overlay \ |
| 260 | + -o lowerdir=mnt/lower$((i-1)):mnt/lower$((i-2)):mnt/lower1:mnt/lower0,upperdir=mnt/upper,workdir=mnt/work \ |
| 261 | + mnt/merged |
| 262 | + |
| 263 | +ls mnt/merged/ # full rootfs |
| 264 | +``` |
| 265 | + |
| 266 | +Clean up: |
| 267 | + |
| 268 | +```sh |
| 269 | +sudo umount mnt/merged |
| 270 | +for d in mnt/lower*; do sudo umount "$d" 2>/dev/null || fusermount -u "$d"; done |
| 271 | +``` |
| 272 | + |
| 273 | +Production runtimes (containerd's erofs snapshotter, podman/CRI-O with the erofs-aware plugin, etc.) automate this assembly; the manual steps above are for verifying that an apko-built EROFS image really does compose into a valid rootfs. |
| 274 | + |
| 275 | +## Current limitations |
| 276 | + |
| 277 | +- **No compression.** apko emits raw `application/vnd.erofs` layers only. The draft spec defines `application/vnd.erofs+zstd` but neither apko's writer nor the underlying go-erofs library writes compressed images yet. |
| 278 | +- **No dm-verity.** The spec's verified-mount path (§3.5) is not produced. |
| 279 | +- **No chunk index.** Lazy-loading runtimes (per spec §3.4) won't get an index; reads are sequential. |
| 280 | +- **No `overlay-data` or `device` roles.** Only `overlay-lower` (and unannotated final) layers are emitted. |
| 281 | +- **Spec is draft.** Media-type strings and annotation keys may change before the spec stabilizes. Treat any image built today as experimental. |
| 282 | + |
| 283 | +If you need any of the above, please open an issue. |
| 284 | + |
| 285 | +## See also |
| 286 | + |
| 287 | +- [erofs/erofs-image-spec PR #1](https://github.com/erofs/erofs-image-spec/pull/1) — the layer format spec apko tracks. |
| 288 | +- [EROFS kernel documentation](https://erofs.docs.kernel.org/) — on-disk format reference. |
| 289 | +- [Layering in apko](layering.md) — how the multi-layer strategy partitions packages into groups. |
0 commit comments