This guide is for the real moment when you need to move a Monad node from one server to another. It is written in plain language first, then in commands. If you follow the order exactly, the logic is simple: prepare the new server, prove it is healthy, stop the old identity, move the identity once, re-sign discovery for the new IP, then verify everything before switching public links.
In Monad terms, the “passport” is your live node identity: the SECP keystore, the BLS keystore, the matching password, and a fresh signed discovery record for the new public IP.
There are two different things people often confuse:
You can prepare the new server early. You should move the real identity only once, in a short and controlled window.
Never run the same id-secp and id-bls on two servers at the same time.
Replace the placeholders below before you start typing commands.
OLD_HOST=195.3.223.117
OLD_USER=monad-admin
NEW_HOST=38.96.30.106
NEW_USER=admin
NEW_PUBLIC_IP=38.96.30.106
NEW_NODE_NAME=full_proofline-jp-01
NEW_SEQ=2
/home/monad/monad-bft/config/id-secp/home/monad/monad-bft/config/id-bls/home/monad/.env with KEYSTORE_PASSWORD/home/monad/monad-bft/config/node.tomlFirst confirm that the current node is healthy and record its identity data. This gives you a clean before/after reference and makes rollback much easier.
sudo systemctl status monad-bft monad-execution monad-rpc --no-pager
You want all three services to be active (running).
curl -s -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://127.0.0.1:8080
curl -s -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
http://127.0.0.1:8080
The node should answer. If the second command returns that the node is actively syncing or obviously unhealthy, stop and fix that first.
hostnamectl
ip addr show
ip route
grep -E "node_name|self_address|self_record_seq_num|self_name_record_sig" \
/home/monad/monad-bft/config/node.toml
sudo -u monad bash -lc '
source /home/monad/.env
monad-keystore recover \
--password "$KEYSTORE_PASSWORD" \
--keystore-path /home/monad/monad-bft/config/id-secp \
--key-type secp | grep "Secp public key"
monad-keystore recover \
--password "$KEYSTORE_PASSWORD" \
--keystore-path /home/monad/monad-bft/config/id-bls \
--key-type bls | grep "BLS public key"
'
The new server should already be prepared and synced enough to be a real candidate. If it is not ready, this is not a migration window yet.
hostnamectl
uname -r
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL
ip addr show
ip route
df -h
sudo ls -lah /home/monad
sudo ls -lah /home/monad/monad-bft/config
sudo ls -l /dev/triedb
sudo ss -ltnup | egrep ":8000|:8001|:8080|:8889" || true
sudo ufw status || true
sudo systemctl daemon-reload
sudo systemctl list-unit-files | grep monad
Before the cutover window, make sure you have secure copies of the identity bundle from the old server.
id-secpid-bls.envnode.tomlIf you copy files through a workstation, your local staging folder should look like this:
migration-staging/
id-secp
id-bls
.env
node.toml
This is the moment when you stop the old node completely. Do this before placing the real identity on the new server.
sudo systemctl stop monad-rpc
sudo systemctl stop monad-execution
sudo systemctl stop monad-bft
systemctl is-active monad-rpc monad-execution monad-bft
Expected result: all three should return inactive or failed, but not
active.
Place these files on the new server:
id-secp -> /home/monad/monad-bft/config/id-secpid-bls -> /home/monad/monad-bft/config/id-bls.env -> /home/monad/.envnode.toml -> /home/monad/monad-bft/config/node.tomlExample from a local workstation:
scp id-secp NEW_USER@NEW_HOST:/tmp/id-secp
scp id-bls NEW_USER@NEW_HOST:/tmp/id-bls
scp .env NEW_USER@NEW_HOST:/tmp/.env
scp node.toml NEW_USER@NEW_HOST:/tmp/node.toml
ssh NEW_USER@NEW_HOST
sudo mv /tmp/id-secp /home/monad/monad-bft/config/id-secp
sudo mv /tmp/id-bls /home/monad/monad-bft/config/id-bls
sudo mv /tmp/.env /home/monad/.env
sudo mv /tmp/node.toml /home/monad/monad-bft/config/node.toml
sudo chown monad:monad /home/monad/.env
sudo chown monad:monad /home/monad/monad-bft/config/id-secp
sudo chown monad:monad /home/monad/monad-bft/config/id-bls
sudo chown monad:monad /home/monad/monad-bft/config/node.toml
sudo chmod 600 /home/monad/.env
sudo chmod 600 /home/monad/monad-bft/config/id-secp
sudo chmod 600 /home/monad/monad-bft/config/id-bls
sudo chmod 600 /home/monad/monad-bft/config/node.toml
Do not skip this. Bad ownership and permissions are one of the easiest ways to break the cutover.
node.toml for the new serverBefore starting Monad, confirm these values in the new server config:
node_name = "full_proofline-jp-01"self_address = "38.96.30.106:8000"self_record_seq_num = 2self_name_record_sig must be replaced with a fresh signature for the new IPsudo -u monad bash -lc '
source /home/monad/.env
monad-sign-name-record \
--address 38.96.30.106:8000 \
--authenticated-udp-port 8001 \
--keystore-path /home/monad/monad-bft/config/id-secp \
--password "$KEYSTORE_PASSWORD" \
--self-record-seq-num 2
'
Copy the generated signature into the peer_discovery section of
/home/monad/monad-bft/config/node.toml.
Then confirm the final values:
grep -E "self_address|self_record_seq_num|self_name_record_sig" \
/home/monad/monad-bft/config/node.toml
sudo systemctl start monad-bft
sudo systemctl start monad-execution
sudo systemctl start monad-rpc
sudo systemctl status monad-bft monad-execution monad-rpc --no-pager
Do not rush to update public links yet. First verify the node itself.
curl -s -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://127.0.0.1:8080
curl -s -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
http://127.0.0.1:8080
ss -ltnup | egrep ":8000|:8001|:8080|:8889"
journalctl -u monad-bft -n 80 --no-pager
journalctl -u monad-execution -n 80 --no-pager
journalctl -u monad-rpc -n 80 --no-pager
sudo -u monad bash -lc '
source /home/monad/.env
monad-keystore recover \
--password "$KEYSTORE_PASSWORD" \
--keystore-path /home/monad/monad-bft/config/id-secp \
--key-type secp | grep "Secp public key"
monad-keystore recover \
--password "$KEYSTORE_PASSWORD" \
--keystore-path /home/monad/monad-bft/config/id-bls \
--key-type bls | grep "BLS public key"
'
systemctl show monad-bft monad-execution monad-rpc -p NRestarts
watch -n 5 "curl -s -H 'Content-Type: application/json' \
--data '{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1}' \
http://127.0.0.1:8080"
A small positive blockDifference like 1 or 2 can still be
normal in real operation. Do not interpret it alone. Always look at service state, peer count, RPC
health, and whether the local block height keeps moving.
After the new node is clearly healthy, switch:
If the new node cannot stay healthy, rollback should be immediate and boring.
# on the new server
sudo systemctl stop monad-rpc
sudo systemctl stop monad-execution
sudo systemctl stop monad-bft
# on the old server
sudo systemctl start monad-bft
sudo systemctl start monad-execution
sudo systemctl start monad-rpc
A good migration is boring. The new host is prepared first, the identity is moved once, the checks are explicit, the public cutover happens only after health is proven, and rollback remains possible until the new primary has already survived real observation time.