Install
- What you need
- Which image
- One-shot commands
- Compose, for anything that stays up
- Volumes
- Verify it worked
- Upgrading
- Downgrading
- Without Docker
What you need
Docker Engine 20.10 or newer. docker compose (the v2 subcommand, not the old
docker-compose script) if you want the web interface or a background worker.
Nothing else. No Python on the host, no database server, no checkout.
Which image
ghcr.io/oscarvalenzuelab/crane:latest # no validator bundled
ghcr.io/oscarvalenzuelab/crane:X.Y.Z # pinned
ghcr.io/oscarvalenzuelab/crane:ossbomer # ossbomer baked in
ghcr.io/oscarvalenzuelab/crane:X.Y.Z-ossbomer # pinned, ossbomer baked in
The same four tags are published to Docker Hub under ovalenzuela/crane, from the
same build. Either registry pulls the identical image; the examples here use ghcr.io.
docker pull ovalenzuela/crane:latest
Both variants are built for linux/amd64 and linux/arm64, and no release is pushed with
a fixable HIGH or CRITICAL vulnerability in it — the scan runs before the push, so a tag
that exists is a tag that passed.
Two variants, because the validator question has no single answer. SBOM conformance is
checked by ossbomer, a separate tool. The plain
image runs everything except validations, which fail saying the executable was not found.
That is recorded as a coverage gap rather than a compliance verdict, and nothing is scored
as failing because a tool was never installed. The -ossbomer variant carries it, which
is the right default if you are measuring against the CRA or NTIA profiles.
Pin a version for anything you depend on. latest moves.
The version reported by
crane --versionand by/healthzis the version on the image label. The release workflow refuses to publish when the tag,pyproject.tomlandcrane.__version__disagree, so those three cannot drift apart.
One-shot commands
Every CRAne command is the image’s entrypoint, so the container name is followed directly by the command:
mkdir -p data
docker run --rm -v "$PWD/data:/data" --user "$(id -u):$(id -g)" \
ghcr.io/oscarvalenzuelab/crane:latest init
Three things about that line matter, and skipping any of them produces an error that looks like a bug and is not:
The directory has to exist first. Docker creates a missing bind mount source as root. The container is not root, so its first write fails.
--user has to be yours. The image runs as uid 10001. A bind mount keeps the host’s
ownership, so without this nothing under data/ is writable. On Docker Desktop for macOS
the file sharing layer papers over this and you can leave --user off; on Linux you
cannot.
Anything CRAne reads must be under a mount it can see. A path that exists on your host does not exist inside the container. This is the single most common mistake and it has its own section.
Shorten the repetition with a shell function:
crane() {
docker run --rm -i -v "$PWD/data:/data" -v "$PWD/exports:/exports" \
--user "$(id -u):$(id -g)" ghcr.io/oscarvalenzuelab/crane:latest "$@"
}
crane status
crane assess cra --json
Compose, for anything that stays up
The web interface, a background worker, and any deployment you do not want to retype need compose. The file is published alongside these docs and refers to the image by name, so there is still nothing to check out and nothing to build:
curl -O https://oscarvalenzuelab.github.io/CRAne/assets/compose.yaml
mkdir -p data exports runs report-templates
export CRANE_UID=$(id -u) CRANE_GID=$(id -g)
docker compose run --rm crane init
docker compose run --rm crane status
Then bring up the interface:
docker compose --profile ui up
http://127.0.0.1:8000. The port is mapped to 127.0.0.1 on purpose, so by default
nothing outside this machine can reach it. Publishing it further is a deliberate act with
a token attached, covered in Configure.
The compose file defines three services, each behind a profile so that bringing up one does not start the others:
| Service | Profile | What it is |
|---|---|---|
crane |
cli |
One-shot commands. docker compose run enables its profile for you |
ui |
ui |
The interface and the API, with a worker in the same process |
worker |
worker |
A worker on its own, for when the interface runs elsewhere |
Running a worker inside the UI process and a second one as its own service at the same time is safe. A claimed job is leased, so two workers cannot pick up the same job.
Volumes
| Mount | Mode | Holds |
|---|---|---|
/data |
read write | The SQLite database, and uploaded evidence under /data/uploads |
/exports |
read write | Generated reports and evidence packages |
/runs |
read write | One directory per executed tool run: stdout, stderr, artifacts, manifest. Plus objects/, where each artifact is stored once under its own hash and linked into the runs that produced it — so this must be one filesystem, not several mounts stitched together |
/report-templates |
read only | Your own report templates, if you write any |
/sources |
read only | Anything you are importing from |
Only /data is required. The rest are needed by the features that write to them, and
CRAne says so rather than failing obscurely when one is absent.
Mount /sources read only, and not by convention: let Docker enforce it. Importers also
open source databases with SQLite’s mode=ro, so there are two independent barriers
between an importer bug and the system it is reading.
Verify it worked
docker run --rm -v "$PWD/data:/data" --user "$(id -u):$(id -g)" \
ghcr.io/oscarvalenzuelab/crane:latest status
You should see the database path, 18 migration(s) or more, pending: 0, and a table of
row counts that are all zero on a fresh install. If pending is not zero, run migrate.
Upgrading
The database is migrated forward in place, so an upgrade is a new image and one command:
docker pull ghcr.io/oscarvalenzuelab/crane:latest
docker run --rm -v "$PWD/data:/data" --user "$(id -u):$(id -g)" \
ghcr.io/oscarvalenzuelab/crane:latest migrate --dry-run # what would run
docker run --rm -v "$PWD/data:/data" --user "$(id -u):$(id -g)" \
ghcr.io/oscarvalenzuelab/crane:latest migrate
Applied migrations are immutable and recorded with a checksum. A build that tried to change one already applied is refused by the runner rather than silently reapplied.
Back up first. The database is one file, so a backup is a copy of it:
cp data/crane.db "data/crane.db.$(date +%F)"
There is no automatic backup. That is not covered yet.
There is a retention policy, and crane job prune applies it: run directories, ledger
rows past their window, stored objects nothing refers to, and a VACUUM so the file
actually shrinks. Run it on a schedule. --dry-run prints the plan and removes nothing.
The windows, and the rule that keeps a run row that is provenance for a claim however old
it is, are in interfaces.
Upgrading past 0.11.1: identifiers changed
CRAne derives an identifier for a row by joining the parts that describe it. That join used
a bare : and escaped nothing, so a part that itself contained a colon could produce the
identifier of a different arrangement of parts — two different facts sharing one identity.
The delimiter is now escaped.
Most identifiers do not move. The escape leaves any value without a colon in it exactly
as it was, so an id derived from a slug, a numeric key or a table name is unchanged. What
changes are ids derived from a URL, a package URL, a registry/image:tag, a timestamp, or
another key — measured on the example portfolio, about 40% of derivations.
A row keyed the old way cannot be found the new way, so importing the same source into a
database built before this would insert beside the existing rows rather than update
them, doubling the portfolio with no error anywhere. crane migrate records which recipe
your database’s rows came from, and the importers refuse rather than let that happen.
Two ways forward:
# Re-derive everything, which is the clean one:
crane --database data/new.db init
crane --database data/new.db import adapter <name> /sources/<legacy>.db
# Or carry on with the database you have, accepting that rows already in it keep the
# identifiers they were given:
crane --database data/crane.db migrate --accept-id-scheme
A fresh crane init is unaffected and needs neither.
Downgrading
There is no down migration. Restore the file you copied before upgrading, and run the older image against it.
Without Docker
Python 3.11 or newer, and the SQLite that Python is linked against must be SQLite 3.35.0 or later — the version RETURNING arrived in. That is a property of the Python build rather than a package you can upgrade separately, so a host with an older one needs a newer Python or the container image, which carries a supported one. CRAne checks on the first command rather than failing later from inside a write.
No other system dependencies.
The container image is the supported distribution and everything else on this page assumes it; this path needs a checkout of the repository instead:
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
export CRANE_DB=/secure/volume/crane.db
crane init
The import and migration path uses only the standard library, and a test asserts that no module on it reaches for a web dependency. The web interface needs FastAPI, uvicorn and Jinja2, which install alongside it.