Run with Docker
Quick start
mkdir -p ~/mina-state
docker run -d --name mina --restart unless-stopped \
--user "$(id -u):$(id -g)" \
-p 127.0.0.1:3000:3000 -p 8302:8302 \
-v ~/mina-state:/data \
--log-opt max-size=10m --log-opt max-file=3 \
ghcr.io/minanode/mina-rust:0.20.0
docker logs -f minaThe node starts as a devnet full node: it finds peers, syncs and follows the tip. Open the dashboard at http://127.0.0.1:3000. It keeps its state in ~/mina-state, so a new container picks up where the last one stopped. --user runs it as you, so the files there stay yours.
If a container called mina already exists, or another program uses port 3000 or 8302, run this node with its own name, ports and state directory, as below. Two nodes must not share a state directory (see reuse a native node's state). Then use the new name, here mina2, in the docker commands on this page, and open the dashboard at http://127.0.0.1:3010.
mkdir -p ~/mina-state-2
docker run -d --name mina2 --restart unless-stopped \
--user "$(id -u):$(id -g)" \
-p 127.0.0.1:3010:3000 -p 8312:8312 -e MINA_LIBP2P_PORT=8312 \
-v ~/mina-state-2:/data \
--log-opt max-size=10m --log-opt max-file=3 \
ghcr.io/minanode/mina-rust:0.20.0
docker logs -f mina2The image runs mina: docker exec mina mina runs a second mina command in the same container.
docker exec mina mina node statusCheck the image
docker pull prints the image's digest. Compare it with the one in the release notes; each release's notes list its image's digest.
docker pull ghcr.io/minanode/mina-rust:0.20.0
# The Digest line must match the release notes:
# Digest: sha256:66507e39fc2890ce37cf9d77b50517960b60887f5e655cfa2de5a35b2eeceab0A tag can be moved to another image; a digest can't. To run exactly the image you checked, use ghcr.io/minanode/mina-rust:0.20.0@sha256:66507e39fc2890ce37cf9d77b50517960b60887f5e655cfa2de5a35b2eeceab0 in place of ghcr.io/minanode/mina-rust:0.20.0 in the commands on this page. In Compose, set MINA_IMAGE to it in .env.
The GitHub CLI checks the image's build provenance. A passing check shows that the repository's Docker image workflow built the image from the signed release archives.
gh attestation verify oci://ghcr.io/minanode/mina-rust:0.20.0 -R minanode/mina-rustWith Docker Compose
The compose file runs the same node, with the dashboard on this computer only, its SNARK worker controlled from the dashboard, and the state in ./mina-state. It starts again after a reboot.
mkdir -p minanode && cd minanode
curl -fLO https://minanode.org/docker/docker-compose.yml
mkdir -p mina-state
printf 'UID=%s\nGID=%s\n' "$(id -u)" "$(id -g)" > .env
docker compose up -d
docker compose logs -fSettings such as the work directory, the image tag, more node options and the ports go in .env; the file lists them. Stop the node with docker compose stop and start it again with docker compose start.
If you ran the quick start first, stop and remove its container with docker stop mina && docker rm mina. Its state stays in ~/mina-state: to keep that chain, set MINA_STATE_DIR in .env to its full path, which echo ~/mina-state prints.
If a container called mina already exists, or another program uses port 3000 or 8302, set this node up in its own directory with its own settings in .env, as below. Each directory keeps its own mina-state, so the two nodes never share state. Without its own COMPOSE_PROJECT_NAME, a second docker compose up -d replaces the first node's container.
mkdir -p minanode2 && cd minanode2
curl -fLO https://minanode.org/docker/docker-compose.yml
mkdir -p mina-state
printf 'UID=%s\nGID=%s\n' "$(id -u)" "$(id -g)" > .env
cat >> .env <<'EOF'
COMPOSE_PROJECT_NAME=minanode2
MINA_CONTAINER_NAME=mina2
MINA_DASHBOARD_PORT=3010
MINA_LIBP2P_PORT=8312
EOF
docker compose up -d# A minanode devnet node in Docker: https://minanode.org/docs/docker/
#
# mkdir -p mina-state && docker compose up -d
# docker compose logs -f follow its log
# docker compose stop stop it; start it again with `start`
#
# A full node whose SNARK worker you start, stop and price from the
# dashboard, at http://localhost:3000 on this computer only. Its state (the
# synced chain, caches, peer identity and logs) is in ./mina-state.
#
# Optional settings go in a file named .env next to this one:
#
# UID=1000 and GID=1000 the user the node runs as, so the state dir
# stays yours: printf 'UID=%s\nGID=%s\n' \
# "$(id -u)" "$(id -g)" >> .env
# MINA_STATE_DIR=./mina-state the state dir; a native node's full path,
# e.g. /home/you/.mina (stop that node first)
# MINA_IMAGE=ghcr.io/minanode/mina-rust:0.20.0
# MINA_NODE_ARGS=--libp2p-external-ip 203.0.113.7
# more options for `mina node`
# MINA_LIBP2P_PORT=8302 the peer port, inside and outside
# MINA_DASHBOARD_PORT=3000 the dashboard's port on this computer
# MINA_CONTAINER_NAME=mina the container's name, if mina is taken
# COMPOSE_PROJECT_NAME=minanode another name for a second node, so the
# two don't replace each other's container
#
# A block producer adds docker-compose.block-producer.yml through COMPOSE_FILE
# in .env (see that file).
name: minanode
services:
mina:
image: ${MINA_IMAGE:-ghcr.io/minanode/mina-rust:0.20.0}
container_name: ${MINA_CONTAINER_NAME:-mina}
restart: unless-stopped
user: "${UID:-1000}:${GID:-1000}"
# Replaces the image's default command, `node --network devnet`.
# --allow-worker-control is safe only because the dashboard's port is
# published on 127.0.0.1 below: it decides where SNARK fees go.
command: >-
node --network devnet --allow-worker-control ${MINA_NODE_ARGS:-}
environment:
MINA_LIBP2P_PORT: ${MINA_LIBP2P_PORT:-8302}
ports:
# The dashboard and API, on 127.0.0.1 only. A bare "3000:3000" would
# open them to the network, past ufw's rules.
- "127.0.0.1:${MINA_DASHBOARD_PORT:-3000}:3000"
# Peers, on every interface on purpose: other nodes must reach it.
# Open it in your firewall or router to accept incoming peers.
- "${MINA_LIBP2P_PORT:-8302}:${MINA_LIBP2P_PORT:-8302}"
volumes:
# The state dir. Create it first: Docker would make a missing one
# owned by root, which the node can't write to.
- type: bind
source: ${MINA_STATE_DIR:-./mina-state}
target: /data
bind:
create_host_path: false
logging:
# Bound Docker's copy of the log. The node also keeps up to 200 MB of
# rotated logs in the state dir, for `mina report`.
driver: json-file
options:
max-size: 10m
max-file: "3"
Options and settings
Anything after the image name replaces the default command (node --network devnet), so give the whole command with your options. An option with an environment variable, listed by docker run --rm ghcr.io/minanode/mina-rust:0.20.0 node --help, may be set with -e instead.
docker run -d --name mina --restart unless-stopped \
--user "$(id -u):$(id -g)" \
-p 127.0.0.1:3000:3000 -p 8302:8302 \
-v ~/mina-state:/data -e MINA_VERBOSITY=debug \
--log-opt max-size=10m --log-opt max-file=3 \
ghcr.io/minanode/mina-rust:0.20.0 \
node --network devnet --libp2p-external-ip 203.0.113.7The image sets MINA_HOME=/data and HOME=/data, so everything the node writes goes to the work directory, and MINA_HTTP_ADDR=0.0.0.0, so the dashboard is reachable through the published port. Leave these as they are. In Compose, put more options in MINA_NODE_ARGS in .env.
Ports
- 127.0.0.1:3000:3000
- The dashboard and HTTP API, published on this computer only.
- 8302:8302
- Peers (TCP), on every network interface on purpose: other nodes must reach it. Open it in your firewall or router to accept incoming peers.
A plain -p 3000:3000 publishes the dashboard and the whole HTTP API on every network interface. On Linux, Docker's port rules come before ufw's, so ufw doesn't block a published port. On a server, keep 127.0.0.1:3000:3000 and open the dashboard through an SSH tunnel. To use another port on the host, change only the first number, as in 127.0.0.1:3010:3000. Worker control works at any port on 127.0.0.1.
Other listeners, such as --metrics-port, listen on 127.0.0.1 inside the container by default, which the host can't reach. Set their address option to 0.0.0.0 and publish the port on 127.0.0.1 as above.
Peers behind NAT
Behind a router, forward TCP port 8302 to this machine and give the node your public address with --libp2p-external-ip 203.0.113.7. Inside the container, the node sees only Docker's private network and cannot discover that address.
For another peer port, use the same number on both sides and tell the node: -p 8312:8312 -e MINA_LIBP2P_PORT=8312, or MINA_LIBP2P_PORT=8312 in Compose's .env.
The work directory
The directory mounted on /data is the node's work directory, as ~/.mina is for the native node. It outlives the container: docker rm, docker compose down and upgrades leave it in place.
- <chain id>/frontier/
- The saved chain state, with its ledgers: a restart picks up from it. Delete it and the node syncs from the start.
- ledger-accounts/
- The accounts of the staking, next epoch and root ledgers. The node replaces them at each start.
- p2p-secret-key
- The node's peer identity. Keep it for a stable peer id; it is private, like a key.
- .mina/circuit-blobs/
- The verifier indexes, built at the first start. Safe to delete: the next start builds them again.
- proof-cache/
- The SNARK pool's cached proofs. Safe to delete while the node is stopped.
- mina.log, mina.log.1, ...
- The logs. Safe to delete.
- snark-worker-included.json
- The worker's work that blocks included.
- bug-reports/
- Reports mina report saved.
To back it up, stop the node and copy the directory, for example with tar -czf mina-state.tar.gz -C ~ mina-state. Keep the copy private: it holds the peer identity. Never share a work directory between networks.
A named volume works too: -v mina-data:/data, without --user. The node then runs as the image's own user (uid 65532), and Docker keeps the files in its own storage.
If the node stops at start with Permission denied at /data, the directory isn't writable by the user it runs as. Create it yourself before the first start and run the container with --user "$(id -u):$(id -g)": if Docker creates a missing directory, root owns it.
Reuse a native node's state
A container may take over the state of a node you ran with the release binary, keeping its chain and peer id. Stop the native node first: never run two nodes on one work directory at the same time. Run the container as yourself, so the files keep their owner and the native binary can use the directory again later.
systemctl --user stop minanode.service # or Ctrl-C in its terminal
docker run -d --name mina --restart unless-stopped \
--user "$(id -u):$(id -g)" \
-p 127.0.0.1:3000:3000 -p 8302:8302 \
-v ~/.mina:/data \
--log-opt max-size=10m --log-opt max-file=3 \
ghcr.io/minanode/mina-rust:0.20.0In Compose, set MINA_STATE_DIR to the directory's full path, such as /srv/mina-state, and your UID and GID in .env.
Logs and bug reports
docker logs -f mina follows the log. The node also keeps its rotated log files in the work directory, which a new container keeps and mina report reads. The compose file and the --log-opt options in the docker run commands above limit Docker's own copy of the log to 30 MiB.
To collect a bug report, run mina report in the container. With -it it asks before printing the link to a pre-filled GitHub issue; without a terminal, add --yes. Or save it in the work directory with --output. Then follow reporting a bug.
In the container, ~ is /data: a report saved at ~/bug-reports/ is in ~/mina-state/bug-reports/ on the host.
docker exec -it mina mina report --no-browser
docker exec mina mina report -m "Sync stopped progressing" \
--no-browser --yes
docker exec mina mina report -m "Sync stopped progressing" \
--output /data/report.mdStop, start and upgrade
docker stop mina stops the node, and docker start mina starts it again from its saved chain. So does docker compose up -d when it recreates the container. With --restart unless-stopped, it also starts again after a reboot or a crash, unless you stopped it.
To upgrade, pull the new tag, check its digest as in check the image, and run a new container on the same work directory:
docker pull ghcr.io/minanode/mina-rust:NEW_VERSION
docker stop mina && docker rm mina
# Run the same docker run command as before, with the new tag.# In .env: MINA_IMAGE=ghcr.io/minanode/mina-rust:NEW_VERSION
docker compose pull
docker compose up -dHealth check
The image checks the node every minute with mina node status --check: healthy means synced, with a recent tip and at least one peer. The first 10 minutes don't count, to leave time to sync. docker ps shows the result. The check asks port 3000 inside the container, so change only the host side of the port.
docker inspect --format '{{.State.Health.Status}}' minaSNARK worker
The compose file starts the node with --allow-worker-control, so you start, stop and price the worker on the dashboard's SNARK worker page. To start the worker with the node instead, add --run-snarker B62q... --snarker-fee FEE to the command, or to MINA_NODE_ARGS. The worker needs no private key. Fees and choosing jobs are on the SNARK worker page.
Block producer
A producer adds docker-compose.block-producer.yml to the compose file. It mounts the key file read-only at /keys/producer-key, from ./keys/producer-key, outside the work directory, and reads the key's password from producer.env. It also waits 3 s into each won slot before sending the block. See block producer for the key and the producer page.
Put an existing key file at keys/producer-key, readable by you only. Or make a new one in the container:
mkdir -p -m 700 keys
printf "Key password: " && IFS= read -rs MINA_PRIVKEY_PASS && echo &&
export MINA_PRIVKEY_PASS
docker run --rm --user "$(id -u):$(id -g)" -e MINA_PRIVKEY_PASS \
-v "$PWD/keys:/keys" ghcr.io/minanode/mina-rust:0.20.0 \
wallet generate --output /keys/producer-keySave the password, readable by you only. COMPOSE_FILE in .env adds the producer file to every docker compose command. Check that the node can open the key: the wallet address command prints its public key. For rewards to another account, add MINA_NODE_ARGS=--coinbase-receiver B62q... to .env.
(umask 077 &&
printf "MINA_PRIVKEY_PASS='%s'\n" "$MINA_PRIVKEY_PASS" > producer.env)
unset MINA_PRIVKEY_PASScurl -fLO https://minanode.org/docker/docker-compose.block-producer.yml
echo 'COMPOSE_FILE=docker-compose.yml:docker-compose.block-producer.yml' >> .env
docker compose run --rm mina wallet address --from /keys/producer-key
docker compose up -dPut it only in producer.env: not in the compose files, not in .env, and not on a command line, where other users and your shell history can see it. Never commit producer.env or the key. The file holds the password in single quotes, so choose one without a single quote. Anyone who can run Docker commands on the machine can read the container's environment.
# A block producer, on top of docker-compose.yml. Add this line to .env, and
# every docker compose command uses both files:
#
# COMPOSE_FILE=docker-compose.yml:docker-compose.block-producer.yml
#
# Before starting (https://minanode.org/docs/docker/#block-producer):
# - the encrypted key file at ./keys/producer-key, outside the state dir;
# it is mounted read-only. Override with MINA_PRODUCER_KEY in .env.
# - its password in ./producer.env, readable by you only (chmod 600):
# MINA_PRIVKEY_PASS='your password'
# Never in this file, in .env or on a command line, and never committed.
# - for rewards to another account, in .env:
# MINA_NODE_ARGS=--coinbase-receiver B62q...
services:
mina:
# --block-inject-delay-ms 3000 sends each block at least 3 s into its
# slot, as the project's devnet producer does; the default is 1 s.
command: >-
node --network devnet --producer-key /keys/producer-key
--block-inject-delay-ms 3000 --allow-worker-control
${MINA_NODE_ARGS:-}
env_file:
- producer.env
volumes:
- type: bind
source: ${MINA_PRODUCER_KEY:-./keys/producer-key}
target: /keys/producer-key
read_only: true
bind:
create_host_path: false
macOS and Windows
Docker Desktop runs the Linux image in a virtual machine. On a Mac with Apple silicon, Docker pulls the ARM64 image, which runs natively; don't force --platform linux/amd64. On an Intel Mac or a Windows PC, it runs the x86-64 image, so the CPU needs BMI2 and ADX, as on Linux.
Give Docker Desktop's virtual machine the memory the requirements list for your role (Settings, Resources). On Windows, run the commands above in a WSL 2 terminal. The dashboard is at http://127.0.0.1:3000, as on Linux.
Build the image yourself
The image is built from the release archives, not from source. tools/release/docker/build-image.sh builds the same image from the downloaded release, with Docker or Podman. It checks SHA256SUMS.sig against the repository's docs/release/allowed_signers and the archives against SHA256SUMS. Then it builds the image for this machine as ghcr.io/minanode/mina-rust:0.20.0.
git clone https://github.com/minanode/mina-rust mina-rust
cd mina-rust
mkdir release && cd release
url=https://github.com/minanode/mina-rust/releases/download/v0.20.0
curl -fLO "$url/mina-0.20.0-linux-x86_64.tar.gz" \
-O "$url/mina-0.20.0-linux-arm64.tar.gz" \
-O "$url/SHA256SUMS" -O "$url/SHA256SUMS.sig"
cd ..
tools/release/docker/build-image.sh 0.20.0 release