Configuration reference

August 7, 2026 · View on GitHub

Every CASHPILOT_* setting, what reads it, and — where a file can also supply the value — which one wins.

That last column is the reason this page exists. Three settings look identical from the outside (a secret, supplied by an environment variable or by a file under /data) and resolve in three different directions. Each behaviour is defensible on its own; together they are impossible to guess.

!!! warning "The precedence is not uniform, and the differences are deliberate"

- **Credential-encryption key** — the **file wins**. Switching keys would make
  every stored credential unreadable, so an existing `/data/.fernet_key` beats
  `CASHPILOT_ENCRYPTION_KEY`, and CashPilot logs loudly when they differ.
- **Session-signing key** — the **environment wins**. Sessions are cheap to
  invalidate, so `CASHPILOT_SECRET_KEY` takes precedence and the file is only
  a fallback.

If you set an environment variable and nothing changed, this is why.

UI

VariableDefaultWhat it doesPrecedence
CASHPILOT_SECRET_KEYgeneratedSigns session cookies.Env wins, then /data/.secret_key, then a generated key that is persisted. A known-placeholder value is ignored.
CASHPILOT_ENCRYPTION_KEYgeneratedFernet key for credentials at rest.File wins. An existing /data/.fernet_key beats this; the env key is adopted only when no file exists.
CASHPILOT_ALLOW_EPHEMERAL_KEYfalseAllow starting when the encryption key cannot be persisted.
CASHPILOT_API_KEYfrom /fleetShared enrolment key. Not an ongoing credential — see Fleet.Env, else /fleet/.fleet_key, else generated there.
CASHPILOT_ADMIN_API_KEYunsetBearer token for API access without a session. Grants owner: it can deploy, stop and remove containers and read stored credentials.
CASHPILOT_READONLY_API_KEYunsetBearer token for reporting only. Accepted on a small allowlist of GET endpoints (earnings summary and breakdown, fleet summary, health scores, deployed services) and refused everywhere else, including on endpoints added in the future. Use this for a dashboard tile, a Grafana panel or Home Assistant sensors rather than handing them a key that controls containers.
CASHPILOT_DATA_DIR/dataWhere the database and keys live.
CASHPILOT_FLEET_DIR/fleetWhere the shared enrolment key lives.
CASHPILOT_BASE_URLunsetAbsolute base URL, for links in notifications.
CASHPILOT_SECURE_COOKIEautoForce the Secure cookie flag.
CASHPILOT_SESSION_EPOCHunsetBumping this invalidates every existing session.
CASHPILOT_TRUSTED_PROXYunsetTrust X-Forwarded-For from these addresses.
CASHPILOT_COLLECT_INTERVAL60Minutes between earnings collections.
CASHPILOT_HOSTNAME_PREFIXcashpilotPrefix for managed container names.
CASHPILOT_VERSIONdevSet by the image build. Shown in the sidebar.
CASHPILOT_METRICS_ENABLEDfalseServe /metrics.
CASHPILOT_METRICS_TOKENunsetRequire Authorization: Bearer on /metrics.
CASHPILOT_UPDATE_CHECKonSet to off to disable the once-a-day check for a newer release. See below.
CASHPILOT_NTFY_URLunsetntfy endpoint for alerts.
CASHPILOT_WEBHOOK_URLunsetGeneric webhook for alerts.
CASHPILOT_TELEGRAM_BOT_TOKENunsetTelegram alerts.
CASHPILOT_TELEGRAM_CHAT_IDunsetTelegram alerts.
CASHPILOT_WORKER_ALLOWED_HOSTSunsetRestrict which hosts the UI will proxy to.
CASHPILOT_WORKER_ALLOW_METADATAfalseAllow proxying to cloud metadata IPs. Leave off.
CASHPILOT_WORKER_URL_POLICYstrictHow worker URLs are validated.

Worker

VariableDefaultWhat it doesPrecedence
CASHPILOT_UI_URLRequired. Where to send heartbeats.
CASHPILOT_API_KEYRequired for enrolment only. After enrolling, the worker uses its own key from /data/.worker_key.
CASHPILOT_WORKER_NAMEhostnameDisplay name. Set it. Inside a container the default is the container ID, which Docker regenerates on every recreate.
CASHPILOT_WORKER_URLdetectedThe URL this worker advertises.
CASHPILOT_PORT8081The port this worker advertises — see the note below.
CASHPILOT_WORKER_NETWORKdetectedresidential or hosting.
CASHPILOT_EGRESS_DETECTonHourly public-IP lookup. off disables it.
CASHPILOT_EGRESS_IPunsetState the public IP directly. A LAN or tailnet address is rejected.
CASHPILOT_EGRESS_IP_URLunsetCustom IP-echo endpoint.
CASHPILOT_ALLOWED_VOLUME_ROOTSunsetHost paths a deploy may bind-mount.
CASHPILOT_PIDS_LIMITunsetpids limit applied to managed containers.
CASHPILOT_DATA_DIR/dataWhere .worker_id and .worker_key live.

!!! danger "CASHPILOT_PORT does not change the port the worker listens on"

The listen port is fixed at `8081` by the image's `CMD`. `CASHPILOT_PORT`
only changes the port the worker **advertises** to the UI. Setting it alone
makes the worker advertise a port nothing is listening on, and the UI's
container commands then fail with nothing in the logs connecting the two.

