Running a Private Ethereum Blockchain using Docker

This post describes how to setup and run a cluster of nodes to build a private Ethereum network in Docker environment.

tor
SCB Engineer
9 min readMay 14, 2021

--

Although there are many public Ethereum test networks for testing or using for smart contract and decentralized application (dApp) developments, running our own private Ethereum network could still be useful as well as helping us to gain a better understanding on how blockchain works.

The private Ethereum network nodes

Ethereum network is a peer-to-peer network consists of multiple nodes running the Ethereum client such as Geth or OpenEthereum.

In this post, we are setting up a private network with 3 nodes as follows:

  1. Bootnode — the bootstrap node that is used for peer discovery purpose. It listens on port 30303, the other nodes joining the network connect to this bootnode first.
  2. JSON-RPC endpoint — this node exposes JSON-RPC API over HTTP endpoint on port 8545. We will publish the port 8545 of this node container to the host machine to allow external interaction with this private blockchain.
  3. Miner — this node is responsible for mining (the process of creating a new block in our blockchain). When the miner node successfully mines a new block, it receives the rewards into the configured account.
Client nodes in the Private Ethereum Network
Figure 1. Client nodes in the Private Ethereum Network

The genesis block

To setup a private Ethereum blockchain, the Ethereum client requires some information for creating the first block which is called the genesis block.

Here is a simple custom genesis.json containing information to create the genesis block.

We will not go into detail of each parameter here but let’s consider these 2

  • config.chainId — this is the identifier to tell the nodes which blockchain they are on. The chainId was introduced in EIP-155 for replay protection. We set it to be 1214 to avoid conflicting with the public Ethereum networks.
  • config.ethash — this indicates that our blockchain will be using proof-of-work as the consensus engine.

Docker image for Ethereum client

For the Ethereum nodes in our private blockchain, we will use Go Ethereum (Geth) as the client. So let’s create a Dockerfile for building the image of our Ethereum client.

  1. We use official Geth image from Docker Hub the base image.
FROM ethereum/client-go:v1.10.1

2. Copy the genesis.json into the image and use it to initialize the genesis block

COPY genesis.json /tmp
RUN geth init /tmp/genesis.json

3. Remove the nodekey file

RUN rm -f ~/.ethereum/geth/nodekey

The nodekey file is generated when Geth is initializing, it is created the file in the folder named geth under its data directory which (by default) is located at ~/.ethereum.

The nodekey file is used to generate the Geth enode which is kind of the id of each node in Ethereum network.

As we will use the same image built from the Dockerfile to run all the nodes in our Ethereum network, we have to delete the nodekey file created while initializing Geth. So the file will be recreated with different key when each node is starting, otherwise every node in our private network will have the same enode which will prevent the nodes to be connected and synced together.

4. Generate a new account for our Ethereum blockchain. Then remove the password file from the image for security purpose.

RUN echo ${ACCOUNT_PASSWORD} > /tmp/password \
&& geth account new --password /tmp/password \
&& rm -f /tmp/password

WARNING: Do not forget and do not share the password, as it is used to access the created account.

5. Combine things together for a full Dockerfile.

Note: The ACCOUNT_PASSWORD will be provided as argument while building an image from the Dockerfile.

Configuring Ethereum nodes in Docker Compose

As described earlier, our private Ethereum network consists of 3 nodes connected together. In order to connect them, we will put all nodes in 172.16.254.0/28 subnet in a Docker’s bridge network.

The cluster of Ethereum nodes in Docker
Figure 2. The cluster of Ethereum nodes in Docker

And here is the Docker Compose file to run the nodes in Docker environment.

As we want to have 3 nodes performing different roles in our private blockchain, therefore, each node will be configured with different set of parameters.

1. Bootnode command parameters

  • nodekeyhex — we specify the nodekey for the bootnode in order to pre-define the enode (as it is generated from the nodekey) of this bootstrap node to use it for configuring other nodes.
  • nodiscover — this node does not have to discover other nodes because the others will connect to it when joining the network.
  • ipcdisable — to make the node more light-weight as it is only used as bootstrap node.
  • networkid — specify the identifier of the network, every node in the same Ethereum network must have the same networkid. Avoid the values which conflict with the public Ethereum network.
  • netrestrict — to only accept the connection from nodes within the CIDR range.

