Blockchain

Building a Private Blockchain with Hyperledger Besu & Docker: Complete QBFT Network Setup Guide

Azim Memon
Azim Memon
26 min read
BlockchainHyperledger BesuDockerQBFTSmart Contracts
Building a Private Blockchain with Hyperledger Besu & Docker: Complete QBFT Network Setup Guide

Introduction

Imagine spinning up an entire private blockchain network with four nodes, complete consensus, and production-ready configuration—all with a some Docker command. That's the power of containerizing Hyperledger Besu with Docker.

In this guide, you’ll build a fully functional private blockchain network using Docker and Hyperledger Besu. We’ll configure QBFT for robust Byzantine fault tolerance, use Docker to streamline deployment, and integrate tools like Hardhat, web3.js, and Remix for smart contract development. By the end, you’ll have a scalable, containerized blockchain ready to run anywhere — from local environments to production-grade Kubernetes clusters.

What you'll learn:

  • Setting up Hyperledger Besu nodes using Docker and Docker Compose
  • Automatically generating QBFT network configuration with Besu's built-in tools
  • Managing multi-node networks with docker-compose orchestration
  • Understanding Besu's default ports and RPC configurations
  • Deploying and interacting with Solidity smart contracts using local Hardhat
  • Alternative deployment methods using web3.js and Remix

The Architecture: Understanding the Containerized System

Before diving into implementation, let's visualize the Docker-based architecture:

[diagram]
┌───────────────────────────────────────────────────────────┐
│               Docker Network (besu-network)               │
│               Bridge Network: 172.25.0.0/16               │
│                                                           │
│    ┌─────────────┐  ┌──────────────┐  ┌──────────────┐    │
│    │    Node-1   │  │    Node-2    │  │    Node-3    │    │
│    │ (Validator) │  │  (Validator) │  │  (Validator) │    │
│    │  HTTP:8545  │  │   HTTP:8546  │  │   HTTP:8547  │    │
│    └──────┬──────┘  └───────┬──────┘  └────────┬─────┘    │
│           │                 │                  │          │
│           └─────────────────┼──────────────────┘          │
│                             │                             │
│                     ┌───────▼────────┐                    │
│                     │     Node-4     │                    │
│                     │   (RPC Node)   │                    │
│                     │    HTTP:8548   │                    │
│                     └────────────────┘                    │
│                             │                             │
└─────────────────────────────┼─────────────────────────────┘
                              │
                     ┌────────▼────────┐
                     │  Local Hardhat  │
                     │  (Dev Machine)  │
                     │   Port: 8548    │
                     └─────────────────┘

Key Components:

  • Besu Validator Nodes (1-3): Byzantine fault-tolerant consensus participants that propose and finalize blocks
  • Besu RPC Node (4): Dedicated endpoint for smart contract interactions (non-validator)
  • Docker Network: Isolated virtual network for secure inter-node communication
  • Volume Mounts: Persistent storage for blockchain data and configurations
  • Local Hardhat: Development environment running on your machine for smart contract deployment

Part 1: Project Structure and Network Generation

Step 1: Create Project Directory Structure

Create the complete directory structure for your containerized blockchain:

Bash
# Create project root
mkdir besu-docker-network
cd besu-docker-network

# Create necessary directories
mkdir -p config
mkdir -p nodes
mkdir -p smart-contracts
mkdir -p scripts

# Your structure should look like:
# besu-docker-network/
# ├── docker-compose.yml
# ├── .env
# ├── config/
# │   └── qbftConfigFile.json
# ├── nodes/
# │   └── networkFiles/  (will be generated)
# ├── smart-contracts/
# │   ├── hardhat.config.js
# │   ├── package.json
# │   └── contracts/
# └── scripts/
#     └── generate-network.sh

Step 2: Pull Besu Docker Image

First, pull the official Hyperledger Besu image:

Bash
# Pull the latest Besu image
docker pull hyperledger/besu:latest

# Verify the image
docker images | grep besu

# Expected output:
# hyperledger/besu   latest   abc123def456   2 days ago   850MB

Step 3: Create QBFT Network Configuration File

Create config/qbftConfigFile.json to define your network topology:

Json
{
  "genesis": {
    "config": {
      "chainId": 1337,
      "berlinBlock": 0,
      "londonBlock": 0,
      "qbft": {
        "epochlength": 30000,
        "blockperiodseconds": 2,
        "requesttimeoutseconds": 4
      }
    },
    "nonce": "0x0",
    "timestamp": "0x58ee40ba",
    "gasLimit": "0x1C9C380",
    "difficulty": "0x1",
    "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
    "coinbase": "0x0000000000000000000000000000000000000000",
    "alloc": {
      "fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
        "balance": "0x200000000000000000000000000000000000000000000000000000000000000"
      }
    }
  },
  "blockchain": {
    "nodes": {
      "generate": true,
      "count": 4
    }
  }
}

Configuration Parameters Explained:

  • chainId: Unique identifier for your private network (1337 is commonly used for local dev)
  • blockperiodseconds: Time between blocks (2 seconds = high throughput)
  • requesttimeoutseconds: Timeout for consensus messages
  • epochlength: Blocks before validator set can be modified
  • gasLimit: Maximum gas per block (0x1C9C380 = 30,000,000)
  • count: Number of nodes to generate (4 nodes = 3 validators + 1 RPC node)
  • alloc: Pre-funded accounts for testing (90M ETH in this example)

You can replace the sample address inside the alloc section with your own wallet’s public address to pre-fund your account when the network is initialized.

Step 4: Generate Network Configuration Automatically

💡 Quick Start Tip: Steps 4 and 5 explain the network generation process in detail so you understand what's happening under the hood. If you prefer to skip the technical explanation and jump straight to automation, go directly to Step 6 where we've created a convenient script that handles everything automatically.

Now comes the magic! Use Besu's built-in network generator to create all node keys, addresses, and genesis file automatically:

