How to build an Ethereum private blockchain network using geth and Docker

Hemant Gupta
5 min readMay 1, 2023

--

Introduction

In the previous post, we discussed building the Ethereum private network using geth commands. It is suitable for learning fundamentals, but it is not scalable and cannot be used for production-grade work.

This post will build the private network using the official docker image.

You can check the private-blockchain-etherum repo for the source code.

Setup

Step 1: Create a genesis.json file

Every blockchain starts with a genesis (first) block. The genesis block is configured using a genesis.json file for a private network. Inside a new folder/directory, create a genesis.json file with the below content.

Step 2: Create an environment file

We need to configure the network id and account passwords. Let’s add both configurations in a .env file.

NETWORK_ID=3456
ACCOUNT_PASSWORD=<account-password>

Step 3: Create a Dockerfile

We first need the base image of the official ethereum/client-go. Create a Dockerfile in the current directory( along with genesis.json and .env files) and write the first line using FROM command as shown in below code snippet.

Next, we will use the ACCOUNT_PASSWORD argument so declare it. We need to use the genesis.json file, so add a COPY command.

In the RUN command, we will do two important things. We will initialize the blockchain using the genesis.json file and create a new account using the password from the argument. I deleted the nodekey created during initialization and the password file after usage.

Step 4: Create nodekey and enode for bootnode

In the previous post, we created nodes by the geth command and then added peers manually using admin.addPeer(“<enode string>”).

In the dockerized environment, it will not be possible to add enode manually as they will be generated later. Ethereum provides bootnode library/tool for that.

We can also use this discovery method when running nodes on different machines.

Let’s create the nodekey for the bootnode and the enode value of the bootnode.

Run the below command to create the nodekey.

bootnode -genkey bootnode.key

This will create a bootnode.key file in the current directory. The file will have nodekeyhex saved in it.

Now run the below command to generate enode string

bootnode -nodekeyhex <nodekeyhex-from-file> -writeaddress

The output of this command will give enode value. See the example below:

bootnode -nodekeyhex 37d571faed3af03f8302e1ddb940ca2f13010a1acf9443ba1507b93e5b36fcbd -writeaddress
749bac3705e7d0c92c27ace288d911cf2798538edcb46a3f9d058093c80c3533be6668df41ecb1174dd9cf22c39b6c5ed7e0f1aa53ef7928679b506f80b0268e

The enode URL can be constructed from the value using the pattern below.

enode://ENODE-value@IP:PORT

Step 5: Create the docker-compose file

It is the most important step, but we have done the required preparation, so creating a docker-compose.yml file in the current directory will be easy.

We will create one bootnode and 5 minor nodes in the file. Let’s start with bootnode.

mybootnode:
hostname: mybootnode
env_file:
- .env
build:
context: .
args:
- ACCOUNT_PASSWORD=${ACCOUNT_PASSWORD}
command: --nodekeyhex="37d571faed3af03f8302e1ddb940ca2f13010a1acf9443ba1507b93e5b36fcbd" --nodiscover --ipcdisable --networkid=${NETWORK_ID} --netrestrict="172.16.254.0/28"
networks:
priv-eth-net:

The name of the service is mybootnode and I have also given the same name to the hostname.

We are not going to use any docker image from outside but will use the custom docker image created from our Dockerfile. To do this, I have added the build and provided the context as the current directory.

In the command, we are passing the required flags for geth.

One last point, all our nodes need to be connected, so priv-eth-net bridge is mentioned in the network section.

miner-1:
hostname: miner-1
env_file:
- .env
build:
context: .
args:
- ACCOUNT_PASSWORD=${ACCOUNT_PASSWORD}
command: --bootnodes="enode://749bac3705e7d0c92c27ace288d911cf2798538edcb46a3f9d058093c80c3533be6668df41ecb1174dd9cf22c39b6c5ed7e0f1aa53ef7928679b506f80b0268e@mybootnode:30303" --mine --miner.threads=1 --networkid=${NETWORK_ID} --netrestrict="172.16.254.0/28"
networks:
priv-eth-net:

Creating a miner node is simple. The only important point is to provide bootnodes flag in the geth command. This flag takes a list. We have only one bootnode, so we are adding that only. Take note here, we have used @mybootnode as the IP address/hostname in the enode URL.

In the file, I have created five minor nodes. You can do as many as you want. We only need to take care of a unique service name and a unique hostname for each node.

See the complete file below:

Step 6: Build and run docker-compose

We are done with writing all code and configuration required. It’s time to give it a spin!

Run the below commands from the current directory:

# Build images
docker-compose build

# Once build is over, let's run
docker-compose up

Check that the docker-compose up command output has all 6 enode values. See below for an example.

Enode in the docker-compose output
private-blockchain-docker-compose-miner-2-1     | INFO [05-01|16:56:20.927] Started P2P networking                   self=enode://ecd1453c94def09a417cc064e8af06deaefd87019810f8abccfd00dd778ae625787568c1288746c0f792ccdc75be5b4cb5cd7d36d5230523d8a586b05a3e842b@127.0.0.1:30303

Once one epoch is done, we can start working with nodes. You can check epoch progress in docker-compose up command output. It will be similar like below:

