The image contract
para owns no base image — you build your own. para up launches whatever PARA_IMAGE names, so the image is where your toolchain, package installs, and pre-pulled Docker images get baked in once instead of on every up.
What the image must have
para's own mechanism needs very little of the image:
- a uid-1000 user (
$PARA_USER,appby default) —pararuns hooks,para sh, andpara runas it, and every file it pushes is chowned to it; - bash —
parainvokes hooks and shells viasu -s /bin/bash; - git, if your hooks clone (the bundled templates' do;
paraitself never runs git).
Everything else is your project's choice. If your stack is Docker Compose — as the bundled templates' is — then the image also needs docker with nesting resolving to overlayfs (a dir/ext4 Incus pool; btrfs/zfs<2.2 silently falls back to the slow vfs driver) and that uid-1000 user in the docker group. para doesn't know or check that; the templates' image-build.sh verifies it for you, because the payload that installs Docker is what should refuse to publish a half-broken image.
For full ergonomics also include tmux (para run), a login shell like zsh (para sh), and claude/nvim/gh — para degrades rather than breaks without them.
Building with para image-build
para image-build is base-agnostic plumbing. It:
- launches a builder from
$PARA_BASE_IMAGE(any Incus image —images:debian/13,images:voidlinux,images:alpine/edge, …) withsecurity.nesting=true; - runs
$PARA_IMAGE_BOOTSTRAPin it viash -c, if set; - runs your project's
.paraspace/image-build.shin it viabash -s, as root, with$PARA_USER/$PARA_UID/$PARA_GIDand$PARA_PREPULL_IMAGES(the stack's external image tags, scraped from yourdocker-compose.yml/Dockerfile) in the environment; - publishes the result as
$PARA_IMAGE.
The bootstrap step exists because step 3 needs bash and step 2 is the only one guaranteed to run (sh is in every base image). Use it to install bash and/or refresh the package index — xbps-install -Syu xbps bash on Void, apk add --no-cache bash on Alpine, apt-get update on Debian. A base that already ships what your payload needs doesn't need it at all; leave it unset.
Both keys live in the Parafile. PARA_BASE_IMAGE has no default — the distro is entirely the project's call, so para asks rather than picks, and a para update can never swap the ground your image is built on.
- Images are per-arch — build on the machine that runs them (arm64 on Apple Silicon).
-i/--from-currentlayers onto the existing$PARA_IMAGEinstead of a pristine base (and skips the bootstrap) — a fast iterative rebuild while you tuneimage-build.sh. It assumes your payload is idempotent.-q/-vforce or keep the Incus progress bars (auto-quiet when not a tty).
The templates' image-build.sh scripts are the reference: a package list, a user, and Docker set up for nesting.