· updated

How to run a Polygon (Matic) Mainnet Node with Docker

Tech Ethereum

Running a Matic Node seems to consist of two four parts: The the PoS node, a REST API server, a RabbitMQ server, and an EVM node. The PoS node is called heimdall as is the REST API server, and the EVM Node is called bor. According to the documentation we need to run heimdall first and let it sync…so let’s start there.

Also, a word of warning, I’m gonna assume you’re behind some kinda firewall here, don’t expose your node’s RPC/API to the world, duh. I’ll call out what the ports are for below.

# Make a directory to work in
mkdir -p /data/github
cd /data/github

# Clone the repo
git clone https://github.com/maticnetwork/heimdall.git
cd heimdall

# Checkout the mainnet release version
git checkout v0.2.4

# Copy the Dockerfile up a directory
cp docker/Dockerfile .

# Edit their shitty Dockerfile
sed -i 's#./logs#/root/heimdall/logs#' Dockerfile

# Build the docker image
docker build --tag heimdall:latest .

# Make a directory for heimdall data
mkdir -p /data/volumes/heimdall

# Init configs and stuff
docker run -it -v /data/volumes/heimdall:/root/.heimdalld heimdall:latest heimdalld init

# Overwrite the genesis.json file with the mainnet launch genesis.json file
wget https://raw.githubusercontent.com/maticnetwork/launch/master/mainnet-v1/without-sentry/heimdall/config/genesis.json -O /data/volumes/heimdall/config/genesis.json

You might say to yourself “I don’t want to checkout the mainnet release version, I want to be on the latest and greatest version!” but you’d be wrong because it doesn’t work…at all…at the time of writing. :shrug:

Now you need to modify some config files. There are 2 config files of importance here.

config.toml and heimdall-config.toml they’re both located at /data/volumes/heimdall/config/

The key parts to modify in config.toml are:

The key parts to modify in the heimdall-config.toml file are:

  • amqp_url
    • Point this to RabbitMQ server (we’ll run a docker called rabbitmq) so set it to:
      amqp_url = "amqp://guest:guest@rabbitmq:5672"

There’s also two other variables that I suggest editing LATER, after you’re up and running and in sync:

  • eth_rpc_url
    • This should point to an ETH1 endpoint, local or remote like Infura
    • I don’t think this does anything if you’re not validating though
  • bor_rpc_url
    • You can point this to your bor node after your bor node is in sync
    • I don’t think this does anything if you’re not validating though

You can pretty much ignore everything else, we’ll set some other stuff via command-line options. And obviously you haven’t created your bor node yet so you can come back to it later.

SNAPSHOTS

A quick word on sync time…it’s not fast. If you want to validate the entire chain (PoS (heimdall) and EVM (bor)) more power to you. If however, you just want to get up and running as fast as possible, the community offers database snapshot over on their new snapshot page.

https://snapshots.matic.today/

If you want to use a snapshot, now is the time to do that for heimdall. And while you’re waiting for heimdall to catch up, you can install the bor snapshot after you initialize it.

Heimdall Snapshot

Here is an example of how to download and extract the Heimdall snapshot in one line (this saves storage space by not having to download AND extract these large files). Otherwise just do your typical wget and then tar

# Download and tar extract via pipeline (linux magic)
wget -c https://matic-blockchain-snapshots.s3-accelerate.amazonaws.com/matic-mainnet/heimdall-snapshot-2022-01-02.tar.gz -O - | tar -xz -C /data/volumes/heimdall/data

Running Heimdall

# Run it / Make sure it's working
docker run -it -p "26656:26656" -p "26657:26657" -v /data/volumes/heimdall:/root/.heimdalld heimdall:latest heimdalld start

Even without having set proper eth_rpc_url or bor_rpc_url variables (or running the RabbitMQ server yet) it should start to sync the PoS network. However, you do need the other 2 service (REST API and RabbitMQ) for bor to work, so let’s set those up inside a docker-compose file.

Docker Compose

version: '3.4'

