How to Run Your Own Monad Full Node

A Monad full node is three processes working together — monad-bft (consensus), monad-execution (the parallel EVM), and monad-rpc(the RPC server) — reflecting Monad’s pipelined architecture. It gives you a private endpoint on chain ID 143 with no rate limits. The headline constraint to understand before you start: Monad nodes must run on bare metal— cloud VMs aren’t officially supported, because the sub-second consensus leaves no room for virtualization overhead. This guide follows the official process on Ubuntu 24.04.

Hardware requirements

Monad favors high clock speed over core count, and needs two separate NVMe drives— one dedicated raw device for the state database (TrieDB), one for consensus data and the OS.

ResourceFull nodeNotes
CPU16 cores, 4.5 GHz+ basee.g. Ryzen 9950X / 7950X, EPYC 4584PX. Disable HyperThreading/SMT in BIOS.
RAM32 GB+
TrieDB disk2 TB PCIe Gen4×4 NVMeDedicated raw device (no filesystem) for execution state.
MonadBFT / OS disk500 GB PCIe Gen4×4 NVMeConsensus data + operating system.
Bandwidth100 Mbit/s300 Mbit/s for validators.
EnvironmentBare metal onlyCloud/virtualized (AWS/GCP/Azure) not supported — timing-sensitive.

OS: Ubuntu 24.04+ with Linux kernel ≥ 6.8.0.60. Note the known-bad kernels: avoid v6.8.0.56–59 (cause hangs) and v6.8.0.136–139 (fail to start). This kernel sensitivity is real — pin a known-good version.

Step 1 — Prepare the TrieDB disk

The state DB uses a dedicated raw NVMe device (not a mounted filesystem). Identify the drive, give it a GPT partition, expose it via a stable /dev/triedb symlink, and confirm 512-byte LBA formatting:

sudo apt update && sudo apt install -y nvme-cli
nvme list                                  # find the dedicated TrieDB drive
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL   # confirm it has NO mountpoint

TRIEDB_DRIVE=/dev/nvme1n1                   # <-- set to YOUR device
sudo parted $TRIEDB_DRIVE mklabel gpt
sudo parted $TRIEDB_DRIVE mkpart triedb 0% 100%

# stable symlink -> /dev/triedb
PARTUUID=$(lsblk -o PARTUUID $TRIEDB_DRIVE | tail -n 1)
echo "ENV{ID_PART_ENTRY_UUID}==\"$PARTUUID\", MODE=\"0666\", SYMLINK+=\"triedb\"" \
  | sudo tee /etc/udev/rules.d/99-triedb.rules
sudo udevadm trigger && sudo udevadm control --reload && sudo udevadm settle

# must be 512-byte LBA "in use"; if not: sudo nvme format --lbaf=0 $TRIEDB_DRIVE
sudo nvme id-ns -H $TRIEDB_DRIVE | grep 'LBA Format' | grep 'in use'

Step 2 — Install the monad package

Monad ships as a Debian package from the Category Labs APT repo. Add the repo + signing key and install the pinned mainnet version (check the docs for the current version number), then hold it so it won’t auto-upgrade:

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl nvme-cli aria2 jq

cat <<EOF | sudo tee /etc/apt/sources.list.d/category-labs.sources
Types: deb
URIs: https://pkg.category.xyz/
Suites: noble
Components: main
Signed-By: /etc/apt/keyrings/category-labs.gpg
EOF
curl -fsSL https://pkg.category.xyz/keys/public-key.asc \
  | sudo gpg --dearmor --yes -o /etc/apt/keyrings/category-labs.gpg

sudo apt update
sudo apt install -y monad=0.15.1     # <-- use the current mainnet version
sudo apt-mark hold monad

Step 3 — User, directories, and config

Create the monad user and directory tree, then pull the mainnet full-node config templates and generate the node keys (a full node needs an identity for peer discovery). Back up the keystore output somewhere safe:

