Skip to content

Installation

sf2loki requires Python 3.14+ (it uses 3.14 language features). pipx/uvx provision a matching interpreter automatically; the container needs no Python on the host at all.

Use case Install Notes
Run the daemon (recommended) docker pull ghcr.io/rknightion/sf2loki:latest The long-running service. Multi-arch image, non-root, slim.
CLI / setup tooling uvx sf2loki --help Zero-install run of --check, doctor, backfill, config — handy during Salesforce app setup before any infra exists.
CLI, persistent pipx install sf2loki Same CLI on a VM or air-gapped host where a container isn't wanted.
As a library / from source uv sync (repo checkout) or pip install sf2loki Optional sf2loki[s3] / sf2loki[gcs] / sf2loki[k8s] extras for the non-default checkpoint stores and Kubernetes-Lease coordinator.
uvx sf2loki --version
uvx sf2loki --check --config config.yaml    # validate config + wiring, no network calls
uvx sf2loki doctor --config config.yaml     # live preflight (auth, entitlements, Loki write)

The container is the right target for the always-on ingestion daemon; pipx/uvx are for the one-shot CLI surfaces (doctor, --check, backfill) you run by hand around setup and troubleshooting.

Docker / docker-compose

Every push to main publishes a multi-arch image to GHCR (ghcr.io/rknightion/sf2loki:main, plus :main-<sha>); releases add semver tags and :latest. The container is slim, runs as a non-root user (uid 10001), and exposes :8080 for /healthz and /readyz — metrics push over OTLP, so there is no scrape port.

# run the published :latest release image — non-secret values from .env.dev, secrets from ./secrets
docker compose --env-file .env.dev pull
docker compose --env-file .env.dev up -d

Set SF2LOKI_TAG=main (e.g. in .env.dev) to track the rolling edge build instead of a release — it can carry unreleased and breaking changes, so it's opt-in, never the default. SF2LOKI_TAG=main-<sha> pins a specific edge build.

Check the changelog before bumping :latest

Releases are semver'd by release-please from conventional commits. Check the repository's changelog for a feat!: / BREAKING CHANGE: entry between your current and target version before upgrading, the same way you'd check any other dependency's major-version notes.

Volumes and permissions

The container mounts a config file, a read-only secrets directory, and a writable state directory:

  • Config — mounted read-only at /etc/sf2loki/config.yaml.
  • Secrets (*_file paths, e.g. the private key or Loki token) — mounted read-only, e.g. at /etc/sf2loki/secrets. They must be readable by uid 10001 or the container crash-loops with an actionable "permission denied" error at startup. A root-owned chmod 0600 key file is exactly the trap: use chmod 640 plus a group the container user can read (or chown the files to uid 10001).
  • Checkpoint state — bind-mount a volume at /var/lib/sf2loki so resume survives container recreation. The container runs as uid 10001, so the host directory must be writable by it: mkdir -p state && chmod 770 state && chown 10001 state (770 + chown, not a permissive 777 — sf2loki doctor's own failure hint recommends the same).

Health check target — use /readyz, not /healthz, for lifecycle checks

/healthz is liveness (200 whenever the process is up, even mid-startup); /readyz is readiness (200 only once auth has resolved and the pipeline is running, and it degrades to 503 if Loki pushes have been failing continuously). For a standalone instance, point a Docker HEALTHCHECK or an ECS task healthCheck at /readyz — the shipped Dockerfile already does. On an active-passive HA replica, the standby's /readyz is 503 forever by design (it never becomes ready), so a task-level healthCheck restart-loops it and defeats failover — use /healthz there instead. See High Availability for the full readiness-vs-liveness split.

Run exactly one active replica outside of the HA pair described in High Availability — the Pub/Sub API delivers events independently per subscriber connection, so a second concurrently-active instance double-delivers.

From source (uv)

git clone https://github.com/rknightion/sf2loki.git
cd sf2loki
just setup         # uv sync — create the venv from the lockfile
just gate          # ruff + mypy --strict + pytest — the green bar
uv run python -m sf2loki --config config.example.yaml --check

just setup is a thin wrapper over uv sync; use uv sync directly if you don't have just installed. See Development for the full contribution workflow.

Configuration loading

Config loads from a YAML file and/or environment variables, in this precedence order (highest first):

  1. Environment variablesSF2LOKI_*, with __ (double underscore) as the nesting delimiter, e.g. SF2LOKI_SALESFORCE__CLIENT_ID sets salesforce.client_id.
  2. YAML file — passed via --config.
  3. Built-in defaults.

Secrets are never inlined as plain config values by default. Two mechanisms inject them:

  • *_file paths — e.g. salesforce.private_key_file, sink.loki.auth_token_file — point at a mounted secret file; sf2loki reads its contents at startup.
  • ${ENV} interpolation — any config value can reference an environment variable, e.g. client_id: ${SF_CLIENT_ID}, resolved when the file is loaded.

A missing or unreadable secret (an unset ${ENV} reference, or a *_file path that doesn't exist or isn't readable) is fatal at startup — sf2loki never falls back to a silent blank.

Validate the whole thing offline with --check (secrets resolve, Loki labels are legal, sources don't overlap — no network calls):

uv run python -m sf2loki --config config.yaml --check

See Configuration for the full settings guide and config-reference.md for the generated reference of every key, type, default, and description.