docker-monero runs a Monero node and programmatic wallets in one Docker container. Give it persistent storage and an API token, then use HTTP or MCP to create wallets, check balances, inspect the chain and send XMR. Ordinary wallet apps can connect to its restricted node RPC too. The image is psyb0t/monero, and the actual money machinery is the official monerod and monero-wallet-rpc, not some cryptocurrency implementation I hallucinated in Go over lunch.
A node binary already exists. A wallet RPC binary already exists. What I wanted was a service I could operate from scripts and agents without arranging a fucking custody rota for their processes. Building that yourself means taking on these jobs:
- Get the right Monero binaries and verify the downloads.
- Configure a node, its network and its persistent chain database.
- Separate remote-wallet access from the node’s administrative RPC.
- Start, track, save and stop a wallet process for each open wallet.
- Put authenticated HTTP operations around that mess, with useful errors.
- Expose the same operations to an MCP client without writing a second implementation.
docker-monero packages those six jobs. It does not package free disk space, instant blockchain sync or financial judgement. Those remain outside the container, along with your regrettable purchases.
Start with money that does not exist
You do not need to download mainnet before finding out whether your integration is stupid. Regtest is a private offline chain where you generate blocks on demand. Start there. This example needs Docker, curl, OpenSSL and jq:
TOKEN=$(openssl rand -hex 32)
docker run -d --name monero-demo \
-p 127.0.0.1:8080:8080 \
-e DOCKER_MONERO_API_TOKEN="$TOKEN" \
-e DOCKER_MONERO_NETWORK=regtest \
psyb0t/monero:v0.2.3
curl -fsS http://127.0.0.1:8080/readyIf readiness returns 503, wait and retry it. That probe means the node’s RPC answers, not that a mainnet node has finished syncing. The API token needs at least 32 characters; the generated value above is 64 hexadecimal characters. Keep it private. This disposable example has no mounted storage and contains no real funds. Do not copy that storage decision into a real wallet deployment.
A small shell helper keeps the next requests readable:
xmr() {
curl --fail-with-body -sS \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
"$@"
}
xmr -X POST http://127.0.0.1:8080/v1/wallets \
-d '{"name":"demo","password":"EXAMPLE-DO-NOT-USE"}'
ADDR=$(xmr http://127.0.0.1:8080/v1/wallets/demo/addresses \
| jq -r .primaryAddress)
xmr -X POST http://127.0.0.1:8080/v1/node/regtest/generate-blocks \
-d "{\"count\":100,\"address\":\"$ADDR\"}"
xmr http://127.0.0.1:8080/v1/wallets/demo/status
xmr http://127.0.0.1:8080/v1/wallets/demo/balanceCreating the wallet leaves it open. The wallet scans in the background, so poll status until it catches up before expecting the balance to reflect the blocks. The regtest block-generation route refuses other networks. You cannot point it at mainnet and order a hundred blocks like takeaway. The example password is for this fake-money exercise only.
The download and the process babysitting
The image contains the official Monero 0.18.5.4 release binaries. Its build downloads the archive for amd64 or arm64, checks it against a pinned SHA-256 hash and keeps the two executables it needs. The runtime is Debian slim because those Monero binaries need glibc. A static Go service sits in front of them.
That service uses servicepack for its lifecycle. It starts the node, exposes the API and opens wallets on request. If monerod crashes after starting, the supervisor retries with backoff. If a wallet process dies, that wallet becomes closed and you open it again. Nobody quietly calls a dead child healthy because the parent process still has a pulse.
On shutdown, the HTTP listener drains, open wallets are saved and stopped, and the node gets time to flush its database. The Compose file allows three minutes. For a direct run, use docker stop -t 180 monero-demo. The default short Docker stop timeout is not a considerate way to interrupt a database flushing its guts to disk.
Real chain, real storage
For a persistent deployment, download the Compose file and environment example from the same release:
mkdir monero-node
cd monero-node
curl -fsSLO https://raw.githubusercontent.com/psyb0t/docker-monero/v0.2.3/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-monero/v0.2.3/.env.example -o .env
openssl rand -hex 32
${EDITOR:-vi} .env
mkdir -p ./data
sudo chown 10001:10001 ./data
docker compose up -dSet DOCKER_MONERO_API_TOKEN in .env to the random value you generated. Keep that file out of Git. The editor command uses your EDITOR or falls back to vi. The supplied Compose file follows psyb0t/monero:latest; change its image to a version tag or digest if you want a frozen deployment.
Compose mounts ./data at /data. The node stores its database in monerod/, wallets in wallets/, and UID/GID 10001 needs to write there. DOCKER_MONERO_DATA_PATH changes the host-side directory. Prepare that exact directory instead if you change it. Do not chmod the whole fucking project to 777 because one container user needs a data directory.
Mainnet is the default, pruning is on, and stagenet, testnet and regtest are also supported. A pruned node still downloads and verifies the chain; pruning reduces retained data, not the network to a decorative JSON file. Leave serious SSD capacity and time for the initial sync. Read GET /v1/node/info for progress rather than interpreting an open TCP port as completion.
The Compose service runs with a read-only root filesystem, a writable data mount and temporary storage, no Linux capabilities, and explicit CPU, memory and process limits. That is useful containment. It does not mean a stolen API token becomes harmless.
Cake Wallet is not your admin client
Port 18080 is Monero P2P. Port 18089 is restricted node RPC, the endpoint a remote wallet such as Cake Wallet uses as its custom node. Port 8080 is this project’s authenticated REST and MCP service. Metrics use 9091. These are different jobs, not four interchangeable ways into the same API.
The supplied Compose file publishes P2P and restricted RPC on every host interface, while the API and metrics are published only on host loopback. Unrestricted node RPC stays on loopback inside the container at 18081. If you do not want remote-wallet access, change the published ports before starting it. DOCKER_MONERO_PUBLIC_NODE=false means it is not advertised as a public node; it does not mean an already published 18089 is unreachable.
Restricted does not mean authenticated. Node RPC login is off unless you set DOCKER_MONERO_RPC_LOGIN=username:password. The password needs at least 16 characters, and the configuration rejects problematic formatting. Enter the same login in the remote wallet. This is separate from the API bearer token. HTTP digest login is not TLS either; use an appropriate private network or encrypted transport when you need it.
Connecting Cake Wallet to this node does not upload its seed into the programmatic wallet pool. It uses the node to access the chain. Creating or restoring a wallet through the service is a separate decision that puts wallet files on your server.
Wallets open, balances change, money leaves
Each open wallet gets its own monero-wallet-rpc child on an internal loopback port. The default limit is eight open wallets. Close one to save it and release that process; open it again with its password when needed. Wallets do not automatically reopen at boot.
The REST API supports creation, seed/key restoration, opening, closing, sync status, balances, addresses and subaddresses, and paginated transfer history. Restoring without a spend key can produce a view-only wallet. Set the restore height sensibly instead of asking the wallet to scan the entire history for no fucking reason. Seeds and keys belong in your trusted restore path, not an agent conversation.
Amounts travel as strings of atomic units. One XMR is 1000000000000 atomic units, so "1500000000000" means 1.5 XMR. That avoids squeezing exact amounts through JSON floating-point arithmetic. Responses also provide human-readable XMR strings.
POST /v1/wallets/{name}/transfers accepts destinations and amounts. Set dryRun: true to build and price without broadcasting. It is still a transfer operation, so transfer restrictions apply. Both the node and wallet must be caught up; otherwise the service returns NODE_NOT_SYNCED or WALLET_SYNCING. A dry run is not a payment, and a total balance is not necessarily an unlocked balance.
Wallet files are encrypted with their wallet passwords. An open wallet is already usable by the service, so the API token can authorize a send from it when transfers are enabled. There is no comforting little password prompt guaranteed to save you from handing that token to the wrong bastard. Back up wallet data and keep recovery seeds offline.
The agent gets tools, not a second wallet stack
Connect a Streamable HTTP MCP client to http://127.0.0.1:8080/mcp with the same bearer header as REST. Node info, block and transaction inspection, fee estimates, wallet lifecycle, balances, addresses and history use the same core services as HTTP. The native server is HTTP, not stdio. The agent integration includes a bridge for clients that need stdio.
The switches are explicit:
DOCKER_MONERO_READ_ONLY=trueblocks wallet creation/restoration, new subaddresses, sends and regtest block generation. Reads and opening/closing existing wallets still work.DOCKER_MONERO_ALLOW_TRANSFERS=falseblocks sends while leaving the other write operations available.DOCKER_MONERO_MCP_ALLOW_TRANSFERS=trueopts MCP into sending. Transfers must also be enabled globally, and read-only still wins.
Disabled MCP write tools are absent from discovery. The send tool is off by default, not a permanently missing feature. Seed and key restore deliberately stay REST-only. Mempool statistics are REST-only too, so “same core” does not mean every REST route has an identical MCP tool.
The docker-monero manual covers installation, node operation, wallets and the API. Its agent setup guide covers the skill and plugins. Prefer a native HTTP client when you have one. The documented stdio bridge uses mcp-remote, which prints custom headers to stderr at startup, including the token. Keep that output out of shared logs instead of discovering your wallet credential in a CI transcript later.
You still choose the chain, storage and access. But downloading binaries, wiring the node, separating its RPC, babysitting wallet children and building two front doors onto the same operations? Those chores can fuck off into the container. Your application can ask for a balance and get on with whatever it was supposed to do.
Get docker-monero on GitHub. The manual covers the API, configuration and agent setup.