sudo useradd -m -s /bin/bash monad
sudo mkdir -p /home/monad/monad-bft/config/forkpoint \
              /home/monad/monad-bft/config/validators \
              /home/monad/monad-bft/ledger /opt/monad/backup

MF=https://bucket.monadinfra.com
sudo curl -o /home/monad/.env $MF/config/mainnet/latest/.env.example
sudo curl -o /home/monad/monad-bft/config/node.toml \
  $MF/config/mainnet/latest/full-node-node.toml

# keystore password + keys
sudo sed -i "s|^KEYSTORE_PASSWORD=\$|KEYSTORE_PASSWORD='$(openssl rand -base64 32)'|" /home/monad/.env
source /home/monad/.env
monad-keystore create --key-type secp --keystore-path /home/monad/monad-bft/config/id-secp \
  --password "${KEYSTORE_PASSWORD}" > /opt/monad/backup/secp-backup
monad-keystore create --key-type bls  --keystore-path /home/monad/monad-bft/config/id-bls \
  --password "${KEYSTORE_PASSWORD}" > /opt/monad/backup/bls-backup

Then edit /home/monad/monad-bft/config/node.toml: set the beneficiary to the burn address 0x0000000000000000000000000000000000000000 (full nodes don’t earn), give it a unique node_name, set enable_client = true under [fullnode_raptorcast] and expand_to_group = true under [statesync], then sign a name record for peer discovery (monad-sign-name-record with your public IP and ports 8000/8001) and paste the result into [peer_discovery].

Step 4 — Firewall, init, and start

Open the P2P ports, initialize the TrieDB (a one-time format service), then enable the three node services:

sudo ufw allow ssh && sudo ufw allow 8000 && sudo ufw allow 8001 && sudo ufw enable
sudo iptables -I INPUT -p udp --dport 8000 -m length --length 0:1400 -j DROP   # UDP spam guard

sudo chown -R monad:monad /home/monad/

# one-time TrieDB format
sudo systemctl start monad-mpt
sudo journalctl -u monad-mpt -n 14 -o cat

# start consensus + execution + rpc
sudo systemctl enable monad-bft monad-execution monad-rpc
sudo systemctl start  monad-bft monad-execution monad-rpc

Step 5 — Verify

Watch the three services converge, then confirm the RPC is answering on chain ID 143 (0x8f):

journalctl -u monad-bft -f          # consensus
journalctl -u monad-execution -f    # parallel EVM
journalctl -u monad-rpc -f          # RPC server

# once monad-rpc is serving (use the RPC port from your node.toml):
curl http://localhost:8080 -s -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
# -> {"jsonrpc":"2.0","id":1,"result":"0x8f"}   (143 = Monad)

Confirm the node is actually at the tip (not just responding) with the checks in Is your RPC node actually at the chain tip?— compare its block height to a reference and watch it advance. Keep the RPC bound to localhost and put a firewall/proxy in front before exposing it.

The honest part: what running it actually costs

Monad is one of the harder nodes to self-host, for reasons specific to its design:

  • No cloud option. Bare-metal-only means you can’t spin up a cheap VM — you’re renting or buying a dedicated high-clock machine with two Gen4 NVMe drives. That’s a real monthly floor, not a hobby cost.
  • It’s finicky. Specific kernel versions hang or fail to start, SMT must be off, and the state DB wants a raw 512-byte-LBA device. Getting the box right is most of the work.
  • It’s a young, fast-moving chain. Expect frequent releases and coordinated upgrades with real node-recovery procedures — you’re on the hook to keep up.
  • One node is a single point of failure. Any production use needs redundancy — double the hardware, plus a load balancer and health checks.
  • See the full trade-off in Self-Hosted Node vs RPC Provider.

…or skip all of it

SwiftNodes runs load-balanced Monadnodes for you — flat-rate pricing (no per-call compute units), HTTP + WebSocket, no KYC — alongside 75+ other chains under one API key. No bare-metal shopping, no kernel-version roulette.

Grab a key at swiftnodes.io and point your app at https://rpc.swiftnodes.io/rpc/monad?key=YOUR_API_KEY.