Container images¶
People who only want to run yapnr need Docker and nothing else: the image bundles KiCad 10
(kicad-cli and the pcbnew Python module), CPython 3.11 with numpy and CPU-only torch, and yapnr
itself. Bazel is only for contributors (see DEVELOPERS.md).
Image |
What |
|---|---|
|
the yapnr command line, on top of the KiCad base |
|
Ubuntu 24.04 with KiCad 10 from the KiCad team’s PPA, headless |
Both are built natively for linux/amd64 and linux/arm64 (Apple Silicon runs the arm64 image without emulation), run as an unprivileged user (UID 1000), and carry an SBOM, build provenance and a GitHub attestation.
Status: the images are built and smoke-tested on every pull request that changes what goes into them, and
edgeis published frommainwhenever a merge changes it. The engine commands (init,import,run,export, the viewer) arrive with the migration pull requests; until then the image runsyapnr --versionandyapnr doctor. The examples below that need those commands are marked as planned.
Quick start¶
IMG=ghcr.io/studio-fug/yapnr:edge # a release tag such as 0.1 once one exists
docker run --rm "$IMG" doctor # the toolchain report (the default command)
docker run --rm "$IMG" --version
doctor prints the yapnr version and the source revision the image was built from, the Python,
numpy and torch versions, and the KiCad CLI and KiCad Python the image is configured with.
Working on a project¶
The project directory is bind-mounted at /project, the image’s working directory. On Linux,
run as your own user so that new files belong to you:
docker run --rm -u "$(id -u):$(id -g)" -v "$PWD":/project "$IMG" doctor
Planned (the commands arrive with PR3d to PR3f; the store defaults to /project/.yapnr):
U="$(id -u):$(id -g)"
docker run --rm -u "$U" -v "$PWD":/project "$IMG" \
init --frontend kicad --from-kicad design/board.kicad_pro
docker run --rm -u "$U" -v "$PWD":/project "$IMG" import kicad design/board.kicad_pro
# A long run in its own container: detached by Docker, in the foreground inside it.
docker run -d --name yapnr-run -u "$U" -v "$PWD":/project \
--cpus 6 --memory 12g --shm-size 1g --pids-limit 4096 -e OMP_NUM_THREADS=6 \
--stop-timeout 120 "$IMG" run experiments/default.toml
docker logs -f yapnr-run
docker exec yapnr-run yapnr stop <run-id> # in the container that owns the run
# The best candidate as a standalone KiCad project, with its DRC report.
docker run --rm -u "$U" -v "$PWD":/project "$IMG" export kicad <run-id>:best -o out/board
yapnr stop identifies processes by PID and start time, so it must run in the PID namespace of the
run: use docker exec into the container that owns it.
The viewer (planned, PR4a)¶
The viewer serves the project store over HTTP on port 8781. It has no authentication: publish it on
the loopback interface only, and use an SSH tunnel (ssh -L 8781:127.0.0.1:8781 <host>) or an
authenticating reverse proxy for remote access. Never publish the port on all host interfaces.
docker run -d --name yapnr-viewer -u "$U" -v "$PWD":/project \
-p 127.0.0.1:8781:8781 "$IMG" viewer --listen 0.0.0.0 --port 8781
docker exec yapnr-viewer yapnr run --detach experiments/default.toml # controls see this run
Docker Compose¶
A compose.yaml next to the project (planned services marked; doctor works today):
# YAPNR_UID=$(id -u) YAPNR_GID=$(id -g) docker compose run --rm doctor
# YAPNR_UID=$(id -u) YAPNR_GID=$(id -g) docker compose up -d viewer (planned, PR4a)
# docker compose exec viewer yapnr run --detach experiments/default.toml
name: yapnr
x-yapnr: &yapnr
image: ghcr.io/studio-fug/yapnr:${YAPNR_TAG:-edge}
user: '${YAPNR_UID:-1000}:${YAPNR_GID:-1000}'
volumes: ['./:/project']
services:
doctor:
<<: *yapnr
profiles: [tools]
command: [doctor]
viewer:
<<: *yapnr
command: [viewer, --listen, 0.0.0.0, --port, '8781']
ports: ['127.0.0.1:8781:8781']
shm_size: 1g
stop_grace_period: 2m
restart: unless-stopped
healthcheck:
test:
[
'CMD',
'python',
'-c',
"import urllib.request as u; u.urlopen('http://127.0.0.1:8781/', timeout=3)",
]
interval: 30s
batch:
<<: *yapnr
profiles: [batch]
command: [run, experiments/default.toml]
shm_size: 1g
cpus: 6
mem_limit: 12g
pids_limit: 4096
environment: { OMP_NUM_THREADS: '6' }
stop_grace_period: 2m
Compose cannot see the shell’s UID and GID (they are not exported, and UID is read-only in
bash), hence YAPNR_UID and YAPNR_GID; a .env file next to compose.yaml works as well. The
image defines no HEALTHCHECK, because it would mark every run or doctor container unhealthy;
the viewer service brings its own.
Users, file ownership and permissions¶
The image runs as
yapnr, UID/GID 1000, withHOME=/var/lib/yapnr. Nothing needs root, and there is nosudo.Any UID works. The entrypoint goes through
yapnr-kicad-env, which gives a UID without a writableHOMEa private one under/tmpand seeds KiCad’s global library tables there. The same holds fordocker exec <container> yapnr ...:/usr/local/bin/yapnris the wrapper.Linux hosts: pass
-u "$(id -u):$(id -g)"so that files written to the project belong to you rather than to UID 1000.macOS: Docker Desktop and OrbStack map ownership through their file sharing: files the container writes belong to your macOS user, and
-uis not needed. The same holds for colima with virtiofs mounts (colima start --vm-type vz --mount-type virtiofs; checked with colima on 2026-09-29: containers running as UID 1000 and 4242 both wrote to a project owned by the macOS user, and the files belonged to that user). colima’s sshfs and 9p mounts behave differently: there, write a test file first, and pass-u "$(id -u):$(id -g)"if it fails or lands with the wrong owner.Rootless Podman:
--userns=keep-id. SELinux hosts: add:Zto the bind mount (-v "$PWD":/project:Z).Read-only root file system: supported:
--read-only --tmpfs /tmp.
Resources¶
Shared memory. torch and the worker pools use
/dev/shm, and Docker’s default is 64 MB: pass--shm-size 1gfor real runs.CPU and memory. Limit a run with
--cpusand--memory, and setOMP_NUM_THREADSto the same CPU count so torch does not oversubscribe.--pids-limit 4096bounds the worker pools.Stopping.
tiniis PID 1 and forwards signals, sodocker stopasks yapnr to stop gracefully. Give it time:--stop-timeout(or compose’sstop_grace_period) at least as long as the longest worker timeout.Store size. A project store can grow large. On macOS, bind mounts are slower than Docker volumes: for heavy runs, set
YAPNR_STORE=/storewith a named volume mounted there, and move results in and out withyapnr packandyapnr unpack(planned).Nothing GUI-bound. There is no X or Wayland session and
DISPLAYis never set;kicad-cliandpcbnewrun headless.
Apple Silicon¶
Use the native arm64 image (the default on an Apple Silicon host). It runs under Docker Desktop, colima, OrbStack or Podman without emulation.
An amd64-only image, such as the official
kicad/kicadimage (its9.0and10.0tags are amd64-only, checked 2026-09-29; the planned CI KiCad lane of PR6a runs it), needs emulation: enable Rosetta (Docker Desktop: on by default from macOS 14.1; colima:colima start --vm-type vz --vz-rosetta). Rosetta costs about 20%; QEMU without it was measured six to eight times slower on KiCad work.Docker Desktop and colima run a VM with a fixed memory size: give it more than the run’s
--memoryplus the viewer. The Mac itself must stay awake during long runs (for examplecaffeinate -iaround the session); keep-awake inside the container does nothing.Windows: Docker Desktop with WSL2 runs the linux/amd64 image. There is no native Windows support.
What is inside¶
Path |
What |
|---|---|
|
the command line (the entrypoint, through |
|
the controller: CPython 3.11 with the runtime lock (numpy, torch, …) |
|
that CPython, from python-build-standalone (installed by uv) |
|
KiCad’s command line |
|
KiCad’s Python 3.12 with |
|
footprints, symbols and templates (no 3D models) |
|
the global library tables every user starts from |
|
|
|
the working directory; mount the project here |
The controller Python and KiCad’s Python are separate on purpose: the controller runs Bazel’s
exact interpreter and wheel versions (the runtime locks are pinned to requirements.lock), and
KiCad-side code runs under KiCad’s own Python without torch. (KiCad’s Python cannot import
yapnr from /opt/venv yet; the KiCad-side workers of PR6a get their own import path, with only
yapnr’s pure-Python modules on it, and a smoke check.) /opt/venv/bin comes before
/usr/bin on PATH, so a plain python3 in the image is the controller; yapnr finds KiCad through
these environment variables, which the image sets:
Variable |
Value |
|---|---|
|
|
|
|
|
the yapnr commit the image was built from |
|
the stock libraries under |
3D models are not included (kicad-packages3d is 3.3 GB installed). For
yapnr export kicad --with-3d or GLB export, mount a host 3D library read-only:
-v /path/to/3dmodels:/usr/share/kicad/3dmodels:ro.
Sizes (linux/arm64): the KiCad base is about 1.6 GB unpacked, a 390 MB download; the yapnr image adds about 0.67 GB (CPython 0.12 GB, the runtime 0.55 GB), 2.3 GB unpacked and a 570 MB download in all. On linux/amd64 the CPU torch build is larger.
Verifying an image¶
gh attestation verify oci://ghcr.io/studio-fug/yapnr:0.1.0 -R Studio-Fug/yapnr
docker buildx imagetools inspect ghcr.io/studio-fug/yapnr:0.1.0 --format '{{json .SBOM}}'
docker buildx imagetools inspect ghcr.io/studio-fug/yapnr:0.1.0 --format '{{json .Provenance}}'
Licenses and source¶
yapnr is AGPL-3.0-or-later; the image records its exact source (YAPNR_SOURCE_REVISION, the
org.opencontainers.image.revision label), and release source archives are attached to each GitHub
release. KiCad is GPL-3.0-or-later and its libraries are CC-BY-SA-4.0 with the KiCad library
exception; the image label org.opencontainers.image.licenses names the three.
THIRD_PARTY.md lists everything the images redistribute, including the native
libraries inside the numpy and torch wheels and the libraries in the bundled CPython;
/usr/share/doc/yapnr/SOURCES in the image says where each part’s source is,
/usr/share/doc/yapnr/licenses/ holds the license texts that are not elsewhere in the image, and
/usr/share/doc/<package>/copyright holds the Ubuntu packages’ license texts.
The KiCad team’s PPA deletes superseded builds, so pointing at it would not keep the corresponding source available. Every published base tag therefore has a companion image with the complete source packages of its KiCad build, for linux/amd64 and linux/arm64, which is never deleted. CI publishes it before the base tag, and a later run publishes it if it is ever missing:
docker create --name kicad-src ghcr.io/studio-fug/yapnr-kicad:10.0.6-1-src none
docker cp kicad-src:/src ./kicad-src && docker rm kicad-src
SOURCES also names Launchpad’s snapshot of the PPA at the build time and the source files on
Launchpad. The Ubuntu packages come from the archive state recorded at build time in
/etc/yapnr/ubuntu-snapshot (a snapshot.ubuntu.com ID, taken right
after apt-get update, with every package upgraded to that state); SOURCES lists the exact source
package versions, each of which Launchpad keeps at
https://launchpad.net/ubuntu/+source/<package>/<version>:
docker run --rm --entrypoint cat ghcr.io/studio-fug/yapnr-kicad:10.0.6-1 /usr/share/doc/yapnr/SOURCES
Building the images locally¶
Contributors with Docker and Bazel can build both images for their own architecture, load them into Docker and smoke-test them:
tools/image/build_local.sh # yapnr-kicad:local, then yapnr:local
tools/image/build_local.sh --base ghcr.io/studio-fug/yapnr-kicad:10.0.6-1 # reuse the base
Each image is a few GB; remove them afterwards (docker image rm yapnr:local yapnr-kicad:local).
The pieces:
docker/yapnr-kicad/Dockerfile: the base. Its tag isdocker/yapnr-kicad/TAG; any change to the directory needs a new tag. CI refuses to overwrite a published one: every run comparestools/image/base_context.shwith the published base’sio.github.studio-fug.yapnr.kicad.contextlabel.docker/yapnr/Dockerfile: the application image. The yapnr wheel comes from Bazel (bazel build //release:wheel.dist --stamp --embed_label=<version>) through the named build contextdist.docker/yapnr/runtime-{arm64,amd64}.lock: the hashed runtime locks, regenerated withtools/image/update_runtime_locks.shafter a runtime pin changes inrequirements.lock(tests/unit/repo/test_images.pychecks they agree).tools/image/smoke_image.sh: the smoke test CI runs on both architectures:doctor --json(Python 3.11, the numpy and torch pins, KiCad configured, the source revision),kicad-cli version, andpcbnewloading a footprint through the seeded library table, saving a board and runningkicad-cli pcb drcon it; as UID 1000, as an arbitrary UID, and with a read-only root file system; and the notices, license texts and recorded Ubuntu snapshot.
CI: .github/workflows/image.yaml (see releases).