A Docker Compose stack that runs a full Mintlayer node, a headless wallet, and an Astro-based web interface for wallet management.
Browser → Astro web GUI (port 4321)
│ server-side proxy — credentials never reach the browser
▼
wallet-rpc-daemon :3034 (internal)
│ JSON-RPC 2.0
▼
node-daemon :3030 (internal)
│ P2P
▼
Mintlayer network
Optional (--profile indexer):
node-daemon → api-blockchain-scanner-daemon → postgres → api-web-server :3000
▲
web-gui reads the REST API to power Token Management and Trading pages
Without the indexer: the Token Management and Trading pages are hidden. All other features (balance, send, receive, staking, address management) work without it.
- Docker with the Compose plugin (v2) or
docker-compose(v1)
The UI is built with Tailwind CSS 4 and modern CSS (cascade layers, color-mix()),
which sets the minimum supported browsers:
| Browser | Minimum version |
|---|---|
| Chrome / Edge | 111+ |
| Firefox | 128+ |
| Safari (macOS) | 16.4+ |
| Safari (iOS) | 16.4+ |
Older releases — in particular Safari 16.3 and earlier — may render, but are untested and unsupported.
./init.shThe script walks you through every option (network, wallet, passwords, ports, indexer, Pinata JWT), writes .env, and starts the stack. That's all you need for a first run.
Prebuilt images. CI publishes multi-arch (amd64 + arm64) images to
ghcr.io/mintlayer/web-gui (web-gui, bdk-wallet, btc-explorer), and
init.sh pulls them automatically — nothing is compiled on the target.
First-time pulls of a private package need docker login ghcr.io (or flip the
package to public in the GitHub UI). To build from source instead, run
docker compose build after checkout.
A Makefile wraps the most common Docker Compose commands:
| Target | What it does |
|---|---|
make up |
Start all services |
make down |
Stop and remove all containers (all profiles + orphans) |
make restart-gui |
Rebuild and restart only the web-gui container |
make build |
Rebuild all images without starting |
make logs |
Tail logs for all services |
make dev |
Start web-gui in dev mode with HMR — full stack including the indexer profile (node + wallet use prod images) |
make dev-build |
Rebuild the dev image (run after adding npm packages) |
make wallet-cli |
Open an interactive wallet-cli session |
make bitcoin |
Start the optional Bitcoin node + BTC wallet |
make bitcoin-cli CMD='getblockchaininfo' |
Run bitcoin-cli inside the node |
1. Copy and edit the environment file
cp .env.example .envOpen .env and set at minimum:
| Variable | What to change |
|---|---|
NETWORK |
mainnet or testnet |
NODE_RPC_PASSWORD |
anything strong |
WALLET_RPC_PASSWORD |
anything strong |
2. Create the data directory
mkdir -p mintlayer-dataNode chain data and wallet files are stored here. Inside containers it maps to /home/mintlayer/.
3. Start the stack
docker compose up -dServices started:
| Container | What it does |
|---|---|
node-daemon |
Syncs the Mintlayer blockchain (takes hours on first run) |
wallet-rpc-daemon |
Headless wallet — starts with no wallet loaded |
web-gui |
Web interface at http://localhost:4321 |
4. Create your wallet
Open http://localhost:4321/setup and use the Create new wallet form.
- Use
/home/mintlayer/my_walletas the path — it appears as./mintlayer-data/my_walleton the host - Write down the mnemonic shown — it will not be displayed again
After creating your wallet at http://localhost:4321/setup, the dashboard at http://localhost:4321 will show your balance and sync status.
Accessing the GUI from another machine (LAN)? The session cookie is marked
Secure, so browsers only accept it onlocalhostor HTTPS. Login from a plain-HTTP LAN hostname (e.g.http://192.168.1.50:4321) will silently fail. By default the GUI also binds to127.0.0.1only — setWEB_GUI_BIND=0.0.0.0in.envto expose it, and put a TLS reverse proxy in front for real remote access.
Sync time: balance and transaction history only appear once the node has fully synced. On first run this takes several hours for mainnet. The dashboard shows the current block height so you can track progress.
The indexer adds a PostgreSQL database, a blockchain scanner, and a REST API for querying blocks, transactions, and addresses.
docker compose --profile indexer up -dThe REST API is available at http://localhost:3000 (configurable via API_WEB_SERVER_PORT in .env).
Use a v1.4.1 (or newer) indexer. Older indexers still power the Token Management and Trading pages, but some features detect the version and switch off gracefully:
- Pending-transaction awareness: toasts for outgoing transactions verify against the indexer's mempool endpoint, so a stuck transaction shows as still pending instead of a false failure.
- Supply statistics & top holders on the token manage page (v1.4.1
/statisticsendpoints). - Daemon images are pinned in
.env(ML_*_DAEMON_IMAGE); the CI workflow builds them from the Mintlayer version set in.github/workflows/mintlayer-daemons.yml.
Upgrading from an indexer older than v1.4.1 requires a full indexer resync (storage format change); the node and wallet daemons should be upgraded together.
Adds a Bitcoin Core node and a built-in BTC wallet (balance, receive, send) to the web UI.
docker compose --profile bitcoin up -d # or: make bitcoinHow it works:
bitcoindprovides chain data and broadcasts transactions. It needs no host ports.bdk-walletis a light-wallet sidecar (BDK, BIP84) that holds the BTC keys and signs transactions locally. The web GUI talks to it over the internal Docker network.- The wallet seed is generated in the web UI and shown exactly once — back it up when prompted.
- All BTC API routes require a logged-in session; the sidecar is not reachable from outside.
Requirements and warnings
- Mainnet chain data is roughly 700 GB with the default
txindex=1. The first sync can take days. UseBITCOIN_NETWORK=testnet(orregtest) to try it out cheaply. - The BTC wallet is a hot wallet — keep only spending amounts on it.
- Pruning (
BITCOIN_PRUNE) is incompatible with the wallet's history sync; leave it off. - Bitcoin Core is pinned to 25.x: the BDK rpc backend cannot parse the
warningsformat used by Core 26+. Override only if you know what you are doing (BITCOIND_IMAGE).
# Start everything
make up # or: docker compose up -d
# Stop everything
make down # or: docker compose --profile indexer down --remove-orphans
# Watch logs
docker compose logs -f
docker compose logs -f wallet-rpc-daemon
# Interactive wallet CLI (connects to the running daemon)
make wallet-cli # or: docker compose --profile wallet_cli run --rm wallet-cli
# Restart after changing .env
docker compose restart
# Pull latest images
docker compose pull && docker compose up -d| Page | URL | Description |
|---|---|---|
| Dashboard | / |
Balance, sync status, staking state |
| Balances | /balances |
Detailed balance breakdown |
| Management | /management |
Wallet management overview |
| → Addresses | /management/addresses |
List and generate receive addresses |
| → Transactions | /management/transactions |
Transaction history |
| → UTXOs | /management/utxos |
UTXO list |
| → Wallet | /management/wallet |
Wallet settings and info |
| Send | /send |
Send ML to an address |
| Staking | /staking |
Staking status and instructions |
| Token Management | /token-management |
Issue and manage tokens — requires indexer |
| Trading | /trading |
DEX trading — requires indexer |
| Bitcoin | /bitcoin |
BTC balance, receive and send — requires bitcoin profile |
| Wallet setup | /setup |
Create or open a wallet |
Token Management and Trading are hidden when
INDEXER_ENABLED=falsein.env.
The quickest loop is make dev: it boots the full stack (node + wallet prod images, indexer
profile) with the web-gui source mounted for hot reload at http://localhost:4321.
On first use it generates .env.dev automatically: fixed development credentials (dev / dev
etc., from env.dev.example), your host UID/GID, and a fresh random SESSION_SECRET. The dev
stack binds the web UI to 127.0.0.1 and publishes no daemon ports, so those fixed credentials
are local-only by design — they are never valid for a real deployment (use ./init.sh, which
generates random secrets, for that). Delete .env.dev to re-roll the secret.
The daemon images (
ghcr.io/mintlayer/web-gui/node-daemon,wallet-rpc-daemon,api-blockchain-scanner-daemon,api-web-server) are private. Pull them afterdocker login ghcr.io(a GitHub PAT withread:packages), or build them locally with./build-core-images.shand runmake dev-localinstead.
To run the Astro app directly on the host against a running daemon:
cd app
npm install
export WALLET_RPC_URL=http://localhost:3034
export WALLET_RPC_USERNAME=wallet_user
export WALLET_RPC_PASSWORD=your_password
npm run dev
# → http://localhost:4321To expose the wallet RPC port to the host, uncomment the ports block for wallet-rpc-daemon in docker-compose.yml.
The image ships a Model Context Protocol server
(app/scripts/mcp-server.mjs, stdio) that exposes your wallet to AI assistants
such as Claude Desktop or Cursor.
Enable it in the web GUI: Management → Settings → MCP Server. Three tiers:
| Tier | Grants | Requires |
|---|---|---|
| read | balances, addresses, transactions, UTXOs, staking and order overviews | Enabled |
| actions | new address, start/stop staking, abandon transaction | Allow wallet actions |
| spend | send coins, sweep, token issuance/management, order trading | Allow fund-moving operations |
Granting the actions or spend tiers requires 2FA to be configured and a valid
authenticator code. Permissions are re-read from the prefs database on every
tool call, so changes apply immediately. Secret operations (seed phrase,
private-key unlock) are never available through MCP. A per-transaction send cap
can be set with the mcp.max_send_amount pref (decimal ML).
Client configuration (shown in the settings panel):
{
"mcpServers": {
"mintlayer-wallet": {
"command": "docker",
"args": ["compose", "-f", "/path/to/mintlayer-web-gui/docker-compose.yml",
"run", "--rm", "-T", "--no-deps",
"web-gui", "node", "scripts/mcp-server.mjs"]
}
}
}Tests: cd app && npx vitest run scripts/mcp-server.test.mjs scripts/mcp-permissions.test.mjs.
Go to Settings → Change password (/management/settings) while logged in.
If you are locked out and cannot log in, generate a new PBKDF2 hash and write it directly to the prefs database:
# 1. Generate a hash for your new password
NEW_HASH=$(docker run --rm node:22-alpine node -e "
const crypto = require('crypto');
const salt = crypto.randomBytes(16).toString('hex');
crypto.pbkdf2('YOUR_NEW_PASSWORD', salt, 100000, 64, 'sha512', (_, key) => {
process.stdout.write('pbkdf2:sha512:100000:' + salt + ':' + key.toString('hex'));
});
")
# 2. Write it to the database
docker run --rm \
-v "$(pwd)/mintlayer-data/prefs:/prefs" \
alpine sh -c "apk add -q sqlite && sqlite3 /prefs/mintlayer_prefs.sqlite \
\"INSERT OR REPLACE INTO prefs VALUES ('auth.password_hash', '\\\"${NEW_HASH}\\\"');\""Use the bundled update-totp script — it generates a new secret, shows a scannable QR code, and only saves after you confirm with a valid code:
# Inside a running container
docker compose exec web-gui node scripts/update-totp.mjs
# One-shot (stack does not need to be running)
docker run --rm -it \
-v "$(pwd)/mintlayer-data/prefs:/app/prefs" \
<web-gui-image> node scripts/update-totp.mjs
# On the host (if Node is available)
PREFS_DB_PATH=./mintlayer-data/prefs/mintlayer_prefs.sqlite \
node app/scripts/update-totp.mjsAfter saving, restart the web-gui container so the new secret takes effect:
docker compose restart web-gui- Run
./init.shor set strong passwords in.envbefore exposing this to any network. - Wallet RPC credentials are never sent to the browser — all calls are proxied server-side by the Astro app.
- Only a fixed allowlist of RPC methods is callable through the
/api/rpcendpoint. - The wallet RPC (3034) and node RPC (3030) ports are not exposed to the host by default.