To actually move the port, override the container's `command:` **and** set
`CASHPILOT_PORT` to match.

GPU passthrough

CashPilot reports a worker's GPU as one of three answers — yes, no, or unknown — and inside a container the honest answer is almost always unknown: the absence of a GPU there says nothing about the host.

That matters because four services only earn with a real GPU (Salad, Nosana, io.net, Vast.ai), and a GPU service deployed without the device starts, reports healthy, and earns nothing. It is the same shape as the Mysterium /dev/net/tun failure.

To let the worker see an Intel or AMD GPU, uncomment the block in the compose file:

devices:
  - /dev/dri:/dev/dri

!!! warning "Only on a host that actually has one"

Docker **refuses to start a container** when a listed device does not exist,
so this is shipped commented out. Uncommenting it on a GPU-less host breaks
the worker outright.

NVIDIA is a different mechanism

/dev/dri does nothing for an NVIDIA card, and installing the NVIDIA Container Toolkit is only the prerequisite — the toolkit on its own does not hand the GPU to a Compose service. You have to ask for it as well, with either of these:

=== "Shorthand"

```yaml
gpus: all
```

=== "Explicit reservation"

```yaml
deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: all
          capabilities: [gpu]
```

Both are Compose-spec attributes and both were validated against Compose v2.40.3. Pick one; do not set both. Use the explicit reservation when you need to pin particular cards, which it can do via device_ids and capabilities.

!!! warning "These fail on a GPU-less host too"

Verified: on a machine with no NVIDIA card, *both* forms exit 1, the
reservation form reporting `could not select device driver`. So like
`/dev/dri`, they ship commented out rather than enabled by default.

Once the GPU is actually allocated, the worker finds nvidia-smi and reports the real model name rather than just a device count.

Passing a device into the worker only lets the worker see it. A deployed GPU service needs the device too — declare it in that service's catalog entry.

Compose-level

These are read by the compose files, not by CashPilot itself.

VariableDefaultWhat it does
CASHPILOT_BIND_ADDR127.0.0.1Which host interface publishes the UI port.
CASHPILOT_WORKER_BIND_ADDR127.0.0.1Which host interface publishes the worker port, in the fleet compose. The worker holds the Docker socket — root-equivalent on the host — so publish it only on an interface the UI needs, never 0.0.0.0.

Memory

The shipped compose files set no memory limit, deliberately. If you set one, size it from the numbers below rather than from what the container looks like when idle — because those two figures are very far apart.

Measured, not estimated

Read from the kernel's own high-water mark (memory.peak) on a live install after 8 hours of normal operation, which covers roughly 8 hourly collection cycles:

ContainerSteady statePeakDatabase
cashpilot-ui~72 MiB207 MiB54.7 MB
cashpilot-worker~65 MiB130 MiB

Provenance: drumsergio/cashpilot:1.14.1, 15 managed containers, 54.7 MB SQLite database, collectors running hourly in-process under APScheduler. Figures are memory.peak and docker stats from the container's own cgroup.

Why the peak is what matters

The UI idles around 72 MiB and peaks near 207 MiB — close to three times its resting size. Collection runs every service's collector in-process, and the transient cost of that dwarfs the steady state.

So a limit chosen by looking at a running container is almost certainly too low. 128 MiB looks generous against 72 MiB and will be exceeded on the first collection cycle.

!!! danger "An OOM here does not look like an OOM"

The container is killed and restarted mid-collection. The dashboard keeps
serving the last figures it stored, so nothing on screen says anything is
wrong — the symptom is "earnings stopped updating", days later, which is
the hardest kind of failure to notice and the hardest to report.

That is the reason this page gives you a number instead of a limit.

If you do set one

Leave real headroom above the measured peak. 256 MiB leaves only 49 MiB above the 207 MiB observed here, on one install with one database size — a larger database, more services, or a slower provider that holds connections longer will all push it up.

services:
  cashpilot-ui:
    mem_limit: 384m     # 177 MiB above the measured peak
  cashpilot-worker:
    mem_limit: 256m     # 126 MiB above the measured peak

Re-measure on your own install rather than trusting these:

docker exec cashpilot-ui cat /sys/fs/cgroup/memory.peak

Update check

CashPilot asks GitHub once a day whether a newer release exists, and shows a dismissible banner if there is one. A fleet running 33 releases behind with no indication anywhere is the problem this solves.

Three things it deliberately does not do:

  • It never updates anything. It tells you; you decide. This application deploys containers and holds credentials, and nothing about that should happen because a version number changed.
  • It never says "up to date". Offline, firewalled, disabled, or simply not run yet all produce unknown, and unknown renders nothing at all — no error, no spinner, and no reassurance it has not earned.
  • It sends nothing about you. One unauthenticated GET to the public releases endpoint. No request body, no identifier, and your version is not reported upstream. GitHub learns that an IP asked what the latest release is.

Turn it off entirely and no connection is made:

environment:
  - CASHPILOT_UPDATE_CHECK=off

The banner is dismissible per version — dismiss v1.20.1 and it stays gone until there is a v1.20.2, so it cannot become wallpaper.