private-blockchain-docker-compose-miner-4-1     | INFO [05-01|17:13:21.204] Generating DAG in progress               epoch=0 percentage=99 elapsed=16m57.009s
private-blockchain-docker-compose-miner-4-1 | INFO [05-01|17:13:21.213] Generated ethash verification cache epoch=0 elapsed=16m57.018s

Check all the containers up and running like below:

Go to the terminal of one miner node and run the geth attach command to get access to the Javascript console.

Enode string for a node

You can check the enode of each node by running admin.nodeInfo.enode command.

> admin.nodeInfo.enode
"enode://ecd1453c94def09a417cc064e8af06deaefd87019810f8abccfd00dd778ae625787568c1288746c0f792ccdc75be5b4cb5cd7d36d5230523d8a586b05a3e842b@127.0.0.1:30303"

Finally, let’s do a transaction.

First, check the balance of our first account. Create a new account. Unlock the first account. Do the transaction and check the balance of the new account.

> eth.accounts
["0xa366fb7942fdb3abe2651b82bbb5057477b4c79d"]

> eth.getBalance(eth.accounts[0])
4000000000000000000
> personal.newAccount()
Passphrase:
Repeat passphrase:
"0x04e9e71be9885c7fa02d9abd6b182dcebf59cdcd"

> eth.accounts
["0xa366fb7942fdb3abe2651b82bbb5057477b4c79d", "0x04e9e71be9885c7fa02d9abd6b182dcebf59cdcd"]

> eth.getBalance(eth.accounts[1])
0
> personal.unlockAccount(eth.accounts[0], "hemant")
true
> eth.sendTransaction({from:eth.accounts[0], to:"0x04e9e71be9885c7fa02d9abd6b182dcebf59cdcd", value: web3.toWei(4, "ether")})
Error: insufficient funds for transfer
at web3.js:6347:37(47)
at web3.js:5081:62(37)
at <eval>:1:20(18)

> eth.sendTransaction({from:eth.accounts[0], to:"0x04e9e71be9885c7fa02d9abd6b182dcebf59cdcd", value: web3.toWei(1, "ether")})
"0x7ab5845488e6dc806d548099ca8e19a518f30edf9223e9cdd2fd2dc7ca642275"
> eth.getBalance(eth.accounts[1])
1000000000000000000
> eth.getTransaction("0x7ab5845488e6dc806d548099ca8e19a518f30edf9223e9cdd2fd2dc7ca642275")
{
blockHash: "0x990fd1fcb2d0a292e4a54a1911f31baac0c591534a6e46080ee88dda51e61215",
blockNumber: 3,
from: "0xa366fb7942fdb3abe2651b82bbb5057477b4c79d",
gas: 21000,
gasPrice: 1000000000,
hash: "0x7ab5845488e6dc806d548099ca8e19a518f30edf9223e9cdd2fd2dc7ca642275",
input: "0x",
nonce: 0,
r: "0x77a49322203ef2d726d311a0574fb13d67d9c14d9e34809186eb0100dbb9d3d6",
s: "0xd34594109748dd08499faf099396198ee21b6facdbac35ba2c0107b927a8263",
to: "0x04e9e71be9885c7fa02d9abd6b182dcebf59cdcd",
transactionIndex: 0,
type: "0x0",
v: "0x1b24",
value: 1000000000000000000
}
>
> eth.getBlock("0x990fd1fcb2d0a292e4a54a1911f31baac0c591534a6e46080ee88dda51e61215")
{
difficulty: 131072,
extraData: "0xd683010a01846765746886676f312e3136856c696e7578",
gasLimit: 11964883,
gasUsed: 21000,
hash: "0x990fd1fcb2d0a292e4a54a1911f31baac0c591534a6e46080ee88dda51e61215",
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner: "0xa366fb7942fdb3abe2651b82bbb5057477b4c79d",
mixHash: "0x95e0018d9f30ae1986bcc79e29972e681941029f6557340efae79a8362ca9649",
nonce: "0x627be7f554432788",
number: 3,
parentHash: "0xd969fdfa6523e2c1946f474898750d47258a1caa5574f9cb0a03cbe0414ee17e",
receiptsRoot: "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 648,
stateRoot: "0x67f598e0e07d2e2f69dd5c6efd55aec1533ece2816cfb9f53a3cd508be437182",
timestamp: 1682961802,
totalDifficulty: 393217,
transactions: ["0x7ab5845488e6dc806d548099ca8e19a518f30edf9223e9cdd2fd2dc7ca642275"],
transactionsRoot: "0x82ad63aea108551a6f82c726b93c46b3b869782fdaf60b20b6ed68d9c6ed4860",
uncles: []
}
>
Send transaction, its details, and block details

One last point, You can do geth attach on each miner node terminal and run eth.blockNumber command to check if they are almost running the nearby number blocks.

Hope you enjoy the post!

Follow me and subscribe to my newsletter at www.hemantkgupta.com to get the insight of computer science research papers.

Happy BUIDLing.

--

--

Hemant Gupta

https://www.linkedin.com/in/hkgupta/ Working on the challenge to create insights for 100 software eng papers. #100PapersChallenge, AVP Engineering at CoinDCX