Skip to content

Getting Started

Prerequisites

  • Docker and Docker Compose installed on your server
  • A Linux, macOS, or Windows host (amd64 or arm64)
  • At least 1 GB of RAM available for CashPilot + managed services

Quick Start

1. Clone and launch

git clone https://github.com/GeiserX/CashPilot.git
cd CashPilot
docker compose up -d

This starts two containers:

Container Port Purpose
cashpilot-ui 8080 Web dashboard, earnings collection, service catalog
cashpilot-worker 8081 Docker agent that deploys and monitors service containers

2. Open the dashboard

Navigate to http://localhost:8080 in your browser. Since no account exists yet, you'll be redirected to onboarding and then to the registration form.

First-run setup token required

On first start, CashPilot generates a one-time setup token and prints it to the cashpilot-ui container logs:

docker compose logs cashpilot-ui

Look for a line like:

FIRST-RUN SETUP: no account exists yet. Open /register and enter this
one-time setup token to create the owner account: <token>

Copy that token into the Setup Token field on the registration form to create the first (owner) account. It's only ever shown in the logs — never in a URL — and is discarded permanently once the owner account exists.

3. Browse the service catalog

Filter services by category (bandwidth, DePIN, storage, compute), view earning estimates, and check requirements before deploying.

4. Sign up for services

Each service card has a signup link. Create accounts on the services you want to run.

5. Enter credentials and deploy

The setup wizard collects only the credentials each service needs (email/password, API token, etc.). Click Deploy and CashPilot handles the rest -- pulling images, creating containers, and starting health monitoring.

How It Works

graph LR
    A[User] -->|Browse & Configure| B[CashPilot UI<br>Port 8080]
    B -->|Deploy Commands| C[CashPilot Worker<br>Port 8081]
    C -->|Docker API| D[Service Containers]
    D -->|Health & Status| C
    C -->|Heartbeat| B
    B -->|Collect Earnings| E[Service APIs]
    E -->|Balance Data| B
  1. You configure services through the web UI -- pick a service, enter credentials, click deploy.
  2. The UI sends the container spec (image, env vars, volumes) to the worker via REST API.
  3. The worker creates the Docker container and starts monitoring its health.
  4. The worker reports container status back to the UI every 60 seconds via heartbeats.
  5. The UI collects earnings from service APIs on a configurable schedule (default: every hour).
  6. The dashboard shows aggregated earnings, per-service breakdowns, and container health.

Configuration

UI Environment Variables

Variable Default Description
TZ UTC Timezone for scheduling and display
CASHPILOT_SECRET_KEY (auto-generated) Signing key for login sessions. Persisted at /data/.secret_key. Does not encrypt credentials
CASHPILOT_ENCRYPTION_KEY (auto-generated) Fernet key encrypting stored credentials at rest. Persisted at /data/.fernet_key. Adopted only when that file is absent, so set it only to restore a backup
CASHPILOT_API_KEY -- Shared secret between UI and workers for API authentication
CASHPILOT_COLLECT_INTERVAL 60 Minutes between earnings collection cycles
CASHPILOT_BIND_ADDR 127.0.0.1 Host interface the UI port is published on. Loopback by default — the dashboard can command the Docker-socket worker, so it is not exposed to your network out of the box. Set a specific IP (e.g. a Tailscale/VPN address) or 0.0.0.0 to expose it, or (preferred) run an authenticating reverse proxy in front

The UI's web port inside the container is fixed at 8080 (set via the container's CMD); CASHPILOT_BIND_ADDR controls only which host interface it is published on.

Worker Environment Variables

Variable Default Description
TZ UTC Timezone
CASHPILOT_UI_URL -- URL of the UI container, e.g. http://cashpilot-ui:8080
CASHPILOT_API_KEY -- Must match the UI's API key
CASHPILOT_WORKER_NAME (hostname) Display name for this worker in the fleet dashboard
CASHPILOT_WORKER_URL (auto-detected) URL the UI uses to reach this worker, e.g. http://192.168.10.50:8081. Set explicitly for cross-host fleets — auto-detection can report an unreachable container-internal IP
CASHPILOT_WORKER_BIND_ADDR 127.0.0.1 Host interface the worker's Docker-socket API port is published on. Loopback by default. The worker API can deploy/stop any container (= root on the host), so for a remote worker bind a private/VPN interface (e.g. a Tailscale IP), never a public IP
CASHPILOT_PORT 8081 Port the worker advertises to the UI. It does not change the listen port, which is fixed by the image's CMD — see the configuration reference

