DEV Community

Cristian Mendoza
Cristian Mendoza

Posted on

How to Deploy Avalanche Validator Nodes (Local Network) – Step-by-Step Guide

Do you want to experiment, develop, or test subnets on Avalanche without relying on public nodes or mainnet? This guide will teach you how to spin up your own local Avalanche network with multiple validator nodes, each with its own unique identity and properly configured for maximum compatibility and control.


Table of Contents

  1. Prerequisites
  2. Environment Preparation & Base File Generation
  3. Generating Staking Certificates for Each Node
  4. Starting Nodes and Obtaining NodeID/BLS
  5. Building the genesis.json
  6. Docker Compose Example
  7. Cleaning and Starting the Network
  8. Verifying the Network
  9. Advantages of Running Your Own Validator Nodes
  10. Notes & Tips

1. Prerequisites

  • Docker and Docker Compose installed.
  • Terminal access (Linux, macOS, WSL, or Windows with proper Docker file paths).
  • (Optional) Text editor to modify JSON/YAML files.

2. Environment Preparation & Base File Generation

2.1. (Recommended) Generate Base Config Files with AvalancheGo and subnet-cli

Before personalizing your network, it's best to initiate a subnet and run AvalancheGo with the official tools (subnet-cli). This step automatically generates genesis.json and config.json files with the correct parameters and valid syntax.

Why do this?

  • You get files with all required sections and fields.
  • You start from a valid configuration, minimizing format errors.
  • It's much easier to adapt than to build a genesis from scratch.

Typical steps:

  1. Install AvalancheGo and subnet-cli (if not done already):

  2. Create a basic local network:

   avalanche subnet create MySubnet
   avalanche subnet deploy MySubnet --local
Enter fullscreen mode Exit fullscreen mode
  1. Find the generated files: Look for files in paths like:
   ~/.avalanchego/configs/chains/<subnet-id>/config.json
   ~/.avalanchego/configs/chains/<subnet-id>/genesis.json
Enter fullscreen mode Exit fullscreen mode
  1. Copy these files to your project: Place genesis.json and config.json in your node structure. Now you can customize: NodeIDs, BLS keys, staking params, etc.

2.2. Recommended Folder Structure

Organize your project like this:

/your-avalanche-project/
  node1/
    staking/
    chains/
    plugins/
    db/
  node2/
    staking/
    chains/
    plugins/
    db/
  node3/
    staking/
    chains/
    plugins/
    db/
  subnet/
    genesis.json
  docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

3. Generating Staking Certificates for Each Node

Each Avalanche validator node needs a staking keypair: a private key (staker.key) and a public certificate (staker.crt), in addition to the BLS key. These authenticate and identify the node in the network.

Why is this important?

  • Without these certificates, a node cannot validate or sign blocks.
  • The NodeID of each node is derived from its staking key.
  • The BLS keys (generated at node startup) must match those set in the genesis.json.

How to generate staking certificates?

Use the AvalancheGo binary for each node:

./avalanchego keygen
Enter fullscreen mode Exit fullscreen mode

This will generate:

  • staker.crt (public certificate)
  • staker.key (private key)

Move or copy them to the correct folder for each node:

cp staker.crt ./node1/staking/staker.crt
cp staker.key ./node1/staking/staker.key
Enter fullscreen mode Exit fullscreen mode

Repeat for each node, ensuring each has a unique keypair.

⚠️ Never reuse the same staking certificates for multiple nodes. Each node MUST have its own pair.


(Optional) Generate Custom BLS Keys

If you want even more control (for example, to reproduce specific NodeIDs and BLS keys), you can use avalanche-cli or advanced AvalancheGo commands to generate custom BLS keys and configure them in the genesis.json.


4. Starting Nodes and Obtaining NodeID/BLS

Start each node one at a time (to avoid port or file conflicts). Example for node 1:

docker-compose up avalanche-node-1
Enter fullscreen mode Exit fullscreen mode

In another terminal, get the NodeID and BLS keys:

