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. 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_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, 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:

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_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:

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

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, usually lite (venatum/earnapp:lite, never venatum/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 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.

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.

The avatar menu open on a phone, with the theme list: Classic night (default), Dark, Light, Sunrise and Warm, each with a colour swatch

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.

The dashboard in Classic night

The dashboard in Dark

The dashboard in Light

The dashboard in Sunrise

The dashboard in Warm

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.