Install without internet access
Sift can run on a host with no general internet access. Scoring makes no network call: it runs on the CPU against local model weights. What the host would normally download, the container images and the model weights, you stage on a connected machine and carry across. The one connection Sift still needs is to your Kindo API, because every source read and agent run goes through it.
What a running stack connects to
Section titled “What a running stack connects to”| Destination | When |
|---|---|
Your Kindo API (SIFT_KINDO_API_BASE_URL, or the connection’s own address) | Once an admin connects Kindo. Required for Sift to do anything. |
| Webhook receivers | Only if an admin adds one under the gear icon (Administration) › Notifications. |
The weight download (SIFT_MODEL_DOWNLOAD_BASE_URL, Hugging Face by default) | On first start, unless you stage the weights or set SIFT_MODEL_FETCH_ON_START=false. |
| The decision model’s provider | Only if SIFT_JEV_ENABLED is set. Leave it off on an isolated host. |
The object store’s own telemetry is switched off in docker-compose.yml.
Before you start
Section titled “Before you start”- A connected staging host with Docker, of the same CPU architecture as the target.
An image built for
amd64does not run onarm64, anddocker loaddoes not warn you. - The release’s image bundle,
kindo-sift-<version>-images.tar.gz, and its.sha256. It holds every image the stack runs, both backend variants included. - The release’s files: the repository at the release’s tag, with
docker-compose.yml,Makefile,scripts/and.env.example. Bringdocker-compose.tls.ymltoo if you serve over HTTPS. - On the target: Docker,
make,python3, and a route to your Kindo API.
Stage the weights on the connected host
Section titled “Stage the weights on the connected host”Scoring needs one set of model weights, about 134 MB in seven files, staged in a
directory named bge-small-en-v1.5. The files are pinned by checksum:
scripts/fetch_models.py knows the SHA-256 of each one and refuses anything else.
-
Load the release’s images on the staging host:
Terminal window gunzip -c kindo-sift-<version>-images.tar.gz | docker load -
Download the weights into a transfer directory. The container runs as your own user so it can write there:
Terminal window mkdir -p transfer/modelsdocker run --rm -u "$(id -u):$(id -g)" \-e SIFT_SECRET_KEY=staging-only \-e SIFT_DATABASE_URL=postgresql+asyncpg://x:x@localhost/x \-e SIFT_REDIS_URL=redis://localhost:6379/0 \-v "$PWD/transfer/models:/staging" \kindo-sift-backend:<version> \python scripts/fetch_models.py --dest /staging/bge-small-en-v1.5The three environment variables only satisfy the server’s required settings; nothing connects to them.
-
If you use the
setfitimage variant, add the extra weights it trains from (four more files, 133 MB) by running the same command with--fine-tuneadded afterfetch_models.py. -
Verify what was staged, without downloading anything:
Terminal window docker run --rm \-e SIFT_SECRET_KEY=staging-only \-e SIFT_DATABASE_URL=postgresql+asyncpg://x:x@localhost/x \-e SIFT_REDIS_URL=redis://localhost:6379/0 \-v "$PWD/transfer/models:/staging:ro" \kindo-sift-backend:<version> \python scripts/fetch_models.py --check --dest /staging/bge-small-en-v1.5Add
--fine-tunehere too if you staged those files.
fetch_models.py exits with:
| Exit | Meaning |
|---|---|
0 | Present and matching the pin. |
1 | A file does not match its pinned digest. On a staging host that means a damaged or intercepted download: do not carry it across. |
3 | Files are missing. |
4 | The download failed. |
5 | The scoring weights are fine; only the setfit variant’s extra files are missing. |
Transfer
Section titled “Transfer”- Pack the release’s files, the image bundle and the
transfer/modelsdirectory together, and record a checksum of the archive. - Move it to the target by whatever your policy allows.
- Check the checksum on arrival, then unpack.
Install on the target
Section titled “Install on the target”-
Check and load the images:
Terminal window sha256sum -c kindo-sift-<version>-images.tar.gz.sha256gunzip -c kindo-sift-<version>-images.tar.gz | docker load -
Write
.env, generating its secrets on this host:Terminal window make init -
Edit
.env:Terminal window SIFT_MODEL_FETCH_ON_START=falseSIFT_DEFAULT_ADMIN_PASSWORD=<a password of 12 characters or more>With
SIFT_MODEL_FETCH_ON_START=falsethe weight fetcher only verifies and never opens a connection, so missing weights are reported at once instead of after a blocked download times out. AddSIFT_EXTRAS=setfitif you use thesetfitvariant, and the rest of the production checklist. -
Copy the weights into the
model-weightsvolume before the first start:Terminal window PROJECT=$(docker compose config --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["name"])')docker volume create "${PROJECT}_model-weights"docker run --rm -u 0 \-v "${PROJECT}_model-weights:/dest" \-v "$PWD/transfer/models:/src:ro" \--entrypoint sh kindo-sift-backend:<version> -c \'cp -r /src/bge-small-en-v1.5 /dest/ && chown -R 1000:1000 /dest'The volume’s name starts with the Compose project, which is the directory name unless you set
COMPOSE_PROJECT_NAME; the first line reads it from Compose. Because you created the volume yourself, Compose warns on the first start that it already exists but was not created by Docker Compose, and uses it. The copy runs as root (-u 0) because a new volume belongs to root, andchownhands the files to uid 1000, which the containers run as. -
Start the stack:
Terminal window make up -
Check the API’s start-up log:
Terminal window docker compose logs sift-api | grep '"logger":"entrypoint"'You should see
migrations_done,buckets_done,seed_admin_doneandmodel_weights_ready.
| Event | Meaning |
|---|---|
model_weights_ready | The weights are present and match the pin. |
model_weights_digest_mismatch | The staged files are not the pinned ones. The container stops. Re-check the copy. |
model_weights_unavailable | The weights are not staged. The stack runs, but filters whose recipes read text (Text analysis, Collapse repeats, and Anomaly detection on text fields) cannot score or train until they are. Fixed-rule, structured-field and numeric-only filters are unaffected. |
fine_tune_weights_unavailable | setfit variant only: everything scores, and only training a Text analysis (small sample) filter is refused. |
To verify the weights at any time:
make check-modelsAn internal mirror instead
Section titled “An internal mirror instead”If you run a mirror that serves files in Hugging Face’s layout,
{base}/{repo_id}/resolve/{revision}/{file}, leave SIFT_MODEL_FETCH_ON_START=true and
point Sift at it:
SIFT_MODEL_DOWNLOAD_BASE_URL=https://mirror.internal/hfsift-api then downloads from the mirror on first start and checks every file against
the pin, as it would from Hugging Face.
Upgrading an isolated host
Section titled “Upgrading an isolated host”Stage the new release’s images the same way and carry them across with its files, then
follow Upgrade Sift. Keep the model-weights volume: the same
weights serve every release unless a release changes the pin, and its upgrade notes say
so when it does.