Install

  1. What you need
  2. Which image
  3. One-shot commands
  4. Compose, for anything that stays up
  5. Volumes
  6. Verify it worked
  7. Upgrading
    1. Upgrading past 0.11.1: identifiers changed
  8. Downgrading
  9. 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 --version and by /healthz is the version on the image label. The release workflow refuses to publish when the tag, pyproject.toml and crane.__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.


Back to top

CRAne. The database is one SQLite file and it is yours.

This site uses Just the Docs, a documentation theme for Jekyll.