Bash
# Generate complete network configuration
docker run --rm \
  -v $(pwd)/config:/config \
  -v $(pwd)/nodes:/nodes \
  hyperledger/besu:latest \
  operator generate-blockchain-config \
  --config-file=/config/qbftConfigFile.json \
  --to=/nodes/networkFiles \
  --private-key-file-name=key

What just happened?

This single command:

  1. Generated 4 node private keys and public keys
  2. Extracted Ethereum addresses for each node
  3. Created a complete genesis.json with proper extraData (containing validator addresses)

Verify the generated files:

Bash
# Check the generated structure
tree nodes/networkFiles

# Expected output:
# nodes/networkFiles
# ├── genesis.json
# └── keys/
#     ├── 0x1234...5678
#     │   ├── key
#     │   └── key.pub
#     ├── 0x2345...6789
#     │   ├── key
#     │   └── key.pub
#     ├── 0x3456...7890
#     │   ├── key
#     │   └── key.pub
#     └── 0x4567...8901
#         ├── key
#         └── key.pub
# 6 directories, 9 files

View generated genesis file:

Bash
# Pretty print the genesis file
cat nodes/networkFiles/genesis.json | jq '.'

# Check validator addresses in extraData
cat nodes/networkFiles/genesis.json | jq -r '.extraData'

Pro Tip: The generate-blockchain-config command is a game-changer! It eliminates manual key generation, address extraction, and genesis file configuration—reducing setup time from hours to seconds.

Step 5: Organize Node Directories

Now that we've generated the network configuration, we need to organize the keys into individual node directories. Note: This step is also automated in Step 6's script, but understanding the manual process helps you troubleshoot if needed.

Create individual data directories for each node:

Bash
# Create data directories for all nodes
for i in {1..4}; do
  mkdir -p nodes/node-$i/data
done

# Copy node keys to respective directories
# Get list of generated addresses
ADDRESSES=($(ls nodes/networkFiles/keys/))

# Copy keys to node directories
for i in {1..4}; do
  NODE_INDEX=$((i))
  NODE_ADDRESS=${ADDRESSES[$NODE_INDEX]}

  echo "Setting up Node-$i with address: $NODE_ADDRESS"

  # Copy keys
  cp nodes/networkFiles/keys/$NODE_ADDRESS/key nodes/node-$i/data/
  cp nodes/networkFiles/keys/$NODE_ADDRESS/key.pub nodes/node-$i/data/

  # Save address for reference
  echo $NODE_ADDRESS > nodes/node-$i/data/address
done

echo "Node directories configured successfully!"

Security best practices:

  • The nodes directory contains sensitive security data — including private keys, network configuration files, and other confidential node information.
  • Do not expose, commit, or share any files from this directory publicly (for example, on GitHub or cloud storage).

Step 6: Automated Network Generation Script (Recommended)

If you followed Steps 4 and 5 manually, great—you now understand the process! For everyone else (and for future regenerations), use this automated script that combines both steps into a single command.

This script handles:

  • Cleaning previous network files
  • Generating network configuration with Besu
  • Creating node directories
  • Distributing keys to each node
  • Securing private key permissions

Create scripts/generate-network.sh:

Bash
#!/bin/bash

echo "🔧 Generating Besu QBFT Network Configuration..."
echo "================================================"

# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

# Check if config file exists
if [ ! -f "config/qbftConfigFile.json" ]; then
    echo -e "${RED}❌ Error: config/qbftConfigFile.json not found!${NC}"
    exit 1
fi

# Clean previous network files
echo -e "${YELLOW}🧹 Cleaning previous network files...${NC}"
rm -rf nodes/networkFiles
rm -rf nodes/node-*/data

# Generate network configuration
echo -e "${YELLOW}⚙️  Generating network configuration...${NC}"
docker run --rm \
  -v $(pwd)/config:/config \
  -v $(pwd)/nodes:/nodes \
  hyperledger/besu:latest \
  operator generate-blockchain-config \
  --config-file=/config/qbftConfigFile.json \
  --to=/nodes/networkFiles \
  --private-key-file-name=key

# Check if generation was successful
if [ ! -f "nodes/networkFiles/genesis.json" ]; then
    echo -e "${RED}❌ Network generation failed!${NC}"
    exit 1
fi

echo -e "${GREEN}✅ Network configuration generated successfully!${NC}"

# Create node data directories
echo -e "${YELLOW}📁 Creating node directories...${NC}"
for i in {1..4}; do
  mkdir -p nodes/node-$i/data
done

# Copy keys to node directories
echo -e "${YELLOW}🔑 Distributing node keys...${NC}"
ADDRESSES=($(ls nodes/networkFiles/keys/))

for i in {1..4}; do
  NODE_INDEX=$((i-1))
  NODE_ADDRESS=${ADDRESSES[$NODE_INDEX]}

  echo -e "  ${GREEN}✓${NC} Node-$i: $NODE_ADDRESS"

  cp nodes/networkFiles/keys/$NODE_ADDRESS/key nodes/node-$i/data/
  cp nodes/networkFiles/keys/$NODE_ADDRESS/key.pub nodes/node-$i/data/
  echo $NODE_ADDRESS > nodes/node-$i/data/address
done

# Secure key files
echo -e "${YELLOW}🔒 Securing private keys...${NC}"
chmod 600 nodes/node-*/data/key
chmod 700 nodes/node-*/data

echo ""
echo -e "${GREEN}✨ Network setup complete!${NC}"
echo ""
echo "📋 Generated Files:"
echo "  - Genesis file: nodes/networkFiles/genesis.json"
echo "  - Node keys: nodes/node-{1..4}/data/key"
echo ""
echo "Next steps:"
echo "  Review the genesis file: cat nodes/networkFiles/genesis.json | jq"
echo ""

Make it executable and run:

Bash
chmod +x scripts/generate-network.sh
./scripts/generate-network.sh

Expected output:

Plaintext
🔧 Generating Besu QBFT Network Configuration...
================================================
🧹 Cleaning previous network files...
⚙️ Generating network configuration...
   Network configuration generated successfully!
