Images
This repo builds two images and publishes neither. Everything else is pulled.
Built here — local tags only
| Image | Built from | Used by |
|---|---|---|
neops-lab-frr:latest |
devices/frr/ (Dockerfile + entrypoint.sh only) |
containerlab, as the image for every kind: linux node |
neops-lab-bootstrap:latest |
bootstrap/ |
the lab_bootstrap compose service |
Nothing is pushed to a registry
Both images are local tags only. Every machine that runs the lab builds
them itself. There is no quay.io/zebbra/neops-lab-*, and there is no
publish pipeline in this repo.
docker-compose.worker.yml pins lab_bootstrap to the exact tag
neops-lab-bootstrap:latest and declares build: ./bootstrap, so the
compose build and make build-docker cannot drift apart.
make build-docker is a prerequisite of make local-lab-up, so in normal use you never call it directly. CI runs it as a separate job (on the hetzner runner) purely so a broken Dockerfile fails in CI instead of on a developer’s first lab bring-up.
neops-lab-frr
frrouting/frr:v8.4.1 plus an SSH server and an frr / frr login, because discovery reaches devices over SSH:
opensshwithPasswordAuthentication yesandPermitRootLogin no- the
frruser gets a home directory, a shell, and membership infrrvty frr.confanddaemonsare not baked in — they are scenario data, mounted at/laband installed by the entrypoint- the entrypoint copies
/lab/frr.confand/lab/daemonsinto/etc/frr/, rewrites the hostname, fixes ownership, seeds/etc/machine-id, startssshd, then hands off to FRR’s owndocker-start
The FRR image needs NET_ADMIN and SYS_ADMIN
The FRR binary refuses to start without cap_sys_admin, even with no VRFs
configured. containerlab grants both capabilities to kind: linux nodes,
which is why this works under containerlab deploy — if you ever run the
image by hand with plain docker run, you must add them yourself.
No /lab mount, no boot
The entrypoint exits non-zero with /lab/frr.conf is not mounted rather
than starting FRR on whatever happens to be in /etc/frr. A silently wrong
routing config costs far more to debug than a container that refuses to
start. Both flavours mount it: containerlab binds the three files from
generated/<scenario>/scenario/devices/frr/, and the Kubernetes flavour
carries them in its ConfigMap.
One image serves every scenario. Because nothing is baked, a scenario that needs a different FRR baseline — bgpd instead of ospfd, say — ships a devices/frr/daemons or devices/frr/frr.conf delta in its own directory. There is no second image to build and no second tag to pin. See Scenarios.
Interface descriptions are not baked in either: containerlab execs /lab/set-aliases.sh after the links are wired, which sets each interface’s Linux alias from the generated .iface file. The script comes from scenarios/_base/devices/frr/set-aliases.sh. See Topology as source of truth.
neops-lab-bootstrap
A python:3.12-slim image with pyyaml and requests, whose entrypoint is register.py. It runs once per docker compose up, publishes every workflow document in the resolved scenario (generated/<scenario>/scenario/workflows/, sourced from scenarios/_base/workflows/ unless the scenario overrides or adds one) — *.yaml, *.yml and *.json alike — through the engine’s POST /workflow-definition/publish, and exits. Publishing is idempotent: an unchanged document answers 200 unchanged, which is the normal outcome of re-running make local-lab-up. Published content is immutable, so editing a workflow without bumping its version answers 409 and fails the target — bump majorVersion/minorVersion/patchVersion instead. Engines older than 068753a0 have no publish route; register.py falls back to the legacy POST /workflow-definition on a 404. docker compose wait lab_bootstrap in local-lab-up blocks on that exit and propagates the code.
Pulled — the NeOps control plane
| Service | Default image | Override with |
|---|---|---|
cms |
quay.io/zebbra/neops-cms-free:develop |
NEOPS_CMS_IMAGE |
workflow_engine, workflow-engine-client |
quay.io/zebbra/neops-workflow-engine-preview:${NEOPS_ENGINE_TAG:-develop} — the developer preview, public |
NEOPS_ENGINE_TAG (tag) or NEOPS_WORKFLOW_ENGINE_IMAGE (image — e.g. the full licensed quay.io/zebbra/neops-workflow-engine:develop) |
web_client |
quay.io/zebbra/neops-web-client:develop |
NEOPS_WEB_CLIENT_IMAGE |
worker |
quay.io/zebbra/neops-worker-sdk:develop — carries the base function blocks |
NEOPS_WORKER_SDK_IMAGE |
The CMS, the web client, the worker and the developer-preview engine are all public, so the default lab pulls with no docker login. The full licensed engine (quay.io/zebbra/neops-workflow-engine) is the one exception.
The make targets pull the published tags with --policy always; the services’ pull_policy: missing would otherwise skip images already on disk. The default develop is republished on every merge to the engine’s develop branch and never expires, so make local-env-up is all it takes to move forward. 0.42.2-beta.3 remains the oldest engine tag the lab supports (raised body limits + the publish route).
Do not pin the preview’s latest
The preview’s latest is 0.42.1 — below the oldest engine the lab supports — because latest only advances on non-prerelease releases. Prerelease tags carry a quay.expires-after=20d label and vanish — 0.42.2-beta.3 did, on 2026-09-07 — so a pinned beta stops resolving without warning; develop carries no such label.
Plus third-party images that need no credentials: postgres:15-alpine, redis:7-alpine, docker.elastic.co/elasticsearch/elasticsearch:8.9.2, busybox, and ghcr.io/nokia/srlinux:26.3 for the SR Linux devices.
Running a locally-built image
Edit .env (docker compose reads it automatically; make local-lab-up creates it from .env.example if missing) or export the variable:
Each overridable service also sets pull_policy: ${NEOPS_*_PULL_POLICY:-missing}. missing means an image already present locally is used as-is, with no registry call — which is exactly what makes a local tag work.
docker compose pull warns on a local tag
A tag with no registry host resolves as docker.io/library/…, which does
not exist. The explicit pulls in local-env-init, local-env-up and
local-lab-up run with --ignore-pull-failures, so an override like this
is a warning; up -d uses the local image (pull_policy: missing).
Why you would
- Worker SDK — to exercise a function block before it is released. The lab
depends on
fb.base.neops.io/global_discover_network:0.1.0shipping inside the worker image rather than being bind-mounted from here, so a block you are editing reaches the lab only through a rebuilt image:make -C ../neops-worker-sdk-py build-docker, then pointNEOPS_WORKER_SDK_IMAGEatneops-worker-sdk:latest. - Workflow engine — discovery emits a few hundred
Interfacerows in one job result, so it needs an engine with the large-payload and reference-resolution fixes. If the publisheddeveloptag lags, run a local build. - Web client — to preview UI changes against a populated CMS.
Verifying what you are running
export COMPOSE_FILE=docker-compose.yml:docker-compose.worker.yml
export SCENARIO=wan-and-fabric # compose refuses to interpolate without it
docker compose images # image + tag per service
docker compose exec worker ls /app/neops/fb # base function blocks in the image
docker compose exec worker ls /app/lab # this repo, through the read-only mount
docker compose exec worker ls /app/lab/generated/wan-and-fabric/scenario # the resolved scenario