3. JSON-RPC endpoint node command parameters

  • bootnodes— specify the list of nodes to connect to for peer discovery
  • allow-insecure-unlock — to allow unlocking the account we created over the HTTP. This is unsafe if the endpoint is exposed to external, consider remove this parameter in the production environment.
  • http — to enable JSON-RPC over HTTP protocol.
  • http.api — the list of APIs to enable via HTTP-RPC interface. In production environment, specify only the ones which are required.
  • http.addr — set to 0.0.0.0 to accept HTTP connection on all IPs of the node container.
  • http.corsdomain — to allow connection from cross-origin web pages.
  • networkid — specify the identifier of the network, every node in the same Ethereum network must have the same networkid. Avoid the values which conflict with the public Ethereum network.
  • netrestrict — to only accept the connection from nodes within the CIDR range.

4. Miner node command parameters

  • bootnodes — specify the list of nodes to connect to for peer discovery
  • mine — to enable the mining for this node
  • miner.threads — specify the number of CPU thread to use for mining
  • miner.etherbase—specify the address of the account to receive mining rewards. We do not specify it here, so the rewards go to the primary account we created by default.
  • networkid — specify the identifier of the network, every node in the same Ethereum network must have the same networkid. Avoid the values which conflict with the public Ethereum network.
  • netrestrict — to only accept the connection from nodes within the CIDR range.

Running and interacting with the private Ethereum network

To run our private Ethereum network from the docker-compose.yaml file, we have to specify 2 environment variables in the .env file

Then start the nodes with the command:

docker-compose up -d

After the containers are up and running, we can interacting with the blockchain via the Inter-process Communication (IPC) or the RPC endpoint.

We can use a software wallet, such as MetaMask or MyCrypto, to connect to our private blockchain via the HTTP-RPC endpoint. However, this post will use curl to demonstrate how we interact with our private blockchain.

As we map the HTTP-RPC port 8545 from the container to our host machine, we can then connect to it at localhost:8545 .

  1. First of all, let’s check the connectivity of the nodes.
curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "admin_peers",
"params": []
}'

Since we are talking to the RPC endpoint node, we will see that the peer it is connecting to is the bootnode (recognized from the enode value is the same as we set as bootnodes parameters in the docker-compose.yml).

{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
"enode": "enode://af22c29c316ad069cf48a09a4ad5cf04a251b411e45098888d114c6dd7f489a13786620d5953738762afa13711d4ffb3b19aa5de772d8af72f851f7e9c5b164a@172.16.254.2:30303",
...}

2. Then, get the latest block number of the blockchain.

curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 2,
"method": "eth_blockNumber",
"params": []
}'

After running the nodes for a while, we should see this number greater than 0x0, which means our miner node already created other blocks after the genesis block.

{
"jsonrpc": "2.0",
"id": 2,
"result": "0x3"
}

Note: The miner node takes several minutes to initialize before it starts mining. However if the miner has already been running for some time but still get the 0x0 as the latest block number, we should go check the log of the miner node.

3. Next, we will get the address of our primary account created while the image is being built.

curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 3,
"method": "eth_accounts",
"params": []
}'

The response will contain a list of the created accounts, in our case there should be only 1 account for now.

{
"jsonrpc": "2.0",
"id": 3,
"result": [
"0x08d1f47128f5c04d7a4aee69e90642645059acd6"
]
}

Note: The address of the account will be different as it is generated while the image is being built.

4. Now, let’s see how rich we are 😁

curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 4,
"method": "eth_getBalance",
"params": [
"0x08d1f47128f5c04d7a4aee69e90642645059acd6",
"latest"
]
}'

Note: We send the address of our created account as a parameter to check its balance.

If the latest block number of our private blockchain is greater than 0x0, we should see non-zero balance in the account. As this first account we created is, by default, the one receives the mining rewards.