📁 Creating node directories...
🔑 Distributing node keys...
  ✓ Node-1: 0x324582da6b84869df933e8b32f1b0d29250fce3b
  ✓ Node-2: 0x407ad940ce5abee3eb1dc85e4184b8cd2b4b16b3
  ✓ Node-3: 0x8d993e4e1ffcf2321a57e87f4133a9bb55766916
  ✓ Node-4: 0xa3ff928703b195257c2ccb844df6723754a63f8c
🔒 Securing private keys...

✨ Network setup complete!

📋 Generated Files:
  - Genesis file: nodes/networkFiles/genesis.json
  - Node keys: nodes/node-{1..4}/data/key

Next steps:
  Review the genesis file: cat nodes/networkFiles/genesis.json | jq

Part 2: Docker Compose Configuration

Step 1: Create Environment Configuration

Create .env file for easy configuration management:

Bash
# Network Configuration
NETWORK_ID=1337

# Besu Version
BESU_IMAGE=hyperledger/besu:latest

# Gas Configuration
MIN_GAS_PRICE=0

Step 2: Create Docker Compose Configuration

Create docker-compose.yml with optimized QBFT configuration:

Yaml
services:
  node-1:
    image: ${BESU_IMAGE}
    container_name: besu-node-1
    hostname: node-1
    networks:
      besu-network:
        ipv4_address: 172.25.0.2
    ports:
      - '8545:8545' # HTTP JSON-RPC
      - '30303:30303' # P2P
    volumes:
      - ./nodes:/nodes
    command:
      # Data and Network
      - --data-path=/nodes/node-1/data
      - --genesis-file=/nodes/networkFiles/genesis.json
      - --network-id=${NETWORK_ID}

      # HTTP JSON-RPC
      - --rpc-http-enabled
      - --rpc-http-host=0.0.0.0
      - --rpc-http-port=8545
      - --rpc-http-api=ETH,NET,QBFT,WEB3,ADMIN,DEBUG,TXPOOL
      - --rpc-http-max-active-connections=80
      - --rpc-http-cors-origins=*
      - --host-allowlist=*

      # P2P Networking
      - --p2p-enabled=true
      - --p2p-host=0.0.0.0
      - --p2p-port=30303

      # WS-RPC
      - --rpc-ws-enabled=true
      - --rpc-ws-host=0.0.0.0
      - --rpc-ws-port=8546
      - --rpc-ws-api=ETH,NET,QBFT,WEB3,ADMIN,DEBUG,TRACE,TXPOOL
      - --rpc-ws-max-active-connections=80
      - --data-storage-format=BONSAI
      - --bootnodes=enode://$(cat /nodes/node-1/data/key.pub | sed 's/^0x//')@172.25.0.2:30303,enode://$(cat /nodes/node-2/data/key.pub | sed 's/^0x//')@172.25.0.3:30303,enode://$(cat /nodes/node-3/data/key.pub | sed 's/^0x//')@172.25.0.4:30303,enode://$(cat /nodes/node-4/data/key.pub | sed 's/^0x//')@172.25.0.5:30303
      - --min-gas-price=${MIN_GAS_PRICE}
    restart: unless-stopped

  node-2:
    image: ${BESU_IMAGE}
    container_name: besu-node-2
    hostname: node-2
    networks:
      besu-network:
        ipv4_address: 172.25.0.3
    ports:
      - '8546:8545' # HTTP JSON-RPC
      - '30304:30303' # P2P
    volumes:
      - ./nodes:/nodes
    depends_on:
      - node-1
    command:
      # Data and Network
      - --data-path=/nodes/node-2/data
      - --genesis-file=/nodes/networkFiles/genesis.json
      - --network-id=${NETWORK_ID}

      # HTTP JSON-RPC
      - --rpc-http-enabled
      - --rpc-http-host=0.0.0.0
      - --rpc-http-port=8545
      - --rpc-http-api=ETH,NET,QBFT,WEB3,ADMIN
      - --rpc-http-cors-origins=*
      - --rpc-http-max-active-connections=80
      - --host-allowlist=*

      # P2P Networking
      - --p2p-enabled=true
      - --p2p-host=0.0.0.0
      - --p2p-port=30303

      # WS-RPC
      - --rpc-ws-enabled=true
      - --rpc-ws-host=0.0.0.0
      - --rpc-ws-port=8546
      - --rpc-ws-api=ETH,NET,QBFT,WEB3,ADMIN,DEBUG,TRACE,TXPOOL
      - --rpc-ws-max-active-connections=80
      - --data-storage-format=BONSAI
      - --bootnodes=enode://$(cat /nodes/node-1/data/key.pub | sed 's/^0x//')@172.25.0.2:30303,enode://$(cat /nodes/node-2/data/key.pub | sed 's/^0x//')@172.25.0.3:30303,enode://$(cat /nodes/node-3/data/key.pub | sed 's/^0x//')@172.25.0.4:30303,enode://$(cat /nodes/node-4/data/key.pub | sed 's/^0x//')@172.25.0.5:30303
      - --min-gas-price=${MIN_GAS_PRICE}
    restart: unless-stopped

  node-3:
    image: ${BESU_IMAGE}
    container_name: besu-node-3
    hostname: node-3
    networks:
      besu-network:
        ipv4_address: 172.25.0.4
    ports:
      - '8547:8545' # HTTP JSON-RPC
      - '30305:30303' # P2P
    volumes:
      - ./nodes:/nodes
    depends_on:
      - node-1
    command:
      # Data and Network
      - --data-path=/nodes/node-3/data
      - --genesis-file=/nodes/networkFiles/genesis.json
      - --network-id=${NETWORK_ID}

      # HTTP JSON-RPC
      - --rpc-http-enabled
      - --rpc-http-host=0.0.0.0
      - --rpc-http-port=8545
      - --rpc-http-api=ETH,NET,QBFT,WEB3,ADMIN
      - --rpc-http-max-active-connections=80
      - --rpc-http-cors-origins=*
      - --host-allowlist=*

      # P2P Networking
      - --p2p-enabled=true
      - --p2p-host=0.0.0.0
      - --p2p-port=30303

      # WS-RPC
      - --rpc-ws-enabled=true
      - --rpc-ws-host=0.0.0.0
      - --rpc-ws-port=8546
      - --rpc-ws-api=ETH,NET,QBFT,WEB3,ADMIN,DEBUG,TRACE,TXPOOL
      - --rpc-ws-max-active-connections=80
      - --data-storage-format=BONSAI
      - --bootnodes=enode://$(cat /nodes/node-1/data/key.pub | sed 's/^0x//')@172.25.0.2:30303,enode://$(cat /nodes/node-2/data/key.pub | sed 's/^0x//')@172.25.0.3:30303,enode://$(cat /nodes/node-3/data/key.pub | sed 's/^0x//')@172.25.0.4:30303,enode://$(cat /nodes/node-4/data/key.pub | sed 's/^0x//')@172.25.0.5:30303
      - --min-gas-price=${MIN_GAS_PRICE}
    restart: unless-stopped

  node-4:
    image: ${BESU_IMAGE}
    container_name: besu-node-4
    hostname: node-4
    networks:
      besu-network:
        ipv4_address: 172.25.0.5
    ports:
      - '8548:8545' # HTTP JSON-RPC
      - '30306:30303' # P2P
    volumes:
      - ./nodes:/nodes
    depends_on:
      - node-1
    command:
      # Data and Network
      - --data-path=/nodes/node-4/data
      - --genesis-file=/nodes/networkFiles/genesis.json
      - --network-id=${NETWORK_ID}

      # HTTP JSON-RPC (RPC Node - no ADMIN/QBFT APIs for security)
      - --rpc-http-enabled
      - --rpc-http-host=0.0.0.0
      - --rpc-http-port=8545
      - --rpc-http-api=ETH,NET,WEB3,DEBUG,TRACE,TXPOOL
      - --rpc-http-max-active-connections=80
      - --rpc-http-cors-origins=*
      - --host-allowlist=*

      # P2P Networking
      - --p2p-enabled=true
      - --p2p-host=0.0.0.0
      - --p2p-port=30303
      - --rpc-ws-enabled=true
      - --rpc-ws-host=0.0.0.0
      - --rpc-ws-port=8546
      - --rpc-ws-api=ETH,NET,WEB3,DEBUG,TRACE,TXPOOL
      - --rpc-ws-max-active-connections=80
      - --data-storage-format=BONSAI
      - --bootnodes=enode://$(cat /nodes/node-1/data/key.pub | sed 's/^0x//')@172.25.0.2:30303,enode://$(cat /nodes/node-2/data/key.pub | sed 's/^0x//')@172.25.0.3:30303,enode://$(cat /nodes/node-3/data/key.pub | sed 's/^0x//')@172.25.0.4:30303,enode://$(cat /nodes/node-4/data/key.pub | sed 's/^0x//')@172.25.0.5:30303
      - --min-gas-price=${MIN_GAS_PRICE}
    restart: unless-stopped