curl -s --location --request POST 'http://localhost:9650/ext/info' \
  --header 'Content-Type: application/json' \
  --data-raw '{"jsonrpc":"2.0","id":1,"method":"info.getNodeID"}'
Enter fullscreen mode Exit fullscreen mode

Save:

  • nodeID
  • publicKey
  • proofOfPossession

Do the same for all nodes (using the correct ports: 9652, 9654, etc.).

Shut down each node before continuing to the next.


5. Building the genesis.json

In the initialStakers section of your genesis.json, set the unique NodeID and BLS data for each node:

"initialStakers": [
  {
    "nodeID": "NodeID-...",
    "rewardAddress": "X-...",
    "delegationFee": 10000,
    "signer": {
      "publicKey": "0x...",
      "proofOfPossession": "0x..."
    }
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

The rest of the genesis file can include your addresses, funds, and custom parameters.


6. Docker Compose Example

Below is a practical example of a Docker Compose file for running three Avalanche validator nodes in a local network. You can strip out services you don't need, but this includes healthy settings for networking, volumes, and bootstrapping.

version: '3.7'

services:
  avalanche-node-1:
    image: avaplatform/avalanchego:v1.13.3
    container_name: avalanche-node-1
    ports:
      - "9650:9650"
      - "9651:9651"
    volumes:
      - ./node1/staking:/root/.avalanchego/staking
      - ./node1/chains:/root/.avalanchego/chains
      - ./node1/plugins:/root/.avalanchego/plugins
      - ./subnet/genesis.json:/root/.avalanchego/genesis.json
    entrypoint: >
      /bin/sh -c "
        echo '{\"network-id\": 1337, \"api-admin-enabled\": true, \"http-host\": \"0.0.0.0\", \"http-port\": 9650, \"staking-port\": 9651, \"staking-host\": \"0.0.0.0\", \"public-ip\": \"172.20.0.10\", \"network-allow-private-ips\": true, \"log-display-level\": \"info\", \"log-level\": \"info\", \"plugin-dir\": \"/root/.avalanchego/plugins\"}' > /root/.avalanchego/config.json &&
        /avalanchego/build/avalanchego --genesis-file=/root/.avalanchego/genesis.json --config-file=/root/.avalanchego/config.json --staking-tls-cert-file=/root/.avalanchego/staking/staker.crt --staking-tls-key-file=/root/.avalanchego/staking/staker.key --chain-config-dir=/root/.avalanchego/chains --api-metrics-enabled=true --api-health-enabled=true --api-info-enabled=true
      "
    networks:
      subnet-network:
        ipv4_address: 172.20.0.10
    restart: unless-stopped

  avalanche-node-2:
    image: avaplatform/avalanchego:v1.13.3
    container_name: avalanche-node-2
    ports:
      - "9652:9650"
      - "9653:9651"
    volumes:
      - ./node2/staking:/root/.avalanchego/staking
      - ./node2/chains:/root/.avalanchego/chains
      - ./node2/plugins:/root/.avalanchego/plugins
      - ./node2/db:/root/.avalanchego/db
      - ./subnet/genesis.json:/root/.avalanchego/genesis.json
    entrypoint: >
      /bin/sh -c "
        echo '{\"network-id\": 1337, \"api-admin-enabled\": true, \"http-host\": \"0.0.0.0\", \"http-port\": 9650, \"staking-port\": 9651, \"staking-host\": \"0.0.0.0\", \"public-ip\": \"172.20.0.11\", \"network-allow-private-ips\": true, \"log-display-level\": \"info\", \"log-level\": \"info\", \"plugin-dir\": \"/root/.avalanchego/plugins\", \"bootstrap-ips\": \"172.20.0.10:9651\", \"bootstrap-ids\": \"NodeID-5DA3FxiLCbsmzbKXmXC4uvhqLj7tK9afB\"}' > /root/.avalanchego/config.json &&
        /avalanchego/build/avalanchego --genesis-file=/root/.avalanchego/genesis.json --config-file=/root/.avalanchego/config.json --staking-tls-cert-file=/root/.avalanchego/staking/staker.crt --staking-tls-key-file=/root/.avalanchego/staking/staker.key --chain-config-dir=/root/.avalanchego/chains --api-metrics-enabled=true --api-health-enabled=true --api-info-enabled=true --api-admin-enabled=true
      "
    networks:
      subnet-network:
        ipv4_address: 172.20.0.11
    depends_on:
      - avalanche-node-1
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9650/ext/info || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    restart: unless-stopped

  avalanche-node-3:
    image: avaplatform/avalanchego:v1.13.3
    container_name: avalanche-node-3
    ports:
      - "9654:9650"
      - "9655:9651"
    volumes:
      - ./node3/staking:/root/.avalanchego/staking
      - ./node3/chains:/root/.avalanchego/chains
      - ./node3/plugins:/root/.avalanchego/plugins
      - ./node3/db:/root/.avalanchego/db
      - ./subnet/genesis.json:/root/.avalanchego/genesis.json
    entrypoint: >
      /bin/sh -c "
        echo '{\"network-id\": 1337, \"api-admin-enabled\": true, \"http-host\": \"0.0.0.0\", \"http-port\": 9650, \"staking-port\": 9651, \"staking-host\": \"0.0.0.0\", \"public-ip\": \"172.20.0.12\", \"network-allow-private-ips\": true, \"log-display-level\": \"info\", \"log-level\": \"info\", \"plugin-dir\": \"/root/.avalanchego/plugins\", \"bootstrap-ips\": \"172.20.0.10:9651\", \"bootstrap-ids\": \"NodeID-5DA3FxiLCbsmzbKXmXC4uvhqLj7tK9afB\"}' > /root/.avalanchego/config.json &&
        /avalanchego/build/avalanchego --genesis-file=/root/.avalanchego/genesis.json --config-file=/root/.avalanchego/config.json --staking-tls-cert-file=/root/.avalanchego/staking/staker.crt --staking-tls-key-file=/root/.avalanchego/staking/staker.key --chain-config-dir=/root/.avalanchego/chains --api-metrics-enabled=true --api-health-enabled=true --api-info-enabled=true --api-admin-enabled=true
      "
    networks:
      subnet-network:
        ipv4_address: 172.20.0.12
    depends_on:
      - avalanche-node-1
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9650/ext/info || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    restart: unless-stopped

networks:
  subnet-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
          gateway: 172.20.0.1
Enter fullscreen mode Exit fullscreen mode

7. Cleaning and Starting the Network

Delete each node's database to avoid leftover data from previous tests:

rm -rf ./node1/db ./node2/db ./node3/db
Enter fullscreen mode Exit fullscreen mode

Start all nodes in parallel:

docker-compose up -d avalanche-node-1 avalanche-node-2 avalanche-node-3
Enter fullscreen mode Exit fullscreen mode

8. Verifying the Network

Check the health status of each node:

curl -s -X POST --data '{"jsonrpc":"2.0","id":1,"method":"health.health"}' -H 'content-type:application/json' http://localhost:9650/ext/health
Enter fullscreen mode Exit fullscreen mode

All should respond with "healthy": true and see each other in the network.


9. Advantages of Running Your Own Validator Nodes

  • Independence: No need for public nodes or AvalancheGo infrastructure to create subnets.
  • Faster experimentation: Test consensus changes, new VMs, upgrades, stress tests, forks, etc. with no limits or fees.
  • Full control: Decide when to update, how to monitor, and access complete logs and metrics.
  • Privacy: Your tests and subnets can be 100% private and isolated.
  • Deep learning: Understand Avalanche and subnets from the inside out.
  • Production readiness: Practice upgrades, failures, recovery, and monitoring in a safe environment.

10. Notes & Tips

  • If you see NodeID or BLS errors in the logs, check that each node's signer in the genesis.json matches its real BLS key.
  • The genesis.json file must be identical on all nodes.
  • Always delete the node databases (db/) after changing the genesis.
  • Use absolute paths and make sure Docker can access the files from your OS.

Questions or comments? Drop them below or reach out!

Avalanche #Blockchain #Subnets #Validators #Infrastructure #Docker #Web3 #Development

Top comments (0)