Skip to content

Configuration reference

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.

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

Variable Default What it does Precedence
CASHPILOT_SECRET_KEY generated Signs session cookies. Env wins, then /data/.secret_key, then a generated key that is persisted. A known-placeholder value is ignored.
CASHPILOT_ENCRYPTION_KEY generated Fernet 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_KEY false Allow starting when the encryption key cannot be persisted.
CASHPILOT_API_KEY from /fleet Shared enrolment key. Not an ongoing credential — see Fleet. Env, else /fleet/.fleet_key, else generated there.
CASHPILOT_ADMIN_API_KEY unset Bearer token for API access without a session. Grants owner: it can deploy, stop and remove containers and read stored credentials.
CASHPILOT_READONLY_API_KEY unset Bearer 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 /data Where the database and keys live.
CASHPILOT_FLEET_DIR /fleet Where the shared enrolment key lives.
CASHPILOT_BASE_URL unset Absolute base URL, for links in notifications.
CASHPILOT_SECURE_COOKIE auto Force the Secure cookie flag.
CASHPILOT_SESSION_EPOCH unset Bumping this invalidates every existing session.
CASHPILOT_TRUSTED_PROXY unset Trust X-Forwarded-For from these addresses.
CASHPILOT_COLLECT_INTERVAL 60 Minutes between earnings collections.
CASHPILOT_HOSTNAME_PREFIX cashpilot Prefix for managed container names.
CASHPILOT_VERSION dev Set by the image build. Shown in the sidebar.
CASHPILOT_METRICS_ENABLED false Serve /metrics.
CASHPILOT_METRICS_TOKEN unset Require Authorization: Bearer on /metrics.
CASHPILOT_UPDATE_CHECK on Set to off to disable the once-a-day check for a newer release. See below.
CASHPILOT_NTFY_URL unset ntfy endpoint for alerts.
CASHPILOT_WEBHOOK_URL unset Generic webhook for alerts.
CASHPILOT_TELEGRAM_BOT_TOKEN unset Telegram alerts.
CASHPILOT_TELEGRAM_CHAT_ID unset Telegram alerts.
CASHPILOT_WORKER_ALLOWED_HOSTS unset Restrict which hosts the UI will proxy to.
CASHPILOT_WORKER_ALLOW_METADATA false Allow proxying to cloud metadata IPs. Leave off.
CASHPILOT_WORKER_URL_POLICY strict How worker URLs are validated.

Worker

Variable Default What it does Precedence
CASHPILOT_UI_URL Required. Where to send heartbeats.
CASHPILOT_API_KEY Required for enrolment only. After enrolling, the worker uses its own key from /data/.worker_key.
CASHPILOT_WORKER_NAME hostname Display name. Set it. Inside a container the default is the container ID, which Docker regenerates on every recreate.
CASHPILOT_WORKER_URL detected The URL this worker advertises.
CASHPILOT_PORT 8081 The port this worker advertises — see the note below.
CASHPILOT_WORKER_NETWORK detected residential or hosting.
CASHPILOT_EGRESS_DETECT on Hourly public-IP lookup. off disables it.
CASHPILOT_EGRESS_IP unset State the public IP directly. A LAN or tailnet address is rejected.
CASHPILOT_EGRESS_IP_URL unset Custom IP-echo endpoint.
CASHPILOT_ALLOWED_VOLUME_ROOTS unset Host paths a deploy may bind-mount.
CASHPILOT_PIDS_LIMIT unset pids limit applied to managed containers.
CASHPILOT_DATA_DIR /data Where .worker_id and .worker_key live.

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

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:

gpus: all
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.

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.

Variable Default What it does
CASHPILOT_BIND_ADDR 127.0.0.1 Which host interface publishes the UI port.
CASHPILOT_WORKER_BIND_ADDR 127.0.0.1 Which 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:

Container Steady state Peak Database
cashpilot-ui ~72 MiB 207 MiB 54.7 MB
cashpilot-worker ~65 MiB 130 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.

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.