services:
  rabbitmq:
    container_name: rabbitmq
    image: rabbitmq:3-alpine
    ports:
      - "5672:5672" # RabbitMQ
    restart: unless-stopped
  
  heimdalld:
    container_name: heimdalld
    image: heimdall:latest
    build: /data/github/heimdall
    restart: unless-stopped
    volumes:
      - /data/volumes/heimdall:/root/.heimdalld
    ports:
      - "26656:26656" # P2P (TCP)
      - "26657:26657" # RPC (TCP)
    depends_on:
      - rabbitmq
    command:
      - heimdalld
      - start
      - --moniker=MyNodeName
      - --p2p.laddr=tcp://0.0.0.0:26656
      - --rpc.laddr=tcp://0.0.0.0:26657

  heimdallr:
    container_name: heimdallr
    image: heimdall:latest
    build: /data/github/heimdall
    restart: unless-stopped
    volumes:
      - /data/volumes/heimdall:/root/.heimdalld
    ports:
      - "1317:1317" # Heimdall REST API
    depends_on:
      - heimdalld
    command:
      - heimdalld
      - rest-server
      - --chain-id=137
      - --laddr=tcp://0.0.0.0:1317
      - --node=tcp://heimdalld:26657

Now if you run docker-compose up -d your docker container will start in the background. It’ll pull RabbitMQ and run that, then it will launch heimdalld the PoS node, and heimdallr the REST API server. Both heimdalld and heimdallr will read from the same config files so they’re mounted to the same location. As you can see from the config heimdallr points to heimdalld with the --node flag.

Wait until you’re in sync

How do you know if you’re in sync?

$ curl http://localhost:26657/status
{
  "jsonrpc": "2.0",
  "id": "",
  "result": {
    "node_info": {
      "protocol_version": {
        "p2p": "7",
        "block": "10",
        "app": "0"
      },
      "id": "d09d79f111a56284495dec4762056f801bdafd17",
      "listen_addr": "tcp://0.0.0.0:26656",
      "network": "heimdall-137",
      "version": "0.32.7",
      "channels": "4020212223303800",
      "moniker": "MysticRyuujin",
      "other": {
        "tx_index": "on",
        "rpc_address": "tcp://0.0.0.0:26657"
      }
    },
    "sync_info": {
      "latest_block_hash": "D081C8AEA93F78526E6088379819548D4ED780B4D2D24E405D744CB368817510",
      "latest_app_hash": "F43512329EC6E77C9FABC8D38FFD75747C4C787631829676AB55DB787399570D",
      "latest_block_height": "342083",
      "latest_block_time": "2020-06-21T09:43:49.695593592Z",
      "catching_up": true
    },
    "validator_info": {
      "address": "5FC7043EECEA93C19B55FD33760E45CE246DCB93",
      "pub_key": {
        "type": "tendermint/PubKeySecp256k1",
        "value": "BA5qrjvWfVGHa8TWH47gMIq3O0dlIl+VWENts7kN7fZcP06A7y6Lf/yXuFxdcQ6sfEX8eFsv6MmttQ2eG9XA3qk="
      },
      "voting_power": "0"
    }
  }
}

Looking at the above information we can see that our node has this info:

      "latest_block_time": "2020-06-21T09:43:49.695593592Z",
      "catching_up": true

When that says false and the timestamp makes sense, you’re in sync

bor? Boring

Bor is basically just a Geth clone, the setup is similar

# Re-use our github folder
cd /data/github

# Clone the repo
git clone https://github.com/maticnetwork/bor.git
cd bor

# This is the current recommended version
# git checkout v0.2.13

# Build the docker image
docker build --tag bor:latest .

# Make a directory for bor data
mkdir -p /data/volumes/bor

# We need to download the genesis.json file FIRST this time
wget https://raw.githubusercontent.com/maticnetwork/launch/master/mainnet-v1/without-sentry/bor/genesis.json -O /data/volumes/bor/genesis.json

# Initialize bor with genesis file
docker run -it -v /data/volumes/bor:/datadir bor:latest bor --datadir /datadir init /datadir/genesis.json

Now would be a good time to install the bor snapshot if you’re so inclined.