networks:
  besu-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.25.0.0/16

Key Docker Compose Features:

  • Official Image: hyperledger/besu:latest
  • Volume Mounts: Each node's data directory is mounted for persistent storage
  • Shared Genesis: All nodes share the same genesis file
  • Network Isolation: All nodes communicate via besu-network bridge

Understanding Port Mappings:

  • Default Besu Ports (inside containers):

    • 8545: HTTP JSON-RPC (standard default)
    • 8546: WebSocket JSON-RPC (standard default)
    • 8551: Engine API (not used in this setup)
    • 30303: P2P networking
  • Host Port Mappings (exposed to your machine):

    • Node-1: localhost:8545 → container HTTP 8545
    • Node-2: localhost:8546 → container HTTP 8545
    • Node-3: localhost:8547 → container HTTP 8545
    • Node-4: localhost:8548 → container HTTP 8545

Why different host ports? Each container uses the standard port 8545 internally, but we map them to different host ports (8545, 8546, 8547, 8548) to avoid conflicts. This allows you to access all nodes simultaneously from your machine.

Important: The $(cat nodes/node-1/data/key.pub | tr -d '\n') command reads the bootnode's public key directly from the mounted volume. This eliminates manual enode URL configuration!

Security Note: The --rpc-http-cors-origins=* and --host-allowlist=* settings are permissive and suitable for local development. For production deployments:

  • Restrict CORS to specific origins
  • Limit host allowlist to known hosts
  • Consider adding authentication
  • Never expose ADMIN APIs publicly

Part 3: Starting and Managing the Blockchain Network

Step 1: Start the Network

Launch all containers:

Bash
# Start all nodes in detached mode
docker compose up -d

# Watch the logs in real-time
docker compose logs -f

Expected startup sequence:

Plaintext
[+] Running 5/5
 ✔ Network besu-docker-network_besu-network      Created          0.1s
 ✔ Container besu-node-1                         Started          2.3s
 ✔ Container besu-node-2                         Started          2.1s
 ✔ Container besu-node-3                         Started          2.2s
 ✔ Container besu-node-4                         Started          2.3s

Success indicators in logs:

Plaintext
besu-node-1  | INFO  | EthProtocolManager | P2PNetwork | enode://abc123...@node-1:30303
besu-node-1  | INFO  | JsonRpcHttpService | HTTP JSON-RPC service started
besu-node-1  | INFO  | QbftBesuControllerBuilder | Starting QBFT BlockchainMiner
besu-node-2  | INFO  | PeerDiscoveryAgent | Discovered 1 peers
besu-node-1  | INFO  | QbftRound | Imported #1 / 0 tx / 0 pending
besu-node-2  | INFO  | QbftRound | Imported #1 / 0 tx / 0 pending
besu-node-3  | INFO  | QbftRound | Imported #1 / 0 tx / 0 pending

Step 2: Verify Network Status

Check that all containers are running:

Bash
# Check container status
docker compose ps

