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 (
*_filepaths, 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-ownedchmod 0600key file is exactly the trap: usechmod 640plus a group the container user can read (orchownthe files to uid 10001). - Checkpoint state — bind-mount a volume at
/var/lib/sf2lokiso 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):
- Environment variables —
SF2LOKI_*, with__(double underscore) as the nesting delimiter, e.g.SF2LOKI_SALESFORCE__CLIENT_IDsetssalesforce.client_id. - YAML file — passed via
--config. - Built-in defaults.
Secrets are never inlined as plain config values by default. Two mechanisms inject them:
*_filepaths — 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):
See Configuration for the full settings guide and config-reference.md for the generated reference of every key, type, default, and description.