All configuration is by environment variable.

Variable Default Purpose
LFSX_BIND 0.0.0.0:8080 listen address
LFSX_STORAGE_ROOT /var/lib/lfsx root of the object store
LFSX_PUBLIC_URL the requested host public URL used to build transfer links; set it behind a proxy, because the fallback trusts the caller's headers
LFSX_AUTH github permission source: github, gitlab, gitea (or forgejo, the same provider), or disabled to accept every request
LFSX_GITHUB_API_URL https://api.github.com API root, point it at your GitHub Enterprise host
LFSX_GITLAB_API_URL https://gitlab.com/api/v4 API root, point it at your self-managed GitLab
LFSX_GITEA_API_URL none, required API root of your Gitea or Forgejo instance, https://git.example.com/api/v1
LFSX_ANONYMOUS_READ false true to let a request with no credentials read a repository the forge serves publicly
LFSX_ALLOWED none repositories this server serves, same syntax as LFSX_RESTRICTED; anything else is 404 without asking the forge. Unset serves every repository on the forge, see Allowed namespaces
LFSX_FORGES none more forges served beside the one LFSX_AUTH names, each under /-/{name}/ and configured by LFSX_FORGE_<NAME>_*, see Several forges
LFSX_RESTRICTED none repositories whose objects take write access to read, comma-separated, org/repo or org/prefix-* or org/*
LFSX_AUTH_CACHE_TTL 60 seconds a granted permission is reused before being checked again
LFSX_AUTH_REJECTION_TTL 10 seconds a refusal is remembered, so a bad token cannot hammer the forge
LFSX_AUTH_LOOKUP_BUDGET 600 forge lookups a minute this server will spend, counting only what the caches could not answer; 0 removes the ceiling
LFSX_DASHBOARD false true to serve the web dashboard at /-/dashboard/, see Dashboard
LFSX_DASHBOARD_REPO none org/repo whose admins may sign in to the dashboard with their forge token; required with it unless LFSX_AUTH=disabled
LFSX_DASHBOARD_DIR /usr/share/lfsx/dashboard where the dashboard's built pages are
LFSX_GITHUB_APP_ID unset a GitHub App id, giving the server its own identity (and quota) for the anonymous public-repository lookup
LFSX_GITHUB_APP_KEY_FILE unset path to the App's RSA private key in PEM form; comes together with the id, one without the other refuses to start
LFSX_GC_GRACE 1209600 seconds an object must have been untouched before collection can take it
LFSX_STAGING_MAX_AGE 86400 seconds before an interrupted upload's leftovers are reclaimed, on the volume and in the bucket
LFSX_LOCK_MAX_AGE never seconds a lock may go untouched before anyone can take it
LFSX_MAX_OBJECT_SIZE unlimited bytes an object may reach before the server refuses it
LFSX_REPO_QUOTA unlimited bytes a single repository may hold
LFSX_MAX_CONCURRENT_TRANSFERS 128 uploads and downloads served at once; the transfer past the cap is answered 503 with Retry-After rather than queued; 0 removes the cap
LFSX_STORAGE local s3 to keep objects in a bucket instead of on the volume, azure for a Blob Storage container, gcs for Cloud Storage
LFSX_S3_ENDPOINT / LFSX_S3_BUCKET / LFSX_S3_REGION unset where the bucket is; endpoint and bucket are required with LFSX_STORAGE=s3
LFSX_S3_ACCESS_KEY / LFSX_S3_SECRET_KEY unset credentials for it, required with LFSX_STORAGE=s3
LFSX_S3_PATH_STYLE true false for virtual-host addressing; MinIO and Garage want path style
LFSX_S3_PRESIGN false true to hand transfers to the bucket instead of streaming them through the server, ignored for downloads when compression or encryption is configured, and ignored entirely if the bucket does not prove it verifies upload checksums
LFSX_S3_CACHE_DIR unset directory holding a local copy of what the bucket serves, so a second reader does not pay the round trip again
LFSX_AZURE_ACCOUNT / LFSX_AZURE_CONTAINER unset the storage account and container, required with LFSX_STORAGE=azure
LFSX_AZURE_ENDPOINT https://<account>.blob.core.windows.net blob endpoint, for Azurite, a private endpoint or a sovereign cloud
LFSX_AZURE_ACCOUNT_KEY / LFSX_AZURE_SAS_TOKEN unset at most one; neither authenticates with the managed or workload identity, see Azure
LFSX_GCS_BUCKET unset the bucket, required with LFSX_STORAGE=gcs
LFSX_GCS_CREDENTIALS unset a service account key file, none for an emulator, or unset for the metadata server and workload identity, see Cloud Storage
LFSX_GCS_ENDPOINT https://storage.googleapis.com API endpoint, for an emulator or a private endpoint
LFSX_S3_CACHE_MAX_BYTES unset bytes the cache may hold before the least recently used entries are dropped; required with the directory, which does nothing without it
LFSX_COMPRESSION none zstd, or zstd:1…zstd:19 to pick the level, to compress objects at rest
LFSX_ENCRYPTION_KEY_FILE unset path to a file holding one or more 32-byte keys as hex, to encrypt objects at rest
LFSX_ENCRYPTION_KEY_COMMAND unset command whose stdout is read exactly like the key file, for keys that must never rest on disk; mutually exclusive with the file, setting both refuses to start
RUST_LOG info log filter (tracing_subscriber syntax)
LFSX_OTLP_ENDPOINT unset HTTP traces URL of an OTLP collector (http://collector:4318/v1/traces); unset means the tracing layer is not even installed

Set LFSX_PUBLIC_URL behind a proxy. Unset, the origin is built from the Host and X-Forwarded-Proto the caller sent, and those are a fact about the deployment only for as long as something in front is rewriting both. The URLs in a batch answer are where the client sends the object, with its credential attached, so a caller who chooses the header chooses where its own token goes, and anything caching that answer serves the choice to whoever asks next. The server says so at startup when it is unset and authentication is on.

The fallback is not left wide open in the meantime. The scheme has to be http or https, and the host has to be a host: a Host carrying a / or an @ is refused and the origin falls back to localhost, which is useless to everybody and dangerous to nobody. real.example@evil.example is the one that matters, because it resolves to the second name with the first read as a username.

LFSX_PUBLIC_URL is echoed in the batch response, and the client reconnects to it for every object: if it is wrong, negotiation succeeds and every transfer then fails.

Left unset, the server answers on whatever host the request arrived at, honouring X-Forwarded-Proto from the proxy in front. That is what you want when the same server is reached under more than one name (a public host and an internal one, say) since a single fixed value would be wrong for half the clients. Set it when you want to pin one name regardless of how the request arrived; an explicit value always wins over the request.