# Expected output showing all containers running
NAME            IMAGE                        STATUS
besu-node-1     hyperledger/besu:latest      Up 2 minutes
besu-node-2     hyperledger/besu:latest      Up 2 minutes
besu-node-3     hyperledger/besu:latest      Up 2 minutes
besu-node-4     hyperledger/besu:latest      Up 2 minutes

Verify peer connections:

Bash
# Check peers on node-1
curl -X POST --data '{
  "jsonrpc":"2.0",
  "method":"admin_peers",
  "params":[],
  "id":1
}' http://localhost:8545 | jq '.result | length'

# Expected: 3 (nodes 2, 3, and 4 connected to node-1)

Check block production:

Bash
# Get current block number
curl -X POST --data '{
  "jsonrpc":"2.0",
  "method":"eth_blockNumber",
  "params":[],
  "id":1
}' http://localhost:8545 | jq -r '.result' | xargs printf "%d\n"

# Wait 10 seconds and check again - should increase by ~5 blocks
sleep 10
curl -X POST --data '{
  "jsonrpc":"2.0",
  "method":"eth_blockNumber",
  "params":[],
  "id":1
}' http://localhost:8545 | jq -r '.result' | xargs printf "%d\n"

Step 3: Create Monitoring Script

Create scripts/network-status.sh for quick health checks:

Bash
#!/bin/bash

# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

echo "🔍 Besu Network Status"
echo "======================"
echo ""

# Function to check node
check_node() {
    local name=$1
    local port=$2

    response=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
        http://localhost:$port 2>/dev/null)

    if [ $? -eq 0 ]; then
        block=$(echo $response | jq -r '.result' | xargs printf "%d" 2>/dev/null)
        echo -e "${GREEN}✓${NC} $name (Port $port) - Block: $block"
    else
        echo -e "${RED}✗${NC} $name (Port $port) - Unreachable"
    fi
}

# Check all nodes
echo "📦 Nodes Status:"
check_node "Node-1 (Validator)" 8545
check_node "Node-2 (Validator)" 8546
check_node "Node-3 (Validator)" 8547
check_node "Node-4 (RPC Node)" 8548

echo ""
echo "🔗 Network Info:"
peers=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"admin_peers","params":[],"id":1}' \
    http://localhost:8545 | jq -r '.result | length')
echo "  Connected Peers: $peers"

# Check if blocks are being produced
block1=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
    http://localhost:8545 | jq -r '.result' | xargs printf "%d")
sleep 5
block2=$(curl -s -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
    http://localhost:8545 | jq -r '.result' | xargs printf "%d")

if [ $block2 -gt $block1 ]; then
    echo -e "  Block Production: ${GREEN}Active${NC} (+$((block2-block1)) blocks in 5s)"
else
    echo -e "  Block Production: ${RED}Stopped${NC}"
fi

echo ""
echo "🐳 Docker Containers:"
docker compose ps --format "table {{.Service}}\t{{.Status}}"

Make it executable and run:

Bash
chmod +x scripts/network-status.sh
./scripts/network-status.sh

Step 4: Essential Docker Management Commands

Bash
# View logs from all containers
docker compose logs -f

# View logs from specific container
docker compose logs -f node-1

# View last 100 lines
docker compose logs --tail=100 node-1

# Stop all containers
docker compose stop

# Start stopped containers
docker compose start

# Restart all containers
docker compose restart

# Stop and remove containers (keeps data)
docker compose down

# Stop, remove containers and volumes (DELETES ALL DATA)
docker compose down -v

# Execute command in running container
docker compose exec node-1 bash

# View resource usage
docker stats

# Check Besu version
docker compose exec node-1 besu --version

Part 4: Smart Contract Development with Local Hardhat

Now that your Besu network is running, you'll set up a local Hardhat environment on your development machine to deploy and interact with smart contracts.

Step 1: Initialize Hardhat Project Locally

Bash
# Navigate to smart-contracts directory
cd smart-contracts

# Initialize Hardhat (use Hardhat v2)
npx hardhat --init

Step

2: Configure Hardhat for Besu Network

Create or Update smart-contracts/hardhat.config.js:

Javascript
require('@nomicfoundation/hardhat-toolbox');

module.exports = {
  solidity: {
    version: '0.8.28', // Use this or your project's Solidity version configured in Hardhat
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    besu: {
      url: 'http://localhost:8548', // Node-4 RPC endpoint
      chainId: 1337,
      accounts: [
        '0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63',
      ],
      gasPrice: 0,
      gas: 10000000,
    },
  },
  paths: {
    sources: './contracts',
    tests: './test',
    cache: './cache',
    artifacts: './artifacts',
  },
};

Network Configuration Explained:

  • url: Points to Node-4 (RPC node) at localhost:8548
  • chainId: Must match the chainId in your genesis file (1337)
  • accounts: Private key for the pre-funded account from genesis
  • gasPrice: Set to 0 for free transactions in private network
  • gas: High gas limit for complex contracts

Step 3: Create Smart Contract

Create smart-contracts/contracts/SimpleStorage.sol:

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract SimpleStorage {
    uint256 private storedData;
    address public owner;

    event DataStored(uint256 indexed newValue, address indexed setter, uint256 timestamp);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    constructor(uint256 initialValue) {
        owner = msg.sender;
        storedData = initialValue;
        emit DataStored(initialValue, msg.sender, block.timestamp);
    }

    function set(uint256 newValue) public {
        storedData = newValue;
        emit DataStored(newValue, msg.sender, block.timestamp);
    }

    function get() public view returns (uint256) {
        return storedData;
    }

    function increment() public {
        storedData += 1;
        emit DataStored(storedData, msg.sender, block.timestamp);
    }

    function decrement() public {
        require(storedData > 0, "Value cannot go below zero");
        storedData -= 1;
        emit DataStored(storedData, msg.sender, block.timestamp);
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "New owner cannot be zero address");
        address previousOwner = owner;
        owner = newOwner;
        emit OwnershipTransferred(previousOwner, newOwner);
    }
}

Step 4: Create Deployment Script

Create smart-contracts/scripts/deploy.js:

