A configurable, profile-driven development container image and tooling built around a
modular feature system. solen composes reusable features (small install scripts +
metadata) into hierarchical profiles and generates a multi-stage Dockerfile plus a
docker-bake.hcl for multi-architecture builds.
Quick links
| What | Where |
|---|---|
| GitHub repo | ebpro/jupyter-base (the image and CLI are named solen) |
| Image registry | ghcr.io/ebpro/solen |
| Generated Dockerfile | generated/Dockerfile |
| Bake definition | generated/docker-bake.hcl |
| Profile matrix | profiles/matrix/*.yaml → generated/profiles/ |
| Features | features/ |
| Version source of truth | versions/versions.yaml |
| Flat build manifest | versions.json (generated) |
| CLI | solen-cli/ (pip install -e solen-cli) |
docker pull ghcr.io/ebpro/solen:<profile>-<tag>
docker run --rm -it ghcr.io/ebpro/solen:<profile>-<tag> bashTags follow <profile-slug>-<tag> (e.g. data-science-<sha>, minimal-latest,
quarto-full-v<version>). On releases the base tag is a multi-arch manifest list;
single-arch images are tagged <profile-slug>-<tag>-amd64 / -arm64. List profiles
with solen list profiles.
As a devcontainer:
{
"name": "solen",
"image": "ghcr.io/ebpro/solen:data-science-latest",
"workspaceFolder": "/home/jovyan/workspace"
}generated/devcontainer/ holds per-profile devcontainer.json (and docker-compose.yml
where a profile defines services), generated by solen generate devcontainer --all.
- Feature — a folder under
features/<name>withfeature.json(metadata, options,dependsOn) and an idempotentinstall.sh. Helpers are prebaked into the image at/opt/solen/_lib(FEATURE_HELPERS_DIR) and resolve versions/checksums centrally. - Profile — a named feature set defined in
profiles/matrix/*.yamland expanded bysolen generate profiles ... --chainintogenerated/profiles/<name>(chained profiles inherit their parents). - Versions —
versions/versions.yamlis the single source of truth (pinned versions- upstream sources).
solen versions syncflattens it intoversions.json, the manifest consumed at build time;solen versions sync --check(CI) fails on drift. Per-arch release checksums live inchecksums.json(tools.<name>.checksums.<version>.<arch>).
- upstream sources).
- opencode feature — installs the OpenCode AI coding agent (
opencode-ai, global npm, version-pinned) and, whenSSH_PRIVATE_KEYis supplied at build time, bakes the privateebpro/opencode-configrepo into the dev user's~/.config/opencode/.dependsOn: node. Seefeatures/opencode/.
python -m venv .venv && . .venv/bin/activate
pip install -e solen-cli
solen generate profiles --matrix profiles/matrix --out generated/profiles --chain
solen generate dockerfile --all --output generated/Dockerfile
solen generate bake --output generated/docker-bake.hcl
# or simply:
./build.sh data-science # positional profile; flags: --push --load --no-cache --multi-archbuild.sh runs the generation steps and then docker buildx bake final-<profile>
(solen build for a single profile). On machines without buildx (e.g. podman-only),
fall back to podman build -f generated/Dockerfile --target final-<profile> ..
Optional: pre-download tool binaries (kubectl, helm, gh, quarto, …) into artefacts/
with scripts/prebake-toolcache.sh for faster offline-ish builds.
Large tool archives can be fetched once, verified, and reused across builds:
scripts/utils/fetch-artefacts.sh— download + sha256-verify an offloaded tarball intoartefacts/(env:ARTEFACTS_BASE_URL,ARTEFACT_SHA256, or--manifest).scripts/utils/offload-artefacts.sh— tar upartefacts/+ writeartefacts/offload/toolcache-manifest-<ts>.json.scripts/validate-artefacts.sh— verify manifests, tarball checksums, andchecksums.jsonvalidity (run by theValidate Artefactsworkflow).
At build time the Dockerfile copies artefacts/ to /opt/solen/artefacts and the feature
helpers look for versions.json/checksums.json in the standard search order
(/tmp/… during the build stage, /opt/solen/artefacts/… in the final image).
Workflows live in .github/workflows/ and run on the org's in-cluster GitHub Actions
runner (actions-runner-controller runner set ebpro-org, with a docker:dind sidecar so
docker buildx works as usual):
| Workflow | Trigger | What it does |
|---|---|---|
ci-validate.yml |
PR / push | solen validate features, profile + devcontainer generation, solen versions sync --check, pytest, ruff |
ci-build.yml |
PR / push to develop / dispatch |
Single-arch build of selected profiles (default quarto-full), Syft SBOM + Trivy scan |
ci-publish.yml |
push to develop / tag v* / dispatch |
Multi-arch publish to GHCR (5 jobs: native per-arch builds on ebpro-org/ebpro-org-large/ebpro-org-arm, Harbor registry cache, OCI-index merge, cosign sign) |
validate-artefacts.yml |
artefacts/** changes |
Run scripts/validate-artefacts.sh |
cleanup-ghcr.yml |
schedule | GC old GHCR tags |
ci-publish.yml runs 5 jobs across 3 in-cluster ARC pools — ebpro-org (standard
amd64), ebpro-org-large (heavy/flagship amd64: quarto-full, lang-java-*,
quarto-java-*), and ebpro-org-arm (all profiles, arm64). The three build jobs
(build-amd64-core, build-amd64, build-arm64) each bake natively on a host of their
own architecture (no QEMU/binfmt) against a shared Harbor registry cache
(harbor.ebruno.fr/solen/build-cache:<profile>-<arch>, robot robot$arc-runner-ci). On
a v* tag or manual dispatch every tag is suffixed per-arch (-amd64/-arm64), then
merge-index folds them into a multi-arch manifest list at the base tag and
sign-release cosign-signs the merged indexes (release only). On a develop push the
fast path is amd64-only with unsuffixed tags.
- Add a feature: create
features/<name>/feature.json+ idempotentinstall.sh, then validate:solen validate features. - Add a profile: edit a
profiles/matrix/*.yaml(or add one) and regenerate withsolen generate profiles --matrix profiles/matrix --out generated/profiles --chain. - Pin a version: edit
versions/versions.yaml, runsolen versions sync, and updatechecksums.jsonviascripts/utils/update-checksums.shwhen adding releases. - Check for drift:
solen versions sync --check,solen validate features,python -m pytest tests/,ruff check solen-cli.
Best practices: keep install.sh small and idempotent; prefer system package managers
where sensible; route version lookups through the shared helpers (fh_resolve_version,
fh_resolve_checksum) instead of hardcoding.
solen list profiles # available generated profiles
solen inspect-profile data-science # resolved features/options/services
solen generate devcontainer --all # per-profile devcontainer.json
solen analyze features --output generated/feature-matrix.md
solen validate propagate-versions # feature.json options vs versions.yaml
docker buildx bake --file generated/docker-bake.hcl --print