{
"jsonrpc": "2.0",
"id": 4,
"result": "0x19b21248a3ef280000"
}

Note: The balance is in hexadecimal number and in wei unit.

5. To test transferring some funds, we will create another account to be the recipient.

curl --location --request POST 'http://localhost:8545' \
--header 'Content-type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 5,
"method": "personal_newAccount",
"params": [
"5uper53cr3t"
]
}'

Note: To create an account, we have to specify the password as the parameter. This password is required to access the account later. Keep it safe and secret.

If success, we will get the address of the newly created account in return.

{
"jsonrpc": "2.0",
"id": 5,
"result": "0x2bc05c71899ecff51c80952ba8ed444796499118"
}

6. Before sending a transaction, we must make sure that the sender account is unlocked.

curl --location --request POST 'http://localhost:8545' \
--header 'Content-type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 6,
"method": "personal_unlockAccount",
"params": [
"0x08d1f47128f5c04d7a4aee69e90642645059acd6",
"5uper53cr3t"
]
}'

Note: We will use our primary account as sender as it receives fund from mining rewards. The password used to unlock here must be the same as the one we use to create the account.

If the account is unlocked successfully, it returns true.

{
"jsonrpc": "2.0",
"id": 6,
"result": true
}

7. Now we are ready to transfer some fund.

curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 7,
"method": "eth_sendTransaction",
"params": [
{
"from": "0x08d1f47128f5c04d7a4aee69e90642645059acd6",
"to": "0x2bc05c71899ecff51c80952ba8ed444796499118",
"value": "0xf4240"
}
]
}'

Note: We are sending 0xf4240 wei (equal to 1 ether) from the account, address 0x08d1f47128f5c04d7a4aee69e90642645059acd6 to the account 0x2bc05c71899ecff51c80952ba8ed444796499118.

If the transaction is successfully submitted, we will get the transaction hash in return.

{
"jsonrpc": "2.0",
"id": 7,
"result": "0xa96de080dfcb9c5f908528b92d3df55a0e230cf4e48ae178bb220862c2a544c7"
}

8. We can get the status of a transaction by its hash.

curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 8,
"method": "eth_getTransactionByHash",
"params": [ "0xa96de080dfcb9c5f908528b92d3df55a0e230cf4e48ae178bb220862c2a544c7"
]
}'

And the response will look like:

{
"jsonrpc": "2.0",
"id": 8,
"result": {
"blockHash": "0xf31b1a454f1cd35388476acead342fe93c02667ac99f136a36ef677c462ad04d",
"blockNumber": "0x16f2",
"from": "0x08d1f47128f5c04d7a4aee69e90642645059acd6",
"gas": "0x5208",
"gasPrice": "0x3b9aca00",
"hash": "0xa96de080dfcb9c5f908528b92d3df55a0e230cf4e48ae178bb220862c2a544c7",
"input": "0x",
"nonce": "0x0",
"to": "0x2bc05c71899ecff51c80952ba8ed444796499118",
"transactionIndex": "0x0",
"value": "0xf4240",
"type": "0x0",
"v": "0x99b",
"r": "0xc32053edadd067e93480d76e24fcd03e9879335739c06804916cd292c380cad1",
"s": "0x5f49f5b8c773babff4752abe6662d8f0a1b61e336b051954bd265bf13cd695e9"
}
}

9. Lastly, verify if we get the fund in the recipient’s account.

curl --location --request POST 'localhost:8545' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 9,
"method": "eth_getBalance",
"params": [
"0x2bc05c71899ecff51c80952ba8ed444796499118",
"latest"
]
}'

We expect to see the same value (0xf4240) as we sent from the primary account as the result.

{
"jsonrpc": "2.0",
"id": 9,
"result": "0xf4240"
}

Note: Normally, the transaction submitted to the blockchain takes some time to be processed by miners. But for our private network, it should be quick. If we still do not receive the fund after a while, we can go back to recheck the transaction status again.

✅ That’s it! We have the working private Ethereum blockchain ready for further experimental now.

--

--