Javascript
const hre = require('hardhat');
const fs = require('fs');

async function main() {
  console.log('🚀 Starting deployment to Besu network...\n');

  const initialValue = 42;

  // Get deployer account
  const [deployer] = await hre.ethers.getSigners();
  console.log('Deploying contracts with account:', deployer.address);

  // Check balance
  const balance = await hre.ethers.provider.getBalance(deployer.address);
  console.log('Account balance:', hre.ethers.formatEther(balance), 'ETH\n');

  // Get current block number
  const blockNumber = await hre.ethers.provider.getBlockNumber();
  console.log('Current block number:', blockNumber);

  // Deploy SimpleStorage
  console.log('\n📝 Deploying SimpleStorage contract...');
  const SimpleStorage = await hre.ethers.getContractFactory('SimpleStorage');
  const gasPrice = hre.ethers.parseUnits('1', 'gwei'); // 1 gwei
  const simpleStorage = await SimpleStorage.deploy(initialValue, {
    gasPrice: gasPrice,
  });

  await simpleStorage.waitForDeployment();

  const address = await simpleStorage.getAddress();
  console.log('✅ SimpleStorage deployed to:', address);
  console.log('Initial value:', await simpleStorage.get());

  // Get deployment transaction details
  const deploymentTx = simpleStorage.deploymentTransaction();
  const receipt = await deploymentTx.wait();

  console.log('\n📊 Deployment Transaction:');
  console.log('  Hash:', deploymentTx.hash);
  console.log('  Block:', receipt.blockNumber);
  console.log('  Gas Used:', receipt.gasUsed.toString());
  console.log('  Status:', receipt.status === 1 ? '✅ Success' : '❌ Failed');

  // Save deployment info
  const deployment = {
    network: hre.network.name,
    chainId: (await hre.ethers.provider.getNetwork()).chainId.toString(),
    contractAddress: address,
    deployer: deployer.address,
    initialValue: initialValue,
    blockNumber: receipt.blockNumber,
    transactionHash: deploymentTx.hash,
    gasUsed: receipt.gasUsed.toString(),
    timestamp: new Date().toISOString(),
  };

  console.log('\n📄 Deployment Summary:');
  console.log(JSON.stringify(deployment, null, 2));

  // Save to file
  const deploymentsFile = 'deployments.json';
  let deployments = [];

  if (fs.existsSync(deploymentsFile)) {
    deployments = JSON.parse(fs.readFileSync(deploymentsFile, 'utf8'));
  }

  deployments.push(deployment);
  fs.writeFileSync(deploymentsFile, JSON.stringify(deployments, null, 2));

  console.log(`\n💾 Deployment info saved to ${deploymentsFile}`);
  console.log('\n🎉 Deployment completed successfully!');
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error('❌ Deployment failed:', error);
    process.exit(1);
  });

Step 5: Deploy Contract Using Local Hardhat

Compile and deploy the smart contract:

Bash
# Compile contracts
npx hardhat compile

# Deploy to Besu network
npx hardhat run scripts/deploy.js --network besu

Expected output:

Plaintext
🚀 Starting deployment to Besu network...

Deploying contracts with account: 0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73
Account balance: 904625697166532776746648320380374280103671755200316906558.262375061821325312 ETH

Current block number: 673

📝 Deploying SimpleStorage contract...
✅ SimpleStorage deployed to: 0x42699A7612A82f1d9C36148af9C77354759b210b
Initial value: 42n

📊 Deployment Transaction:
  Hash: 0xe742a1aa6d7f88898b95f993a41eb40b1e13181f5f5a8bec0542a6a0601aed44
  Block: 674
  Gas Used: 303383
  Status: ✅ Success

📄 Deployment Summary:
{
  "network": "besu",
  "chainId": "1337",
  "contractAddress": "0x42699A7612A82f1d9C36148af9C77354759b210b",
  "deployer": "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73",
  "initialValue": 42,
  "blockNumber": 674,
  "transactionHash": "0xe742a1aa6d7f88898b95f993a41eb40b1e13181f5f5a8bec0542a6a0601aed44",
  "gasUsed": "303383",
  "timestamp": "2025-10-25T11:32:49.452Z"
}

💾 Deployment info saved to deployments.json

🎉 Deployment completed successfully!

Part 5: Interacting with Deployed Smart Contracts

Step 1: Create Interaction Script

Create smart-contracts/scripts/interact.js:

Javascript
const hre = require('hardhat');
const fs = require('fs');

