Skip to content

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.

DestinationWhen
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 receiversOnly 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 providerOnly 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.

  • A connected staging host with Docker, of the same CPU architecture as the target. An image built for amd64 does not run on arm64, and docker load does 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. Bring docker-compose.tls.yml too if you serve over HTTPS.
  • On the target: Docker, make, python3, and a route to your Kindo API.

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.

  1. Load the release’s images on the staging host:

    Terminal window
    gunzip -c kindo-sift-<version>-images.tar.gz | docker load
  2. Download the weights into a transfer directory. The container runs as your own user so it can write there:

    Terminal window
    mkdir -p transfer/models
    docker 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.5

    The three environment variables only satisfy the server’s required settings; nothing connects to them.

  3. If you use the setfit image variant, add the extra weights it trains from (four more files, 133 MB) by running the same command with --fine-tune added after fetch_models.py.

  4. 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.5

    Add --fine-tune here too if you staged those files.

fetch_models.py exits with:

ExitMeaning
0Present and matching the pin.
1A file does not match its pinned digest. On a staging host that means a damaged or intercepted download: do not carry it across.
3Files are missing.
4The download failed.
5The scoring weights are fine; only the setfit variant’s extra files are missing.
  1. Pack the release’s files, the image bundle and the transfer/models directory together, and record a checksum of the archive.
  2. Move it to the target by whatever your policy allows.
  3. Check the checksum on arrival, then unpack.
  1. Check and load the images:

    Terminal window
    sha256sum -c kindo-sift-<version>-images.tar.gz.sha256
    gunzip -c kindo-sift-<version>-images.tar.gz | docker load
  2. Write .env, generating its secrets on this host:

    Terminal window
    make init
  3. Edit .env:

    Terminal window
    SIFT_MODEL_FETCH_ON_START=false
    SIFT_DEFAULT_ADMIN_PASSWORD=<a password of 12 characters or more>

    With SIFT_MODEL_FETCH_ON_START=false the weight fetcher only verifies and never opens a connection, so missing weights are reported at once instead of after a blocked download times out. Add SIFT_EXTRAS=setfit if you use the setfit variant, and the rest of the production checklist.

  4. Copy the weights into the model-weights volume 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, and chown hands the files to uid 1000, which the containers run as.

  5. Start the stack:

    Terminal window
    make up
  6. 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_done and model_weights_ready.

EventMeaning
model_weights_readyThe weights are present and match the pin.
model_weights_digest_mismatchThe staged files are not the pinned ones. The container stops. Re-check the copy.
model_weights_unavailableThe 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_unavailablesetfit variant only: everything scores, and only training a Text analysis (small sample) filter is refused.

To verify the weights at any time:

Terminal window
make check-models

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:

Terminal window
SIFT_MODEL_DOWNLOAD_BASE_URL=https://mirror.internal/hf

sift-api then downloads from the mirror on first start and checks every file against the pin, as it would from Hugging Face.

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.