Docker Compose Example

# CashPilot - Self-hosted passive income orchestrator
# https://github.com/GeiserX/CashPilot
#
# UI + Worker on the same server. For multi-server fleet, see docker-compose.fleet.yml
#
#   Start / update:  docker compose pull && docker compose up -d
#
# Images are PINNED to a major.minor tag, which is what the lines below say.
# `docker compose pull && up -d` therefore gets the newest patch of that series,
# not the newest release overall — to move to a newer series, edit the tags.
#
# This header used to claim the file tracked `latest` while pinning 1.4, and the
# string `:latest` it told you to replace did not appear anywhere. Following the
# quickstart therefore installed a version many releases behind, and issue #188
# is someone hitting a first-run bug that had been fixed for months because of
# it. Keep this comment honest about what the file actually does.
# See https://hub.docker.com/r/drumsergio/cashpilot/tags for available tags.
# NOTE: versions before 1.0.0 predate the per-worker fleet-key auth cutover and
# are not compatible with a worker running 1.x -- pin UI and worker together.
# Building from source? Use: docker compose -f docker-compose.build.yml up -d

services:
  cashpilot-ui:
    image: drumsergio/cashpilot:1.35
    pull_policy: always
    container_name: cashpilot-ui
    # Published on loopback by default: the dashboard can command the Docker-socket
    # worker, so it must not be exposed to your LAN/the internet out of the box.
    # To reach it from another machine, point CASHPILOT_BIND_ADDR at a specific
    # interface (e.g. a Tailscale/VPN IP) or, better, run an authenticating reverse
    # proxy in front. CASHPILOT_BIND_ADDR=0.0.0.0 publishes on all interfaces.
    ports:
      - "${CASHPILOT_BIND_ADDR:-127.0.0.1}:8080:8080"
    volumes:
      - cashpilot_data:/data
      - cashpilot_fleet:/fleet
    environment:
      - TZ=${TZ:-UTC}
      - CASHPILOT_SECRET_KEY=${CASHPILOT_SECRET_KEY:-}
      # Optional: only needed to RESTORE credentials onto a fresh volume.
      # An existing /data/.fernet_key always wins, so setting this is safe.
      - CASHPILOT_ENCRYPTION_KEY=${CASHPILOT_ENCRYPTION_KEY:-}
      - CASHPILOT_ALLOW_EPHEMERAL_KEY=${CASHPILOT_ALLOW_EPHEMERAL_KEY:-false}
      - CASHPILOT_API_KEY=${CASHPILOT_API_KEY:-}
      - CASHPILOT_ADMIN_API_KEY=${CASHPILOT_ADMIN_API_KEY:-}
    init: true
    # The image declares its own HEALTHCHECK, so `docker ps` will show
    # "(unhealthy)" when the app stops answering. NOTHING ACTS ON THAT BY
    # DEFAULT: `restart: unless-stopped` restarts a container that EXITS, and a
    # container failing its healthcheck is still running. It will sit there
    # unhealthy indefinitely. (Docker Swarm reschedules unhealthy tasks; plain
    # Compose does not.)
    #
    # To act on it, either alert on it -- see the CashPilotDown rule in
    # docs/guides/prometheus-metrics.md -- or add an autoheal sidecar, which is
    # documented in the same place along with the fact that it needs the Docker
    # socket, i.e. root on the host.
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:size=64M

  cashpilot-worker:
    image: drumsergio/cashpilot-worker:1.35
    pull_policy: always
    container_name: cashpilot-worker
    expose:
      - "8081"
      # GPU passthrough. COMMENTED OUT ON PURPOSE: Docker refuses to start a
      # container when a listed device does not exist, so uncommenting this on a
      # host with no GPU breaks the worker outright (verified: `docker run
      # --device /dev/does-not-exist` fails).
      #
      # Uncomment on a Linux host that HAS a GPU. Without it the worker cannot
      # see the hardware, so CashPilot reports the GPU as *unknown* and cannot
      # tell you whether Salad, Nosana, io.net or Vast.ai would earn here. Worse,
      # a GPU service deployed without the device starts, looks healthy, and
      # earns nothing -- the same shape as the Mysterium /dev/net/tun failure.
      #
      # Intel / AMD -- the kernel exposes the GPU as a DRM render node:
      # devices:
      #   - /dev/dri:/dev/dri
      #
      # NVIDIA is a DIFFERENT mechanism. /dev/dri does nothing for it, and
      # installing the NVIDIA Container Toolkit is only the prerequisite -- the
      # toolkit alone does NOT hand the GPU to a Compose service. You must also
      # ask for it, with EITHER of these (both are Compose-spec attributes):
      #
      # gpus: all
      #
      # deploy:
      #   resources:
      #     reservations:
      #       devices:
      #         - driver: nvidia
      #           count: all
      #           capabilities: [gpu]
      #
      # Both of these also fail on a host with no NVIDIA GPU (verified: exit 1,
      # "could not select device driver"), which is why they are commented out
      # too. Pick ONE; do not set both.
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      # SEPARATE FROM THE UI'S /data, DELIBERATELY. This container has the
      # Docker socket, which is root on the host. The UI's /data holds
      # cashpilot.db and .fernet_key -- the credential store and the only key
      # that can decrypt it -- and the component that can start any container
      # on the machine has no business being able to read them.
      #
      # /fleet IS shared on purpose: it holds only the shared enrolment key,
      # which both sides need by definition.
      #
      # Consolidating these into one volume is the obvious "simplification" and
      # it silently removes that boundary. tests/test_beads_batch_69.py fails if
      # anyone does.
      - cashpilot_worker_data:/data
      - cashpilot_fleet:/fleet
    environment:
      - TZ=${TZ:-UTC}
      - CASHPILOT_UI_URL=http://cashpilot-ui:8080
      - CASHPILOT_API_KEY=${CASHPILOT_API_KEY:-}
      - CASHPILOT_WORKER_NAME=local
    init: true
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:size=64M