async function main() {
  console.log('🔗 Interacting with SimpleStorage Contract\n');

  // Load deployment info
  const deploymentsFile = 'deployments.json';
  if (!fs.existsSync(deploymentsFile)) {
    console.error('❌ No deployments found. Please deploy the contract first.');
    process.exit(1);
  }
  const gasPrice = hre.ethers.parseUnits('1', 'gwei');

  const deployments = JSON.parse(fs.readFileSync(deploymentsFile, 'utf8'));
  const latestDeployment = deployments[deployments.length - 1];
  const contractAddress = latestDeployment.contractAddress;

  console.log('Contract Address:', contractAddress);
  console.log('Network:', hre.network.name);
  console.log('');

  // Get contract instance
  const SimpleStorage = await hre.ethers.getContractFactory('SimpleStorage');
  const simpleStorage = SimpleStorage.attach(contractAddress);

  // Get signer
  const [signer] = await hre.ethers.getSigners();
  console.log('Interacting as:', signer.address);
  console.log('');

  // Read current value
  console.log('📖 Reading current value...');
  const currentValue = await simpleStorage.get();
  console.log('Current stored value:', currentValue.toString());
  console.log('');
  const txOptions = { gasPrice };
  // Update value
  console.log('✏️  Setting new value to 123...');
  const setTx = await simpleStorage.set(123, txOptions);
  console.log('Transaction sent:', setTx.hash);
  const setReceipt = await setTx.wait();
  console.log('Transaction confirmed in block:', setReceipt.blockNumber);
  console.log('Gas used:', setReceipt.gasUsed.toString());
  console.log('Updated value:', (await simpleStorage.get()).toString());
  console.log('');

  // Increment value
  console.log('➕ Incrementing value...');
  const incTx = await simpleStorage.increment(txOptions);
  console.log('Transaction sent:', incTx.hash);
  await incTx.wait();
  console.log(
    'New value after increment:',
    (await simpleStorage.get()).toString()
  );
  console.log('');

  // Decrement value
  console.log('➖ Decrementing value...');
  const decTx = await simpleStorage.decrement(txOptions);
  await decTx.wait();
  console.log(
    'New value after decrement:',
    (await simpleStorage.get()).toString()
  );
  console.log('');

  // Query events
  console.log('📜 Recent DataStored events:');
  const filter = simpleStorage.filters.DataStored();
  const events = await simpleStorage.queryFilter(filter, -20);

  if (events.length === 0) {
    console.log('  No events found');
  } else {
    events.forEach((event, index) => {
      console.log(`  Event ${index + 1}:`);
      console.log(`    Value: ${event.args.newValue}`);
      console.log(`    Setter: ${event.args.setter}`);
      console.log(
        `    Timestamp: ${new Date(
          Number(event.args.timestamp) * 1000
        ).toISOString()}`
      );
      console.log(`    Block: ${event.blockNumber}`);
      console.log(`    Tx Hash: ${event.transactionHash}`);
      console.log('');
    });
  }

  // Get contract owner
  console.log('👤 Contract Information:');
  console.log('  Owner:', await simpleStorage.owner(txOptions));
  console.log('  Current Value:', (await simpleStorage.get()).toString());

  console.log('\n✅ Interaction completed successfully!');
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error('❌ Interaction failed:', error);
    process.exit(1);
  });

Step 2: Execute Interaction Script

Run the interaction script:

Bash
# Interact with deployed contract
npx hardhat run scripts/interact.js --network besu

Expected output:

Plaintext
🔗 Interacting with SimpleStorage Contract

Contract Address: 0x42699A7612A82f1d9C36148af9C77354759b210b
Network: besu

Interacting as: 0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73

📖 Reading current value...
Current stored value: 42

✏️  Setting new value to 123...
Transaction sent: 0x1f06e5d8b85336ff359d9558f39388c009a46e722a7558a89548e9ddba456a1c
Transaction confirmed in block: 746
Gas used: 28253
Updated value: 123

➕ Incrementing value...
Transaction sent: 0xde608c22f6da0b7a5dc57c154958ed2ef2cd95bf6c475ef7a8a41db2311ce4c8
New value after increment: 124

➖ Decrementing value...
New value after decrement: 123

📜 Recent DataStored events:
  Event 1:
    Value: 123
    Setter: 0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73
    Timestamp: 2025-10-25T11:35:13.000Z
    Block: 746
    Tx Hash: 0x1f06e5d8b85336ff359d9558f39388c009a46e722a7558a89548e9ddba456a1c

  Event 2:
    Value: 124
    Setter: 0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73
    Timestamp: 2025-10-25T11:35:15.000Z
    Block: 747
    Tx Hash: 0xde608c22f6da0b7a5dc57c154958ed2ef2cd95bf6c475ef7a8a41db2311ce4c8

  Event 3:
    Value: 123
    Setter: 0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73
    Timestamp: 2025-10-25T11:35:17.000Z
    Block: 748
    Tx Hash: 0x30f9bd4b0d8094e500f2626ee39618519339ac3511fedbeef24261affce62d69

👤 Contract Information:
  Owner: 0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73
  Current Value: 123

✅ Interaction completed successfully!

Part 6: Alternative Deployment Methods

Option 1: Using Web3.js

If you prefer using web3.js instead of Hardhat, here's how to deploy contracts:

Install web3.js:

Bash
npm install web3

Create deployment script smart-contracts/scripts/deploy-web3.js:

Javascript
const { Web3 } = require('web3');
const fs = require('fs');
const path = require('path');

// Connect to Besu node
const web3 = new Web3('http://localhost:8548');

// Account configuration
const privateKey =
  '0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63';
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(account);

async function deploy() {
  console.log('🚀 Deploying with Web3.js...\n');
  console.log('Using account:', account.address);

  // Load compiled contract
  const contractPath = path.join(
    __dirname,
    '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json'
  );
  const contractJson = JSON.parse(fs.readFileSync(contractPath, 'utf8'));

  const SimpleStorage = new web3.eth.Contract(contractJson.abi);

  // Check balance
  const balance = await web3.eth.getBalance(account.address);
  console.log('Balance:', web3.utils.fromWei(balance, 'ether'), 'ETH\n');

  // Deploy contract
  console.log('📝 Deploying SimpleStorage...');

  const deployTx = SimpleStorage.deploy({
    data: contractJson.bytecode,
    arguments: [42], // Initial value
  });

  const gas = await deployTx.estimateGas({ from: account.address });
  console.log('Estimated gas:', gas);

  const contract = await deployTx.send({
    from: account.address,
    gas: Number(gas) + 100000,
    gasPrice: web3.utils.toWei('1', 'gwei'),
  });

  console.log('\n✅ Contract deployed at:', contract.options.address);

  // Test contract
  const value = await contract.methods.get().call();
  console.log('Initial value:', value);

  // Save deployment info
  const deployment = {
    address: contract.options.address,
    deployer: account.address,
    timestamp: new Date().toISOString(),
  };

  fs.writeFileSync('deployment-web3.json', JSON.stringify(deployment, null, 2));

  console.log('\n💾 Deployment info saved to deployment-web3.json');
}

deploy()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error('❌ Deployment failed:', error);
    process.exit(1);
  });

Run the deployment:

Bash
# First compile with Hardhat
npx hardhat compile

# Deploy using web3.js
node scripts/deploy-web3.js

Option 2: Using Remix IDE

Remix provides a user-friendly interface for contract deployment:

Step 1: Access Remix

Open Remix IDE in your browser.

Step 2: Create Contract

  1. Create a new file: SimpleStorage.sol
  2. Paste your Solidity code
  3. Compile the contract (Compiler tab)