Bor Snapshot

Here’s an example of how to grab the bor snapshot, double check the target folder is correct e.g. /data/volumes/bor/bor/chaindata

# Download and tar extract via pipeline (linux magic)
wget -c https://matic-blockchain-snapshots.s3-accelerate.amazonaws.com/matic-mainnet/bor-pruned-snapshot-2022-01-10.tar.gz -O - | tar -xz -C /data/volumes/bor/bor/chaindata

Now it’s a matter of running bor node the same way you would any other Geth node, with a couple of extras.

  • They provide you with a list of bootnodes HERE
  • You need to provide the --bor.heimdall flag and point it to the REST API server
    • --bor.heimdall=http://heimdallr:1317
  • You need to specifically include --networkid=137
  • Turn on the bor API under --http.api and --ws.api if you use websocket
  • Set gas targets and limits (idk why but their documentation does this everywhere)
  • Newer versions of bor disable logs, use --bor.logs to re-enable it
  • YOU MUST RUN IN FULL SYNC MODE

Here’s what that all looks like in a Docker-Compose. Remember: HEIMDALLD NEEDS TO BE IN SYNC BEFORE YOU RUN BOR.

Docker Compose

version: '3.4'

services:
  bor:
    container_name: bor
    image: bor:latest
    build: /data/github/bor
    volumes:
        - /data/volumes/bor:/datadir
    ports:
        - "8545:8545" # RPC
        - "30303:30303" # Peers (TCP)
        - "30303:30303/udp" # Peers (UDP)
    command:
        - bor
        - --syncmode=full
        - --datadir=/datadir
        - --networkid=137
        - --bor.heimdall=http://heimdallr:1317
        - --bor.logs
        - --miner.gaslimit=200000000
        - --miner.gastarget=20000000
        - --http
        - --http.addr=0.0.0.0
        - --http.port=8545
        - --http.api=eth,net,web3,admin,debug,bor
        - --http.corsdomain=*
        - --http.vhosts=*
        - --ws
        - --ws.addr=0.0.0.0
        - --ws.port=8545
        - --ws.api=eth,net,web3,admin,debug,bor
        - --ws.origins=*
        - --ipcdisable
        - --nousb
        - --bootnodes=enode://0cb82b395094ee4a2915e9714894627de9ed8498fb881cec6db7c65e8b9a5bd7f2f25cc84e71e89d0947e51c76e85d0847de848c7782b13c0255247a6758178c@44.232.55.71:30303,enode://88116f4295f5a31538ae409e4d44ad40d22e44ee9342869e7d68bdec55b0f83c1530355ce8b41fbec0928a7d75a5745d528450d30aec92066ab6ba1ee351d710@159.203.9.164:30303

You can customize this to support whatever security you need or don’t need. Just remember to take care when exposing RPC and API ports.

A note about Docker-Compose Networking

By default docker-compose creates a single network for each “docker-compose.yml” file / app definition, whatever you want to call it. So if you’ve followed this tutorial exactly, you’d end up with a network containing bor and another network containing heimdalld, heimdallr, and rabbitmq. They won’t talk to each other by default. You can either a) place the bor service into the same docker-compose file as the other services or b) create an external network and put everything into that network.

https://docs.docker.com/compose/networking/

Option A is the easiest, however, if you want to do Option B, here’s a simple way to do that:

# Run this command to create a network called polygon-network
docker network create -d bridge polygon-network

# Add this to the bottom of both docker-compose files
networks:
  default:
    external: true
    name: polygon-network

Help – I didn’t understand any of that!

Yeah – Matic/Polygon is not the easiest network to run. Which is why there are RPC as a Service providers like QuickNode who can get you up and running in seconds without any of those pesky rate limits that the public RPC providers include (assuming you get a Pro or Scale plan).

I understood all of that and/or you’re doing it wrong!

Then you should come work for QuickNode! https://hire.clarify.so/company/quiknode-inc

Disclaimer: This is my personal blog, it is not actually affiliated with QuickNode - I just thought I'd give a shout out since my employer is hiring and we need super stars!
← All posts