Executive summary

Sophisticated system deployment and launch had been (mostly) successfully migrated to the docker.

Launch of the system is done within one click/one command. Utilization of a host machine resources becomes more efficient - ratio between speed and CPU\RAM footprint.

In many aspects it is better rather than VM usage, except it doesn't give you a full isolation and many things might go wrong if you or 3rd party developers will give root privileges for some of your containers.

Another aspect to consider is some things doesn't work out-of-the-box and requires additional efforts for customization and sometime bugfix as it was with blockchain calculated ledger retention. Maintenance of docker scripts might has its own cost.

Why docker

With project growth there is an increasing demand to speed-up deployment process - smooth and fast, preferably with one click. So far many different tech has been used for this deployed and run into virtual machine. By putting VM into the hibernate mode we could stop and immediately resume our running services.

At the first view Docker looks us a superior solution for a current virtualization technology. Indeed experience shown it is quicker and better utilize machine resources and can be a better alternative for existing VMs, but it is not a quite virtualization technology. Docker proficiently uses OS isolation and virtualization mechanisms to run its containers/applications, but they are executed directly on the host machine kernel vs guest OS in case of VMs. From one side it increase performance and portability, on another side because of direct launch on the host OS kernel isolation is not so robust - giving root privileges to an application might lead to dangerous results which is not an option in case of virtual machines.

Docker image vs docker compose

Docker allows to create an images of your software and quickly deploy on your machine. However, if you have many components in your projects you have to look at Docker compose.

Docker compose allows to orchestrate and tune a variety of applications which should work together. Before that each application has to be dockerised - wrapped into docker image (build and configuration file)

There is also Docker swarm (clusters) and Kubernetes (containers in cloud) which are out of scope of this publication. You can learn more about them here and here.

Custom image per each service

Majority of popular products already offer their docker image which usually can be tuned over input parameters passed from docker compose config file. However, if you need to customize an image, docker compose is not intended for this - you have to write your own image. In my practice I have to write my own image based on existing ones.

General practice is to put each component of your project, e.g. server, database, etc, to a separate service. Docker images are recommended to keep small and concise. For my scenario it is also reasonable to create a separate network for my containers where are just several ports have been forwarded to external world.

My project has a mobile client, a server, database and privately hosted blockchain. Each of this components except of a mobile client became a separate service in docker.

Docker custom image: database

Below is my script for a database container. It uses general mysql image without any customization. Docker allows to map host's machine volumes, in this container it is used to automate schema creation - anything within docker-entrypoint-initdb.d will be executed on mysql startup.

                database:
                  hostname: swap-database
                  container_name: swap-database-default
                  image: mysql:8.0.33 
                  env_file:
                    - .env
                  environment:
                    - "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}"
                    - "MYSQL_DATABASE=${MYSQL_DATABASE}"
                    - "MYSQL_USER=${MYSQL_USER}"
                    - "MYSQL_PASSWORD=${MYSQL_PASSWORD}"
                    - "MYSQL_ROOT_HOST=${MYSQL_ROOT_HOST}"
                  volumes:
                    - ./database/custom-sql-commands.sql:/docker-entrypoint-initdb.d/custom-sql-commands.sql 
                    - ./database/custom.cnf:/etc/custom.cnf
                    - ./infrastructure/mysql:/var/lib/mysql
                    - ./server/db_schema:/docker-entrypoint-initdb.d:rw
                  ports:
                    - "3307:3306"
                  networks:
                    priv-eth-net:
                  restart: on-failure
                  deploy:
                    resources:
                      limits:
                        cpus: '0.5'
                        memory: 512M
                  healthcheck:
                    test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
                    interval: 7s
                    timeout: 20s
                    retries: 3
            


Docker provides features to check application state within container over healthcheck block. In this case a plain ping is used.

Docker custom image: server

The similar script is used for a server deployment and launch:

                server:
                  hostname: swap-server
                  container_name: server
                  image: node:18-alpine 
                  working_dir: /usr/src/swap/server
                  volumes:
                    - ./server:/usr/src/swap/server
                    - ./infrastructure/server:/root
                  command: > 
                    sh -c "npm install && npm run start:docker"
                  ports:
                    - "3000:3000" # port depends on environment config, keep an eye on it when env will be changed
                  networks:
                    priv-eth-net:
                  deploy:
                    resources:
                      limits:
                        cpus: '0.5'
                        memory: 512M
                  depends_on:
                    database:
                      condition: service_healthy
            


Linux Alpine image is used because of its lightweightness. Docker allows to setup order in which containers will be launched (depends_on block)

Docker custom image: blockchain