Step 3: Connect to Besu Network

  1. Go to the "Deploy & Run Transactions" tab
  2. Select "Injected Provider - MetaMask" or "External Http Provider"
  3. If using External Http Provider, enter: http://localhost:8548

Step 4: Configure MetaMask (Optional)

If using MetaMask:

  1. Open MetaMask
  2. Add Custom Network:
    • Network Name: Besu Local
    • RPC URL: http://localhost:8548
    • Chain ID: 1337
    • Currency Symbol: ETH
  3. Import account using the private key from your genesis file

Step 5: Deploy Contract

  1. Set constructor parameter (e.g., 42)
  2. Click "Deploy"
  3. Confirm the transaction
  4. Contract address will appear in the "Deployed Contracts" section

Step 6: Interact with Contract

Use Remix's interface to:

  • Call get() to read the value
  • Call set(uint256) to update the value
  • Call increment() and decrement()
  • View transaction logs and events

Part 7: Adding BlockScout Explorer for Network Visualization

After all those command-line interactions, things might start feeling a bit… exhausting, right? No visuals — just endless logs, hashes, and terminal output.

Let’s fix that. 👀

It’s time to bring your private network to life with BlockScout — a powerful, open-source blockchain explorer that provides a clean, interactive dashboard to visualize blocks, transactions, and smart contracts in real time.

With BlockScout, you can finally see your Besu network in action — making debugging, monitoring, and development not just easier, but also way more fun. 🚀

What is BlockScout?

BlockScout is a full-featured blockchain explorer that allows you to:

  • View real-time block production and transactions
  • Explore deployed smart contracts and their source code
  • Monitor network statistics and validator performance
  • Verify and interact with smart contracts directly from the UI
  • Track token transfers and wallet balances

Step 1: Clone BlockScout Repository

First, clone the official BlockScout Docker setup:

Bash
# Navigate to your project root
cd besu-docker-network

# Clone BlockScout
git clone https://github.com/blockscout/blockscout.git blockscout-explorer

# Navigate to docker-compose directory
cd blockscout-explorer/docker-compose

Step 2: Configure BlockScout for Besu

Locate envs/common-blockscout.env and update or append the following variables at the end of the file. Existing values will be overwritten:

Bash
# Network Configuration
ETHEREUM_JSONRPC_VARIANT=besu
ETHEREUM_JSONRPC_HTTP_URL=http://host.docker.internal:8548
ETHEREUM_JSONRPC_TRACE_URL=http://host.docker.internal:8548
ETHEREUM_JSONRPC_WS_URL=ws://host.docker.internal:8548

# Chain Configuration
CHAIN_ID=1337
COIN=ETH
NETWORK=Besu Private Network

Important Configuration Note:

  • host.docker.internal: This special DNS name allows BlockScout container to access your host machine's ports (where Besu is running)

Step 3: Start BlockScout

Make sure your Besu network is running, then start BlockScout:

Bash
# Ensure you're in blockscout-explorer/docker-compose directory
cd blockscout-explorer/docker-compose

# Start BlockScout services
docker compose up -d

# Monitor startup logs
docker compose logs -f backend

Wait for the initialization - BlockScout needs to:

  1. Create and migrate the database (1-2 minutes)
  2. Start indexing blocks from your Besu network
  3. Boot up the frontend interface

Step 4: Access BlockScout Dashboard

Open your browser and navigate to:

  • Frontend UI: http://localhost:80
  • API Backend: http://localhost:80/api

First-time setup may take 2-3 minutes as BlockScout indexes your existing blocks.

Step 5: Explore Your Blockchain Visually

Once BlockScout is running, you can:

1. Monitor Real-Time Blocks

  • View blocks being produced every 2 seconds
  • See validator addresses confirming transactions
  • Track gas usage and block rewards

2. Search and Track Transactions

Bash
# Deploy a contract to see it in BlockScout
cd ../../smart-contracts
npx hardhat run scripts/deploy.js --network besu

# Copy the contract address and search for it in BlockScout

Step 6: Managing BlockScout

Bash
# Stop BlockScout
docker compose down

# Stop and remove all data (fresh start)
docker compose down -v

# View logs
docker compose logs -f backend
docker compose logs -f frontend

# Restart services
docker compose restart

Now you have a complete development environment: a private Besu blockchain with full visual monitoring through BlockScout! 🎉

Conclusion

Congratulations! You've built a complete enterprise-grade blockchain development environment:

  • Private Besu Network with QBFT consensus and Byzantine fault tolerance
  • Docker Containerization for easy deployment and scaling
  • Smart Contract Development with Hardhat and multiple deployment options
  • Visual Block Explorer with BlockScout for intuitive network monitoring

This setup provides everything you need for:

  • Enterprise blockchain development with production-ready infrastructure
  • Private DApp testing in an isolated, controlled environment
  • Smart contract experimentation with real-time visual feedback
  • Team collaboration with containerized, reproducible environments

Next Steps

Ready to Scale?

  • Add more validator nodes for increased decentralization
  • Set up load balancing for high-throughput applications
  • Deploy to cloud infrastructure (AWS, Azure, GCP) or Kubernetes clusters

Want to Build Custom Blockchain Solutions?

  • Integrate advanced features like private transactions and on-chain governance
  • Develop custom consensus mechanisms for your business needs
  • Build production-ready DApps with monitoring and analytics
  • Implement cross-chain bridges and interoperability solutions

Need Help with Your Blockchain Project? 🚀

I specialize in building and scaling blockchain systems, including:

  • Private Blockchain Networks — Besu, Quorum, and Hyperledger Fabric setups
  • Smart Contracts — Solidity development, auditing, and optimization
  • Enterprise DApps — End-to-end decentralized application development

📩 Let’s collaborate on your next blockchain project. Contact me here


Useful Resources


"From zero to production-ready blockchain in under an hour. This is the power of modern blockchain infrastructure."

Found this guide helpful? Share it with your team, and let me know what you build! Happy blockchain development! 🎉