volumes:
  cashpilot_data:
  cashpilot_worker_data:
  cashpilot_fleet:

This is the real file

The block above is included verbatim from docker-compose.yml in the repository, so it cannot drift from what actually ships. Earlier, a hand-copied version of it published the worker's Docker-socket API on every interface and pinned :latest — both of which the security defaults page tells you not to do.

Docker Socket Access

The worker container requires access to /var/run/docker.sock to manage service containers. This grants the worker significant privileges on the host. Run CashPilot on a dedicated machine or VLAN for best security.

Secret Key Persistence

Credentials are encrypted with CASHPILOT_ENCRYPTION_KEY, not CASHPILOT_SECRET_KEY — the latter only signs login sessions. The encryption key is auto-generated on first run and stored at /data/.fernet_key. Back that file up. If the volume is recreated without it, a fresh key is generated and every stored credential becomes permanently unreadable. Setting CASHPILOT_ENCRYPTION_KEY is for restoring that backup; the key file always wins, so setting it on a healthy instance changes nothing.

Passwords and secrets in the UI

Change your own password any time from the avatar menu -> Change password (available to all roles); this signs out your other sessions. In Settings, stored secrets are write-only: enter a value to change it, or leave the field blank to keep the existing one. Saved credentials are never sent back to the browser.

Updating CashPilot

The published images use floating tags, so updating is just a pull + recreate:

docker compose pull
docker compose up -d

docker compose pull fetches the newest published image and up -d recreates only the containers whose image changed. The shipped compose files set pull_policy: always, so even a bare docker compose up -d will pull the latest image first.

You do not need to rebuild

CashPilot ships prebuilt multi-arch images on Docker Hub (drumsergio/cashpilot, drumsergio/cashpilot-worker). docker compose build / --build is only relevant if you deliberately build from source with docker-compose.build.yml. For normal installs, pull + up -d is the complete and correct update procedure.

Pinning a specific version

:latest always tracks the newest release. To stay on a fixed version, replace the tag (e.g. drumsergio/cashpilot:0.6.13) and remove pull_policy: always. Browse available tags on Docker Hub. The minor tag (e.g. :0.6) tracks the latest patch within that minor series.

Automating updates (optional)

If you want hands-off updates, point a scheduler at the same two commands — for example a daily cron entry:

0 4 * * *  cd /path/to/cashpilot && docker compose pull && docker compose up -d

Or run an auto-updater such as Watchtower or Diun against the CashPilot containers. These are entirely optional — CashPilot does not bundle an updater.

Supported Services

CashPilot tracks 49 services across four categories:

  • Bandwidth Sharing (22 services) -- Share your internet bandwidth for passive income
  • DePIN (20 services) -- Decentralized physical infrastructure networks
  • GPU Compute (6 services) -- Rent out your GPU for AI and compute workloads
  • Storage (1 service) -- Share disk space on decentralized storage networks

Of these, 16 services can be deployed and managed automatically via Docker. The rest are browser extension or desktop-only services tracked in the catalog with signup links and earning estimates.

Browse the full catalog in the Service Guides section.

Next Steps