Blockchain require three services: bootnode, miner and rpc-endpoint. For this aims I also had to write custom images.

                # blockchain section
                geth-bootnode:
                  hostname: geth-bootnode
                  container_name: geth-bootnode
                  env_file:
                    - .env
                  image: geth-client
                  build:
                    context: ./blockchain/private-chain/node-bootnode
                    args:
                      - ACCOUNT_PASSWORD=${ACCOUNT_PASSWORD}
                      - NODE_ACCOUNT_PWD_FILENAME=${NODE_1_ACCOUNT_PWD_FILENAME}
                      - NODE_ACCOUNT_PWD=${NODE_1_ACCOUNT_PWD}
                      - NODE_ACCOUNT=${NODE_1_ACCOUNT}
                      - NETWORK_ID=${NETWORK_ID}
                      - BOOTNODE_NODEKEYHEX=${BOOTNODE_NODEKEYHEX}
                      - NETWORK=${NETWORK}
                  environment:
                    - NODE_ACCOUNT=${NODE_1_ACCOUNT}
                  # Ref. issue: https://github.com/ethereum/go-ethereum/issues/27298
                  volumes:
                    # - ./infrastructure/eth-chain/bootnode/geth/chaindata:/tmp/geth/chaindata
                    - ./infrastructure/eth-chain/bootnode/geth/ethash:/tmp/geth/ethash
                    - ./infrastructure/eth-chain/bootnode/ethash:/root/.ethash
                    - /etc/localtime:/etc/localtime:ro
                  networks:
                    priv-eth-net:
              
                geth-rpc-endpoint:
                  hostname: geth-rpc-endpoint
                  container_name: geth-rpc-endpoint
                  env_file:
                    - .env
                  build:
                    context: ./blockchain/private-chain/node-geth-rpc-endpoint
                    args: 
                      - NODE_ACCOUNT_PWD_FILENAME=${NODE_2_ACCOUNT_PWD_FILENAME}
                      - NODE_ACCOUNT_PWD=${NODE_2_ACCOUNT_PWD}
                      - NODE_ACCOUNT=${NODE_2_ACCOUNT}
                      - NETWORK_ID=${NETWORK_ID}
                      - BOOTNODE=${BOOTNODE}
                      - NETWORK=${NETWORK}
                  environment:
                    - NODE_ACCOUNT=${NODE_2_ACCOUNT}
                  volumes:
                    # - ./infrastructure/eth-chain/rpc-endpoint/geth/chaindata:/tmp/geth/chaindata
                    - ./infrastructure/eth-chain/rpc-endpoint/geth/ethash:/tmp/geth/ethash
                    - ./infrastructure/eth-chain/rpc-endpoint/ethash:/root/.ethash
                    - /etc/localtime:/etc/localtime:ro
                  depends_on:
                    - geth-bootnode    
                  ports:
                    - "8545:8545"
                  networks:
                    priv-eth-net:
              
                geth-miner:
                  hostname: geth-miner
                  container_name: geth-miner
                  env_file:
                    - .env
                  build:
                    context: ./blockchain/private-chain/node-miner
                    args: 
                      - NODE_ACCOUNT_PWD_FILENAME=${NODE_3_ACCOUNT_PWD_FILENAME}
                      - NODE_ACCOUNT_PWD=${NODE_3_ACCOUNT_PWD}
                      - ETHERBASE_ACCOUNT=${NODE_3_ACCOUNT}
                      - NODE_ACCOUNT=${NODE_3_ACCOUNT}
                      - NETWORK_ID=${NETWORK_ID}
                      - BOOTNODE=${BOOTNODE}
                      - NETWORK=${NETWORK}
                  environment:
                    - NODE_ACCOUNT=${NODE_3_ACCOUNT}
                  volumes:
                    # - ./infrastructure/eth-chain/miner/geth/chaindata:/tmp/geth/chaindata
                    - ./infrastructure/eth-chain/miner/geth/ethash:/tmp/geth/ethash
                    - ./infrastructure/eth-chain/miner/ethash:/root/.ethash
                    - /etc/localtime:/etc/localtime:ro
                  depends_on:
                    - geth-bootnode
                  networks:
                    priv-eth-net:
              
                  deploy:
                    resources:
                      limits:
                        cpus: '2'
                        memory: 2048M
            


Environment arguments for custom image are passed from .env file defined for each service. Matched volumes allows to preserve setup and configuration of the chain, but I had not managed yet to preserve calculated ledger between launches. There is unclear error when necessary path has been mapped that left unresolved for now. It is good for tests or development mode, but for launch in production this issue should be solved.

Growth of the ledger might easily occupy all available space. Docker support host machine resource constraints used by each container and limits for CPU and RAM has been set.

Below is a custom docker image for one of this blockchain services. The rest are looking similar:

                FROM ethereum/client-go:v1.10.1

                ARG NODE_ACCOUNT
                ARG NODE_ACCOUNT_PWD
                ARG NETWORK_ID
                ARG BOOTNODE
                ARG NETWORK

                ENV NODE_ACCOUNT=${NODE_ACCOUNT}
                ENV NETWORK_ID=${NETWORK_ID}
                ENV BOOTNODE_NODEKEYHEX=${BOOTNODE_NODEKEYHEX}
                ENV NETWORK=${NETWORK}

                COPY ./swap.ethash.genesis.json /tmp

                COPY ./boot.key /tmp

                RUN mkdir -p /tmp/keystore

                COPY ./keystore/${NODE_ACCOUNT_PWD_FILENAME} /tmp/keystore

                RUN geth --datadir /tmp init /tmp/swap.ethash.genesis.json \
                    && rm -f ~/.ethereum/geth/nodekey 

                RUN echo ${NODE_ACCOUNT_PWD} >> /tmp/pwd.txt

                COPY ./entrypoint.sh /tmp

                RUN chmod +x ./tmp/entrypoint.sh

                # This way entrypoint usage would ignore OS signals sent to the container. That might cause some issues
                ENTRYPOINT ./tmp/entrypoint.sh ${NODE_ACCOUNT} ${NETWORK_ID} ${BOOTNODE_NODEKEYHEX} ${NETWORK}
            


To use input arguments passed from docker compose config file, they should be copied in local ENVIRONMENT variables.

Some commands are not allowed to be launched from docker image and a separate shell script is used as a workaround for this.

For more details you can check the source code of the project.

A separate network for docker

A separate network for group of containers isolates all project's services from external world, except ports which are intentionally forwarded for external access. Which network to use is defined for each service above and how network has been setup is shown below:

                networks:
                  priv-eth-net:
                    driver: bridge
                    ipam:
                      config:
                        - subnet: 172.16.254.0/28