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_keybeatsCASHPILOT_ENCRYPTION_KEY, and CashPilot logs loudly when they differ. - Session-signing key — the environment wins. Sessions are cheap to
invalidate, so
CASHPILOT_SECRET_KEYtakes 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:
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:
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:
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
GETto 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:
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.