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. Two settings look identical
from the outside (a secret, supplied by an environment variable or by a file
under /data) and resolve in opposite 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, and it reads nothing: every dashboard API route answers it with 403. Tiles, widgets and scripts use CASHPILOT_READONLY_API_KEY. 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 | The URL you open CashPilot at. When it starts with https, session cookies get the Secure flag (unless CASHPILOT_SECURE_COOKIE says otherwise). |
- |
CASHPILOT_SECURE_COOKIE |
auto | true forces the Secure cookie flag on, false forces it off. Unset, it is on when CASHPILOT_BASE_URL is https or a trusted proxy reports HTTPS. |
- |
CASHPILOT_SESSION_EPOCH |
0 |
A Unix timestamp: sessions issued before it are rejected. Set it to the current time to sign everyone out. | - |
CASHPILOT_TRUSTED_PROXY |
off | Set to 1 (or true) only when the UI sits behind exactly one reverse proxy you control. CashPilot then trusts X-Forwarded-For for the client address and X-Forwarded-Proto for the Secure cookie flag. Whatever this is set to, the proxy must pass the original Host header or send X-Forwarded-Host: a signed-in browser's changes are refused with 403 when its page address matches neither. |
- |
CASHPILOT_COLLECT_INTERVAL |
60 |
Minutes between earnings collections. | - |
CASHPILOT_HOSTNAME_PREFIX |
cashpilot |
Fills {hostname} in a service's device name when a deploy names no host. Container names are cashpilot-<service>, with -2, -3 and so on for extra copies on the same box. |
- |
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_DRIFT_WARNINGS |
on |
Set to off to stop judging containers against the catalog: no "Needs fixing" row for a container that misses a catalog change. Set it on the worker too, which then stops reporting container settings. A single warning can be muted from its row instead (Ignore). Containers you made by hand are never judged. |
- |
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 | Worker hosts allowed under the strict policy: CIDRs, host names and *.suffix patterns, separated by commas. |
- |
CASHPILOT_WORKER_ALLOW_METADATA |
false |
Allow proxying to cloud metadata IPs. Leave off. | - |
CASHPILOT_WORKER_URL_POLICY |
permissive |
How worker URLs are validated. permissive accepts any address except loopback, link-local and cloud metadata, so LAN, Tailscale and public addresses all pass. strict accepts only CASHPILOT_WORKER_ALLOWED_HOSTS. |
- |
CASHPILOT_ACTIVITY_ASN_URL |
iptoasn.com's ip2asn-combined.tsv.gz |
Where the Activity page's network and country table is downloaded from, weekly while a box has Activity on. Empty disables the download; networks and countries then read as unknown unless you place /data/ipgeo.sqlite by hand. |
- |
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_DRIFT_WARNINGS |
on | off stops reporting each container's settings, so the UI judges none of this worker's containers against the catalog. |
- |
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. | - |
DOCKER_HOST |
set by compose | Where the worker reaches Docker. The shipped compose files point it at the socket proxy (unix:///run/cashpilot-docker/docker.sock); unset, the worker uses /var/run/docker.sock. |
- |
CASHPILOT_PIDS_LIMIT |
512 |
pids limit applied to managed containers. A value that is not a positive integer falls back to the default. |
- |
CASHPILOT_ISOLATION |
auto |
auto, on or off. With auto, a new install fences its earners off your LAN, the host, your tailnet and cloud metadata, and an upgraded install whose earners are on Docker's default bridge keeps them there until you turn isolation on from the Fleet page. on fences from the start; off keeps Docker's default bridge. See LAN isolation by default. |
- |
CASHPILOT_FENCE_BACKEND |
auto |
For testing only: legacy makes the fence use iptables-legacy on a host that has nf_tables, nft forces nf_tables. auto picks nf_tables when the kernel has it. |
- |
CASHPILOT_CONTAINER_NETWORK |
unset | A bridge network you created, which containers that would land on Docker's default bridge join instead, so your own host firewall rules can confine them. Setting it turns off CashPilot's own fence on that worker. Host-networked services are unaffected. The worker never creates it; a deploy is refused while it is missing. See Protecting your home network. | - |
CASHPILOT_CONTAINER_DNS |
1.1.1.1,9.9.9.9 |
DNS servers, IP addresses separated by commas, for containers on CashPilot's isolated network or on CASHPILOT_CONTAINER_NETWORK. Other containers and the host keep the Docker daemon's list. Set it empty to give the isolated containers the daemon's list too. Applies when a container is next deployed. See name lookups. |
- |
CASHPILOT_CONTAINER_RUNTIME |
unset | A Docker runtime, such as a gVisor runtime you registered in daemon.json, for containers on bridge networking. Host-networked services keep Docker's default. A deploy is refused while the daemon does not provide it. See gVisor. |
- |
CASHPILOT_CONTAINER_RUNTIME_OVERRIDES |
unset | Per-service exceptions, slug=runtime separated by commas, e.g. bitping=runsc-hostnet-raw. slug=runc runs that service under runc, the runtime Docker ships with, whatever the daemon's default is. |
- |
CASHPILOT_TUNNEL_IMAGE |
drumsergio/cashpilot-tunnel:<worker version> |
The tunnel image for proxied copies, for a mirror or a test build. Only an exact version tag or a digest is accepted, never latest. |
- |
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_UI_PORT |
8080 |
Which host port publishes the UI. Set it when 8080 is already taken on the host. |
CASHPILOT_WORKER_BIND_ADDR |
127.0.0.1 |
Which host interface publishes the worker port, in the fleet and worker-only compose files. The worker's API can create containers on the host, so publish it only on an interface the UI needs, never 0.0.0.0. |
CASHPILOT_COMPOSE_DIR |
unset | Your compose folder. The socket proxy lets containers bind it, which the in-app update needs. Unset, the Update button refuses and prints the line to add. |
CASHPILOT_PROXY_BIND_MOUNTS |
unset | Other host folders the socket proxy lets containers bind, comma-separated with no spaces: the data folder of a service you deploy with a host path, such as Storj (/mnt/user/storj). Each entry also counts as a CASHPILOT_ALLOWED_VOLUME_ROOTS entry, so this one setting opens the folder for both the worker and the proxy. A deploy outside them is refused before anything is touched, with a message naming this variable. |
CASHPILOT_PROXY_POLICY |
set by the compose file | The fingerprint of the socket proxy policy in the same compose file, written by scripts/socket_proxy_policy.py; the worker and the proxy both carry it. Do not edit it. The worker compares it with the policy its own catalog needs and, when they differ, its log, its status page and the update card give the command that updates the policy in your compose file (how). |
CASHPILOT_MIGRATE_COMPOSE_VOLUMES |
set by the compose file | Read by the one-shot cashpilot-migrate service: pairs old=new of compose volumes whose data it copies, once, into the renamed volume that carries the worker's owner label. The old volume is kept. Do not edit it. See UPGRADING.md. |
The UI's web port inside the container is fixed at 8080 (set via the container's CMD). CASHPILOT_UI_PORT and CASHPILOT_BIND_ADDR choose only the host side: which port and which interface it is published on.
Put these in a .env file next to the compose file, not on the command line. The Update button in Settings reads .env but never sees your shell. If the published ports came from the command line, it refuses to update rather than move the dashboard back to 127.0.0.1:8080.
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:
Running a service more than once on one worker¶
Open the service and set Instances next to a node. Each extra copy is its own container
(cashpilot-<service>-2, -3, ...) and registers with the provider as its own device: every
device name the catalog derives from the host name gets -2, -3 added.
- Providers count devices per public IP, and copies on one machine share its connection. Extra copies add devices, not bandwidth, so they only help where the provider pays per device and allows several per IP (Traffmonetizer and Mysterium document that). Where a provider allows one device per IP, the box is locked at 1; add workers on other connections instead.
- A service that keeps data in a host directory or publishes a host port runs once per worker, because its copies would share the directory and fight over the port.
- The count is saved per worker and kept by every redeploy. Lowering it removes the highest copies first; removing the service from a worker removes all of them.
- Each copy has its own Restart, Stop and Logs under Deployed on in the service's details and on the dashboard.
- A worker runs more than one copy only once it is on a release that supports it; older workers keep the box locked until they are updated.
A service's image¶
Each service runs from the image its catalog entry names. To run it from another one, open the
service, expand Advanced and type the image in the Image box: a pinned tag of an image the
catalog leaves floating (honeygain/honeygain:0.8.1), a registry mirror, or a community build for
an architecture the provider does not publish.
- Blank means the catalog image. Clearing the box and deploying goes back to it.
- The choice is saved per worker with the rest of the deployment, and every redeploy keeps it until you clear the box. Redeploy running instances with this image applies it to the workers that already run the service, keeping their credentials.
- Only the image changes. Capabilities, devices, ports, the entrypoint and the stop timeout still come from the worker's catalog entry, so a custom image gets nothing the service does not already have. It does receive the service's credentials, so use an image you trust.
- An image that boots systemd (its main process is
/sbin/init, and its own docs ask for--privileged) is refused at deploy time with that reason: it needs the host's cgroup tree, which CashPilot never grants, so it would exit 255 and restart forever with empty logs. Pick the image's non-systemd tag instead, usuallylite(venatum/earnapp:lite, nevervenatum/earnapp). - The dashboard marks such an instance custom image and judges "update available" against the image you chose, not the catalog's.
To run an image the catalog does not list at all, add it as a custom service instead.
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.
Themes¶
Every signed-in user picks a theme in the avatar menu (top right). It applies straight away and is saved to that account, so it follows you to any browser. A user who never picked one gets Classic night.

| Theme | What it looks like |
|---|---|
| Classic night (default) | Blue-black, violet hairlines, rose buttons and a cyan focus ring. |
| Dark | Graphite and white, no accent colour: colour only ever means a status. |
| Light | The dark theme's rules on paper white. |
| Sunrise | Dark, with the logo's sunrise gradient at the edges: a line under the top bar, the active tab, the main buttons, the focus ring and the balance. |
| Warm | Dark, with one sun orange on everything you click and the charts in the logo's colours. The warning amber leans yellow so it is not mistaken for the orange. |
Status colours (green up, amber warning, red down) mean the same thing in every theme, and every text and control meets WCAG AA contrast.





To add a theme, add one entry to app/themes.json and one block plus one swatch rule in app/static/css/style.css. The server, the settings check, the menu and the CI checks all read that list.