Appearance
Preparing a repository
The builder fetches your repository at one commit and builds what it finds in the build context — the repository root unless you name a subdirectory. Three layouts are supported:
| Layout | You add | Typical build time |
|---|---|---|
| Plain Dockerfile | a Dockerfile | minutes |
| pvpkg package with a Dockerfile | pvpkg.json, a Dockerfile, optional pvpkg/ overlay | minutes |
| Yocto recipe | pvpkg.json with build.kind: yocto, kas files, your layer | minutes from the shared cache; hours cold |
| Buildroot recipe | pvpkg.json with build.kind: buildroot, a defconfig | tens of minutes |
Repositories must be reachable over HTTPS on GitHub or GitLab. Private ones need a git secret.
Plain Dockerfile
Any repository with a Dockerfile builds as is:
text
my-app/
├── Dockerfile
└── src/…The Dockerfile is built for each requested architecture (use FROM --platform=$BUILDPLATFORM and TARGETARCH for cross builds, or base images that exist for every architecture) and packaged as a Pantavisor container named after the repository, or the app you give the build. Paths can be changed with the build's Dockerfile and context settings; both must stay inside the repository.
pvpkg.json
pvpkg.json is the packaging manifest used by Pantacor's container CI (container-ci): a repository that already builds there builds the same way here. It sits at the top of the build context, beside an optional pvpkg/ folder:
text
my-app/
├── pvpkg.json
├── Dockerfile
└── pvpkg/
├── generic/ # laid over every architecture's export
│ └── _config/my-app/etc/my-app/config.json
└── arm64v8/ # only over the arm64 export
└── my-app/src.jsonjson
{
"name": "my-app",
"url": "ghcr.io/acme/my-app:main",
"package_url": "https://github.com/acme/my-app",
"arch": ["arm64v8", "amd64"],
"dockerfile": "Dockerfile",
"src_extra": {
"args": { "PV_GROUP": "app" }
}
}| Key | Meaning |
|---|---|
name | The app's name in the export and on the device. |
url | The image name (and tag base) recorded in src.json. |
arch | Architectures the package exists for: amd64, arm64v8, arm32v6, arm32v7. A build of another architecture is refused. Omit to allow all. |
version | The package version; see Versions. |
dockerfile | The Dockerfile, relative to the manifest. |
type | singlearch tags the image per architecture (arm64v8-1.0.0). |
src_extra | Extra src.json fields, such as the container's Pantavisor args, or format_options for squashfs (-comp xz, -b 1M, … from an allowed list). Fields the builder generates (docker_*, rootfs_url, pvr, …) cannot be set. |
remove_volumes | Volumes the image declares (VOLUME) to drop from the container's config. |
build | A Yocto or Buildroot recipe instead of the Dockerfile. |
The pvpkg/ folder is copied over the pvr checkout of each export: generic/ for every architecture, then the architecture's own folder (amd64/, arm64v8/, arm32v6/). Use it for configuration files (_config/<app>/…), a committed src.json the builder completes, or other parts of the state. It may hold only directories and regular files: a symlink in pvpkg/ fails the build. A .pvr folder in it is skipped.
Yocto
A Yocto package builds a root filesystem image with kas and meta-pantavisor, and packages it as the container's rootfs. The manifest names the kas file per architecture and the image recipe:
json
{
"name": "hello-http",
"url": "ghcr.io/acme/hello-http:main",
"package_url": "https://github.com/acme/hello-http",
"arch": ["arm64v8", "amd64"],
"build": {
"kind": "yocto",
"kas": { "arm64v8": "kas-arm64.yml", "amd64": "kas-amd64.yml" },
"target": "hello-http-image",
"config": "config.json",
"args": "args.json"
}
}build key | Meaning |
|---|---|
kind | yocto |
kas | The kas file: one path for every architecture, or an object by architecture. |
target | The image recipe kas builds (bitbake <target>). |
config | The container's image config (Entrypoint, Cmd, Env, WorkingDir, User, Volumes, ExposedPorts): a JSON file of the repository, or the object inline. |
args | A JSON file with the app's Pantavisor args (PV_GROUP, PV_ROLES, …). |
image | The build environment image. Leave it out for the pinned Pantacor kas image. |
A layout that works:
text
hello-http/
├── pvpkg.json
├── kas-arm64.yml # kas config for arm64v8
├── kas-amd64.yml # kas config for amd64
├── config.json # {"Entrypoint": ["/usr/bin/hello-http"], …}
├── args.json # {"PV_GROUP": "app"}
├── meta-hello/ # your layer: conf/layer.conf, recipes-*/
├── app/ # the program's source, used by the layer's recipe
└── pvpkg/generic/… # optional overlayThe kas file lists your layer as the repository itself (a repo entry with no url, like hello: with layers: meta-hello:), plus poky, meta-openembedded, meta-virtualization and meta-pantavisor. The builder adds IMAGE_FSTYPES += "tar.gz" for the target and takes the image's rootfs tarball.
Start from meta-pantavisor CI to build in minutes
Runners serve the shared state cache (sstate) and downloads of meta-pantavisor's CI read-only. A build whose kas configuration matches a CI build — same machine, distro, layer commits and local.conf — reuses nearly everything, and only your layer's recipes compile: a few minutes instead of the hour or more a cold build needs for its toolchain.
To get there, copy the CI configuration your container targets (for example kas/build-configs/release/containers/docker-armv8-scarthgap.yaml in meta-pantavisor) into your kas file, keep its machine, distro, layer commits and local_conf_header as they are, and only add your layer and change target. The build's log prints the Sstate summary; aim for a match above 95%. Common layers (poky, meta-openembedded, meta-virtualization, meta-pantavisor, meta-arm) are also mirrored next to the runners, so kas does not clone them from upstream in every build.
Private layers and sources
A recipe that fetches from a private repository (a private layer in the kas file, or a SRC_URI like git://github.com/acme/sdk;protocol=https) needs the account's git secret named in the build — see secrets in recipes.
Buildroot
A Buildroot package builds a defconfig of your repository and takes output/images/rootfs.tar:
json
{
"name": "tiny-app",
"arch": ["arm64v8"],
"build": {
"kind": "buildroot",
"ref": "2025.02.6",
"defconfig": "configs/tiny_arm64_defconfig",
"external": "br2-external",
"config": { "Entrypoint": ["/usr/bin/tiny-app"] }
}
}build key | Meaning |
|---|---|
kind | buildroot |
ref | The Buildroot tag or branch (required), such as 2025.02.6. |
buildroot | The Buildroot repository (HTTPS). Default: https://gitlab.com/buildroot.org/buildroot.git. |
defconfig | The defconfig file: one path, or an object by architecture. |
external | A BR2_EXTERNAL tree in the repository. |
config, args, image | As for Yocto. |
The defconfig must enable a tar root filesystem (BR2_TARGET_ROOTFS_TAR=y); the builder enables it if missing. A change to the defconfig starts from a clean output directory.
Check before you push
- Every path in
pvpkg.jsonis relative and inside the repository (no.., no absolute paths). archcontains every architecture you will request.- The kas files and defconfig exist at the commit you build — the builder never sees uncommitted files.
- For a private repository, a git secret covers its host and path.