Installing and Configuring Blockchain Nodes on Ubuntu 24 LTS,
Part 4: Writing Docker Compose Files and Launching the Network

Write Docker Compose files for orderers, peers, CouchDB, and certificate authorities, then launch and verify all 13 FjordTrade containers forming a Raft-based Hyperledger Fabric network.

In Part 3, you generated all the cryptographic material and channel artifacts that these containers need: MSP certificates, TLS certificates, private keys, the genesis block, and the channel creation transaction. This part takes those artifacts and wires them into Docker Compose files that define every container, its environment variables, volume mounts, port mappings, and startup dependencies. By the end, all 13 FjordTrade containers will be running: 3 orderers forming a Raft cluster, 4 peers with CouchDB sidecars, and 2 certificate authority services.

FjordTrade is the scenario company used throughout this series. FjordTrade is a Nordic commodity trading platform that facilitates cross-border trade settlement across three offices: Oslo (Org1), Helsinki (Org2), and Tallinn (Org3, added later). Each office operates its own organization node within a permissioned Hyperledger Fabric network.

Free to use, share it in your presentations, blogs, or learning materials.
FjordTrade Docker Compose deployment topology showing 4 compose file groups containing 13 containers connected through a shared Docker bridge network
FjordTrade Docker Compose deployment topology with 13 containers organized across four compose files, all connected through the shared fjordtrade_network Docker bridge.

The topology above shows how the FjordTrade deployment is split across four Docker Compose files. The orderer compose file manages the three-node Raft consensus cluster. Each peer organization has its own compose file containing two peers and two CouchDB instances. A separate compose file handles the certificate authorities. All 13 containers connect to a single Docker bridge network, allowing them to communicate using container hostnames.

Prerequisites

Before proceeding, confirm the following.

Completed Parts 1 through 3: Docker CE is running, all Fabric CLI tools are installed, and the crypto-config/ and channel-artifacts/ directories contain the generated material from Part 3.

Verify artifacts are in place
$ ls ~/fjordtrade-network/channel-artifacts/
$ ls ~/fjordtrade-network/crypto-config/peerOrganizations/
$ ls ~/fjordtrade-network/crypto-config/ordererOrganizations/
Expected output
Org1MSPanchors.tx  Org2MSPanchors.tx  fjordtradechannel.tx  genesis.block
org1.fjordtrade.com  org2.fjordtrade.com
orderer.fjordtrade.com

Creating the Shared Environment File

All four compose files share common variables like the project name, Fabric image tags, and CA image versions. Rather than duplicating these values in every file, define them once in a .env file that Docker Compose reads automatically.

Create the shared environment file
$ vim ~/fjordtrade-network/docker/.env
docker/.env
COMPOSE_PROJECT_NAME=fjordtrade
IMAGE_TAG=2.5.10
CA_IMAGE_TAG=1.5.12
COUCHDB_IMAGE_TAG=3.3.3
FABRIC_CFG_PATH=/etc/hyperledger/fabric

Press Esc, type :wq, press Enter to save and exit.

The COMPOSE_PROJECT_NAME sets the prefix for all container names and volumes. IMAGE_TAG controls which Fabric peer and orderer image versions are pulled. These values are referenced in compose files using ${IMAGE_TAG} syntax.

Writing the Orderer Compose File

The orderer compose file defines three Raft consensus nodes. Each orderer mounts the genesis block, its own MSP directory, and its TLS certificates. The three orderers communicate with each other to elect a leader and replicate the transaction log.

Free to use, share it in your presentations, blogs, or learning materials.
Three-node Raft orderer cluster showing orderer1 as leader replicating logs to orderer2 and orderer3 followers with heartbeat connections and mutual TLS
Raft consensus topology for the FjordTrade orderer cluster, showing leader election, log replication from leader to followers, and mutual TLS heartbeat connections between all three nodes.

The Raft cluster above shows how orderer1 acts as the initial leader, replicating committed log entries to orderer2 and orderer3. If orderer1 goes down, the remaining two nodes hold an election and promote a new leader. This requires a quorum of 2 out of 3 nodes, meaning the cluster tolerates one node failure without losing availability.

Create the orderer compose file
$ vim ~/fjordtrade-network/docker/docker-compose-orderer.yaml
docker/docker-compose-orderer.yaml
version: ‘3.7’

volumes:
  orderer1_data:
  orderer2_data:
  orderer3_data:

networks:
  fjordtrade_network:
    name: fjordtrade_network

services:
  orderer1.orderer.fjordtrade.com:
    container_name: orderer1.orderer.fjordtrade.com
    image: hyperledger/fabric-orderer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
      – ORDERER_GENERAL_LISTENPORT=7050
      – ORDERER_GENERAL_LOCALMSPID=OrdererMSP
      – ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
      – ORDERER_GENERAL_TLS_ENABLED=true
      – ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_GENERAL_CLUSTER_CLIENTCERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_GENERAL_CLUSTER_CLIENTPRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_GENERAL_CLUSTER_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_GENERAL_BOOTSTRAPMETHOD=file
      – ORDERER_GENERAL_BOOTSTRAPFILE=/var/hyperledger/orderer/orderer.genesis.block
      – ORDERER_CHANNELPARTICIPATION_ENABLED=true
      – ORDERER_ADMIN_TLS_ENABLED=true
      – ORDERER_ADMIN_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_ADMIN_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_ADMIN_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_ADMIN_TLS_CLIENTROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_ADMIN_LISTENADDRESS=0.0.0.0:7053
      – ORDERER_OPERATIONS_LISTENADDRESS=0.0.0.0:9443
      – ORDERER_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: orderer
    volumes:
      – ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
      – ../crypto-config/ordererOrganizations/orderer.fjordtrade.com/orderers/orderer1.orderer.fjordtrade.com/msp:/var/hyperledger/orderer/msp
      – ../crypto-config/ordererOrganizations/orderer.fjordtrade.com/orderers/orderer1.orderer.fjordtrade.com/tls:/var/hyperledger/orderer/tls
      – orderer1_data:/var/hyperledger/production/orderer
    ports:
      – “7050:7050”
      – “7053:7053”
      – “9443:9443”
    networks:
      – fjordtrade_network

  orderer2.orderer.fjordtrade.com:
    container_name: orderer2.orderer.fjordtrade.com
    image: hyperledger/fabric-orderer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
      – ORDERER_GENERAL_LISTENPORT=7050
      – ORDERER_GENERAL_LOCALMSPID=OrdererMSP
      – ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
      – ORDERER_GENERAL_TLS_ENABLED=true
      – ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_GENERAL_CLUSTER_CLIENTCERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_GENERAL_CLUSTER_CLIENTPRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_GENERAL_CLUSTER_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_GENERAL_BOOTSTRAPMETHOD=file
      – ORDERER_GENERAL_BOOTSTRAPFILE=/var/hyperledger/orderer/orderer.genesis.block
      – ORDERER_CHANNELPARTICIPATION_ENABLED=true
      – ORDERER_ADMIN_TLS_ENABLED=true
      – ORDERER_ADMIN_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_ADMIN_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_ADMIN_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_ADMIN_TLS_CLIENTROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_ADMIN_LISTENADDRESS=0.0.0.0:8053
      – ORDERER_OPERATIONS_LISTENADDRESS=0.0.0.0:9444
      – ORDERER_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: orderer
    volumes:
      – ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
      – ../crypto-config/ordererOrganizations/orderer.fjordtrade.com/orderers/orderer2.orderer.fjordtrade.com/msp:/var/hyperledger/orderer/msp
      – ../crypto-config/ordererOrganizations/orderer.fjordtrade.com/orderers/orderer2.orderer.fjordtrade.com/tls:/var/hyperledger/orderer/tls
      – orderer2_data:/var/hyperledger/production/orderer
    ports:
      – “8050:7050”
      – “8053:8053”
      – “9444:9444”
    networks:
      – fjordtrade_network

  orderer3.orderer.fjordtrade.com:
    container_name: orderer3.orderer.fjordtrade.com
    image: hyperledger/fabric-orderer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
      – ORDERER_GENERAL_LISTENPORT=7050
      – ORDERER_GENERAL_LOCALMSPID=OrdererMSP
      – ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
      – ORDERER_GENERAL_TLS_ENABLED=true
      – ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_GENERAL_CLUSTER_CLIENTCERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_GENERAL_CLUSTER_CLIENTPRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_GENERAL_CLUSTER_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_GENERAL_BOOTSTRAPMETHOD=file
      – ORDERER_GENERAL_BOOTSTRAPFILE=/var/hyperledger/orderer/orderer.genesis.block
      – ORDERER_CHANNELPARTICIPATION_ENABLED=true
      – ORDERER_ADMIN_TLS_ENABLED=true
      – ORDERER_ADMIN_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
      – ORDERER_ADMIN_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
      – ORDERER_ADMIN_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_ADMIN_TLS_CLIENTROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
      – ORDERER_ADMIN_LISTENADDRESS=0.0.0.0:9053
      – ORDERER_OPERATIONS_LISTENADDRESS=0.0.0.0:9445
      – ORDERER_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: orderer
    volumes:
      – ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
      – ../crypto-config/ordererOrganizations/orderer.fjordtrade.com/orderers/orderer3.orderer.fjordtrade.com/msp:/var/hyperledger/orderer/msp
      – ../crypto-config/ordererOrganizations/orderer.fjordtrade.com/orderers/orderer3.orderer.fjordtrade.com/tls:/var/hyperledger/orderer/tls
      – orderer3_data:/var/hyperledger/production/orderer
    ports:
      – “9050:7050”
      – “9053:9053”
      – “9445:9445”
    networks:
      – fjordtrade_network

Press Esc, type :wq, press Enter to save and exit.

Each orderer service follows the same pattern. The key environment variables control TLS (enabled for all connections), the MSP identity (OrdererMSP for all three), and the bootstrap method (file-based, reading the genesis block). The volume mounts connect three host directories into each container: the genesis block file, the node’s MSP directory (signing certificates and CA roots), and the node’s TLS directory (server certificate and key). A named volume persists the orderer’s production data across container restarts.

The port mappings differ for each orderer because all three run on the same host machine. Orderer1 maps host port 7050 to container port 7050, orderer2 maps 8050 to 7050, and orderer3 maps 9050 to 7050. Inside the container, each orderer listens on 7050, but the host-side ports must be unique.

Port Allocation Summary

The following port allocation applies across all FjordTrade services. Each service listens on a standard internal port, mapped to a unique host port.

FjordTrade port allocation
# Orderer Services
orderer1    Host 7050  -> Container 7050  (gRPC)
orderer1    Host 7053  -> Container 7053  (Admin)
orderer1    Host 9443  -> Container 9443  (Operations)
orderer2    Host 8050  -> Container 7050  (gRPC)
orderer2    Host 8053  -> Container 8053  (Admin)
orderer2    Host 9444  -> Container 9444  (Operations)
orderer3    Host 9050  -> Container 7050  (gRPC)
orderer3    Host 9053  -> Container 9053  (Admin)
orderer3    Host 9445  -> Container 9445  (Operations)

# Peer Services
peer0.org1  Host 7051  -> Container 7051  (gRPC)
peer1.org1  Host 8051  -> Container 7051  (gRPC)
peer0.org2  Host 9051  -> Container 7051  (gRPC)
peer1.org2  Host 10051 -> Container 7051  (gRPC)

# CouchDB Instances
couchdb0.org1  Host 5984  -> Container 5984  (HTTP)
couchdb1.org1  Host 6984  -> Container 5984  (HTTP)
couchdb0.org2  Host 7984  -> Container 5984  (HTTP)
couchdb1.org2  Host 8984  -> Container 5984  (HTTP)

# Certificate Authorities
ca.org1     Host 7054  -> Container 7054  (HTTP)
ca.org2     Host 8054  -> Container 8054  (HTTP)

Writing the Peer Org1 Compose File

The Org1 compose file defines two peer nodes and their CouchDB sidecar databases. Each peer mounts its own MSP and TLS directories from the crypto material generated in Part 3. The CouchDB containers provide rich query support against the world state, allowing chaincode to perform complex queries beyond simple key lookups.

Create the Org1 peer compose file
$ vim ~/fjordtrade-network/docker/docker-compose-peer-org1.yaml
docker/docker-compose-peer-org1.yaml
version: ‘3.7’

volumes:
  peer0_org1_data:
  peer1_org1_data:
  couchdb0_org1_data:
  couchdb1_org1_data:

networks:
  fjordtrade_network:
    external: true

services:
  couchdb0.org1.fjordtrade.com:
    container_name: couchdb0.org1.fjordtrade.com
    image: couchdb:${COUCHDB_IMAGE_TAG}
    environment:
      – COUCHDB_USER=fjordtradeadmin
      – COUCHDB_PASSWORD=fjordtrade_couchdb_pw
    volumes:
      – couchdb0_org1_data:/opt/couchdb/data
    ports:
      – “5984:5984”
    networks:
      – fjordtrade_network

  couchdb1.org1.fjordtrade.com:
    container_name: couchdb1.org1.fjordtrade.com
    image: couchdb:${COUCHDB_IMAGE_TAG}
    environment:
      – COUCHDB_USER=fjordtradeadmin
      – COUCHDB_PASSWORD=fjordtrade_couchdb_pw
    volumes:
      – couchdb1_org1_data:/opt/couchdb/data
    ports:
      – “6984:5984”
    networks:
      – fjordtrade_network

  peer0.org1.fjordtrade.com:
    container_name: peer0.org1.fjordtrade.com
    image: hyperledger/fabric-peer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – CORE_PEER_ID=peer0.org1.fjordtrade.com
      – CORE_PEER_ADDRESS=peer0.org1.fjordtrade.com:7051
      – CORE_PEER_LISTENADDRESS=0.0.0.0:7051
      – CORE_PEER_CHAINCODEADDRESS=peer0.org1.fjordtrade.com:7052
      – CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
      – CORE_PEER_GOSSIP_BOOTSTRAP=peer1.org1.fjordtrade.com:8051
      – CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.fjordtrade.com:7051
      – CORE_PEER_LOCALMSPID=Org1MSP
      – CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/fabric/msp
      – CORE_PEER_TLS_ENABLED=true
      – CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
      – CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
      – CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
      – CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      – CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb0.org1.fjordtrade.com:5984
      – CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=fjordtradeadmin
      – CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=fjordtrade_couchdb_pw
      – CORE_OPERATIONS_LISTENADDRESS=0.0.0.0:9446
      – CORE_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: peer node start
    volumes:
      – ../crypto-config/peerOrganizations/org1.fjordtrade.com/peers/peer0.org1.fjordtrade.com/msp:/etc/hyperledger/fabric/msp
      – ../crypto-config/peerOrganizations/org1.fjordtrade.com/peers/peer0.org1.fjordtrade.com/tls:/etc/hyperledger/fabric/tls
      – peer0_org1_data:/var/hyperledger/production
      – /var/run/docker.sock:/var/run/docker.sock
    ports:
      – “7051:7051”
    depends_on:
      – couchdb0.org1.fjordtrade.com
    networks:
      – fjordtrade_network

  peer1.org1.fjordtrade.com:
    container_name: peer1.org1.fjordtrade.com
    image: hyperledger/fabric-peer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – CORE_PEER_ID=peer1.org1.fjordtrade.com
      – CORE_PEER_ADDRESS=peer1.org1.fjordtrade.com:8051
      – CORE_PEER_LISTENADDRESS=0.0.0.0:7051
      – CORE_PEER_CHAINCODEADDRESS=peer1.org1.fjordtrade.com:7052
      – CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
      – CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org1.fjordtrade.com:7051
      – CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org1.fjordtrade.com:8051
      – CORE_PEER_LOCALMSPID=Org1MSP
      – CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/fabric/msp
      – CORE_PEER_TLS_ENABLED=true
      – CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
      – CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
      – CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
      – CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      – CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb1.org1.fjordtrade.com:5984
      – CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=fjordtradeadmin
      – CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=fjordtrade_couchdb_pw
      – CORE_OPERATIONS_LISTENADDRESS=0.0.0.0:9447
      – CORE_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: peer node start
    volumes:
      – ../crypto-config/peerOrganizations/org1.fjordtrade.com/peers/peer1.org1.fjordtrade.com/msp:/etc/hyperledger/fabric/msp
      – ../crypto-config/peerOrganizations/org1.fjordtrade.com/peers/peer1.org1.fjordtrade.com/tls:/etc/hyperledger/fabric/tls
      – peer1_org1_data:/var/hyperledger/production
      – /var/run/docker.sock:/var/run/docker.sock
    ports:
      – “8051:7051”
    depends_on:
      – couchdb1.org1.fjordtrade.com
    networks:
      – fjordtrade_network

Press Esc, type :wq, press Enter to save and exit.

The CouchDB services start first because peers depend on them via the depends_on directive. Each peer points to its specific CouchDB instance through the CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS variable. The gossip bootstrap configuration cross-references the other peer in the same organization: peer0 bootstraps from peer1, and peer1 bootstraps from peer0. This ensures block dissemination works even if a peer restarts.

The Docker socket mount (/var/run/docker.sock) allows peers to launch chaincode containers when chaincode is invoked. Without this mount, chaincode deployment will fail with permission errors.

Note that the network is declared as external: true because the orderer compose file creates it. The peer compose files join the existing network rather than creating a new one.

Writing the Peer Org2 Compose File

The Org2 compose file follows the identical structure as Org1 but with different hostnames, MSP paths, port mappings, and the Org2MSP identity. Helsinki’s peers connect to their own CouchDB instances and bootstrap gossip within Org2.

Create the Org2 peer compose file
$ vim ~/fjordtrade-network/docker/docker-compose-peer-org2.yaml
docker/docker-compose-peer-org2.yaml
version: ‘3.7’

volumes:
  peer0_org2_data:
  peer1_org2_data:
  couchdb0_org2_data:
  couchdb1_org2_data:

networks:
  fjordtrade_network:
    external: true

services:
  couchdb0.org2.fjordtrade.com:
    container_name: couchdb0.org2.fjordtrade.com
    image: couchdb:${COUCHDB_IMAGE_TAG}
    environment:
      – COUCHDB_USER=fjordtradeadmin
      – COUCHDB_PASSWORD=fjordtrade_couchdb_pw
    volumes:
      – couchdb0_org2_data:/opt/couchdb/data
    ports:
      – “7984:5984”
    networks:
      – fjordtrade_network

  couchdb1.org2.fjordtrade.com:
    container_name: couchdb1.org2.fjordtrade.com
    image: couchdb:${COUCHDB_IMAGE_TAG}
    environment:
      – COUCHDB_USER=fjordtradeadmin
      – COUCHDB_PASSWORD=fjordtrade_couchdb_pw
    volumes:
      – couchdb1_org2_data:/opt/couchdb/data
    ports:
      – “8984:5984”
    networks:
      – fjordtrade_network

  peer0.org2.fjordtrade.com:
    container_name: peer0.org2.fjordtrade.com
    image: hyperledger/fabric-peer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – CORE_PEER_ID=peer0.org2.fjordtrade.com
      – CORE_PEER_ADDRESS=peer0.org2.fjordtrade.com:9051
      – CORE_PEER_LISTENADDRESS=0.0.0.0:7051
      – CORE_PEER_CHAINCODEADDRESS=peer0.org2.fjordtrade.com:7052
      – CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
      – CORE_PEER_GOSSIP_BOOTSTRAP=peer1.org2.fjordtrade.com:10051
      – CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org2.fjordtrade.com:9051
      – CORE_PEER_LOCALMSPID=Org2MSP
      – CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/fabric/msp
      – CORE_PEER_TLS_ENABLED=true
      – CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
      – CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
      – CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
      – CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      – CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb0.org2.fjordtrade.com:5984
      – CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=fjordtradeadmin
      – CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=fjordtrade_couchdb_pw
      – CORE_OPERATIONS_LISTENADDRESS=0.0.0.0:9448
      – CORE_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: peer node start
    volumes:
      – ../crypto-config/peerOrganizations/org2.fjordtrade.com/peers/peer0.org2.fjordtrade.com/msp:/etc/hyperledger/fabric/msp
      – ../crypto-config/peerOrganizations/org2.fjordtrade.com/peers/peer0.org2.fjordtrade.com/tls:/etc/hyperledger/fabric/tls
      – peer0_org2_data:/var/hyperledger/production
      – /var/run/docker.sock:/var/run/docker.sock
    ports:
      – “9051:7051”
    depends_on:
      – couchdb0.org2.fjordtrade.com
    networks:
      – fjordtrade_network

  peer1.org2.fjordtrade.com:
    container_name: peer1.org2.fjordtrade.com
    image: hyperledger/fabric-peer:${IMAGE_TAG}
    environment:
      – FABRIC_LOGGING_SPEC=INFO
      – CORE_PEER_ID=peer1.org2.fjordtrade.com
      – CORE_PEER_ADDRESS=peer1.org2.fjordtrade.com:10051
      – CORE_PEER_LISTENADDRESS=0.0.0.0:7051
      – CORE_PEER_CHAINCODEADDRESS=peer1.org2.fjordtrade.com:7052
      – CORE_PEER_CHAINCODELISTENADDRESS=0.0.0.0:7052
      – CORE_PEER_GOSSIP_BOOTSTRAP=peer0.org2.fjordtrade.com:9051
      – CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org2.fjordtrade.com:10051
      – CORE_PEER_LOCALMSPID=Org2MSP
      – CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/fabric/msp
      – CORE_PEER_TLS_ENABLED=true
      – CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
      – CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
      – CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt
      – CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      – CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb1.org2.fjordtrade.com:5984
      – CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=fjordtradeadmin
      – CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=fjordtrade_couchdb_pw
      – CORE_OPERATIONS_LISTENADDRESS=0.0.0.0:9449
      – CORE_METRICS_PROVIDER=prometheus
    working_dir: /root
    command: peer node start
    volumes:
      – ../crypto-config/peerOrganizations/org2.fjordtrade.com/peers/peer1.org2.fjordtrade.com/msp:/etc/hyperledger/fabric/msp
      – ../crypto-config/peerOrganizations/org2.fjordtrade.com/peers/peer1.org2.fjordtrade.com/tls:/etc/hyperledger/fabric/tls
      – peer1_org2_data:/var/hyperledger/production
      – /var/run/docker.sock:/var/run/docker.sock
    ports:
      – “10051:7051”
    depends_on:
      – couchdb1.org2.fjordtrade.com
    networks:
      – fjordtrade_network

Press Esc, type :wq, press Enter to save and exit.

The structure mirrors Org1 exactly. The differences are the MSP identity (Org2MSP), the crypto material paths (under org2.fjordtrade.com), and the port allocations (9051/10051 for peers, 7984/8984 for CouchDB). This consistency makes it straightforward to add additional organizations later.

Writing the CA Compose File

The certificate authority compose file defines CA servers for both organizations. These CAs can issue new identities at runtime, which is useful for registering additional users or renewing certificates without regenerating the entire crypto material tree.

Create the CA compose file
$ vim ~/fjordtrade-network/docker/docker-compose-ca.yaml
docker/docker-compose-ca.yaml
version: ‘3.7’

networks:
  fjordtrade_network:
    external: true

services:
  ca.org1.fjordtrade.com:
    container_name: ca.org1.fjordtrade.com
    image: hyperledger/fabric-ca:${CA_IMAGE_TAG}
    environment:
      – FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
      – FABRIC_CA_SERVER_CA_NAME=ca-org1
      – FABRIC_CA_SERVER_TLS_ENABLED=true
      – FABRIC_CA_SERVER_TLS_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org1.fjordtrade.com-cert.pem
      – FABRIC_CA_SERVER_TLS_KEYFILE=/etc/hyperledger/fabric-ca-server-config/priv_sk
      – FABRIC_CA_SERVER_PORT=7054
    command: sh -c ‘fabric-ca-server start -b admin:adminpw -d’
    volumes:
      – ../crypto-config/peerOrganizations/org1.fjordtrade.com/ca/:/etc/hyperledger/fabric-ca-server-config
    ports:
      – “7054:7054”
    networks:
      – fjordtrade_network

  ca.org2.fjordtrade.com:
    container_name: ca.org2.fjordtrade.com
    image: hyperledger/fabric-ca:${CA_IMAGE_TAG}
    environment:
      – FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
      – FABRIC_CA_SERVER_CA_NAME=ca-org2
      – FABRIC_CA_SERVER_TLS_ENABLED=true
      – FABRIC_CA_SERVER_TLS_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org2.fjordtrade.com-cert.pem
      – FABRIC_CA_SERVER_TLS_KEYFILE=/etc/hyperledger/fabric-ca-server-config/priv_sk
      – FABRIC_CA_SERVER_PORT=8054
    command: sh -c ‘fabric-ca-server start -b admin:adminpw -d’
    volumes:
      – ../crypto-config/peerOrganizations/org2.fjordtrade.com/ca/:/etc/hyperledger/fabric-ca-server-config
    ports:
      – “8054:8054”
    networks:
      – fjordtrade_network

Press Esc, type :wq, press Enter to save and exit.

Each CA server mounts the organization’s CA directory, which contains the CA certificate and private key generated by cryptogen. The -b admin:adminpw flag sets the bootstrap admin credentials used for the first enrollment. In a production deployment, these credentials should be changed immediately after initial setup and stored securely.

Launching the Network

The containers must start in a specific order. CouchDB and orderers have no dependencies and can start first. Peers depend on both CouchDB (for state storage) and orderers (for block delivery). The CA services are independent and can start at any time.

Starting the Orderer Cluster

Start the orderer cluster first. The three orderers will form a Raft cluster and elect a leader.

Start the orderer cluster
$ cd ~/fjordtrade-network/docker
$ docker compose -f docker-compose-orderer.yaml up -d
Expected output
[+] Running 4/4
 ✔ Network fjordtrade_network                    Created
 ✔ Container orderer1.orderer.fjordtrade.com     Started
 ✔ Container orderer2.orderer.fjordtrade.com     Started
 ✔ Container orderer3.orderer.fjordtrade.com     Started

The orderer compose file creates the fjordtrade_network Docker bridge network. All subsequent compose files join this existing network.

Verify Raft leader election
$ docker logs orderer1.orderer.fjordtrade.com 2>&1 | grep -i “leader”
Expected output
2026-03-02 11:00:15.234 UTC [orderer.consensus.etcdraft] becomeLeader -> INFO 012 1 became leader at term 2 channel=system-channel

The log entry confirms that orderer1 (node ID 1) was elected leader for the system channel. If you do not see this message within 10 seconds, check that all three orderers are running with docker ps and inspect the logs of each orderer for errors.

Starting the Peer Nodes

Start Org1 and Org2 peer nodes. Each compose file also starts the CouchDB instances that the peers depend on.

Start Org1 peers and CouchDB
$ docker compose -f docker-compose-peer-org1.yaml up -d
Expected output
[+] Running 4/4
 ✔ Container couchdb0.org1.fjordtrade.com     Started
 ✔ Container couchdb1.org1.fjordtrade.com     Started
 ✔ Container peer0.org1.fjordtrade.com        Started
 ✔ Container peer1.org1.fjordtrade.com        Started
Start Org2 peers and CouchDB
$ docker compose -f docker-compose-peer-org2.yaml up -d
Expected output
[+] Running 4/4
 ✔ Container couchdb0.org2.fjordtrade.com     Started
 ✔ Container couchdb1.org2.fjordtrade.com     Started
 ✔ Container peer0.org2.fjordtrade.com        Started
 ✔ Container peer1.org2.fjordtrade.com        Started

Starting the Certificate Authorities

Start CA services
$ docker compose -f docker-compose-ca.yaml up -d
Expected output
[+] Running 2/2
 ✔ Container ca.org1.fjordtrade.com     Started
 ✔ Container ca.org2.fjordtrade.com     Started

Verifying All Containers

With all compose files started, verify that all 13 containers are running and healthy.

List all running containers
$ docker ps –format “table {{.Names}}\t{{.Status}}\t{{.Ports}}” | sort
Expected output
NAMES                                STATUS          PORTS
ca.org1.fjordtrade.com               Up 30 seconds   0.0.0.0:7054->7054/tcp
ca.org2.fjordtrade.com               Up 28 seconds   0.0.0.0:8054->8054/tcp
couchdb0.org1.fjordtrade.com         Up 45 seconds   0.0.0.0:5984->5984/tcp
couchdb0.org2.fjordtrade.com         Up 40 seconds   0.0.0.0:7984->5984/tcp
couchdb1.org1.fjordtrade.com         Up 44 seconds   0.0.0.0:6984->5984/tcp
couchdb1.org2.fjordtrade.com         Up 39 seconds   0.0.0.0:8984->5984/tcp
orderer1.orderer.fjordtrade.com      Up 1 minute     0.0.0.0:7050->7050/tcp, 0.0.0.0:7053->7053/tcp, 0.0.0.0:9443->9443/tcp
orderer2.orderer.fjordtrade.com      Up 1 minute     0.0.0.0:8050->7050/tcp, 0.0.0.0:8053->8053/tcp, 0.0.0.0:9444->9444/tcp
orderer3.orderer.fjordtrade.com      Up 1 minute     0.0.0.0:9050->7050/tcp, 0.0.0.0:9053->9053/tcp, 0.0.0.0:9445->9445/tcp
peer0.org1.fjordtrade.com            Up 43 seconds   0.0.0.0:7051->7051/tcp
peer0.org2.fjordtrade.com            Up 38 seconds   0.0.0.0:9051->7051/tcp
peer1.org1.fjordtrade.com            Up 42 seconds   0.0.0.0:8051->7051/tcp
peer1.org2.fjordtrade.com            Up 37 seconds   0.0.0.0:10051->7051/tcp

All 13 containers should show “Up” status. If any container shows “Restarting” or is missing, inspect its logs to identify the issue.

Count running containers
$ docker ps -q | wc -l
Expected output
13

Verifying Peer Startup

Check peer0-org1 started successfully
$ docker logs peer0.org1.fjordtrade.com 2>&1 | grep “Started peer”
Expected output
2026-03-02 11:00:45.678 UTC [nodeCmd] serve -> INFO 01a Started peer with ID=[peer0.org1.fjordtrade.com], network ID=[dev], address=[peer0.org1.fjordtrade.com:7051]
Verify CouchDB is accessible
$ curl -s http://localhost:5984/ | python3 -m json.tool
Expected output
{
    “couchdb”: “Welcome”,
    “version”: “3.3.3”,
    “git_sha”: “40afbcfc7”,
    “uuid”: “a1b2c3d4e5f6…”,
    “features”: [
        “access-ready”,
        “partitioned”,
        “pluggable-storage-engines”,
        “reshard”,
        “scheduler”
    ],
    “vendor”: {
        “name”: “The Apache Software Foundation”
    }
}

The CouchDB welcome response confirms that the database is running and accessible on port 5984. Each CouchDB instance can be accessed on its respective host port (5984, 6984, 7984, 8984) using the same URL pattern.

Verifying the Docker Network

Inspect the shared Docker network
$ docker network inspect fjordtrade_network –format ‘{{range .Containers}}{{.Name}} {{end}}’
Expected output
orderer1.orderer.fjordtrade.com orderer2.orderer.fjordtrade.com orderer3.orderer.fjordtrade.com peer0.org1.fjordtrade.com peer1.org1.fjordtrade.com peer0.org2.fjordtrade.com peer1.org2.fjordtrade.com couchdb0.org1.fjordtrade.com couchdb1.org1.fjordtrade.com couchdb0.org2.fjordtrade.com couchdb1.org2.fjordtrade.com ca.org1.fjordtrade.com ca.org2.fjordtrade.com

All 13 containers are connected to the fjordtrade_network bridge. This means every container can reach every other container by hostname, which is essential for peer-to-orderer communication, gossip between peers, and peer-to-CouchDB connections.

Troubleshooting

Container Exits Immediately

If a container starts and exits within seconds, the most common cause is incorrect volume mount paths. The MSP or TLS directory does not exist at the path specified in the compose file.

Check logs of a failing container
$ docker logs orderer1.orderer.fjordtrade.com 2>&1 | tail -20

Look for errors mentioning “cannot find” or “no such file or directory”. Verify that the relative paths in the compose file correctly resolve from the docker/ directory to the crypto-config/ and channel-artifacts/ directories.

Port Already in Use

If Docker reports “port is already allocated”, another service is using that port on the host.

Find which process is using a port
$ sudo ss -tlnp | grep :7050

Stop the conflicting process with sudo kill <PID> or change the host port in the compose file to an unused port.

Raft Leader Election Fails

If no orderer becomes leader, the TLS certificates may be incorrect. Each orderer’s TLS certificate must match what is listed in the genesis block’s consenter set. Regenerate the genesis block after any changes to crypto material (as described in the Part 3 troubleshooting section).

CouchDB Connection Refused

If a peer logs “connection refused” for CouchDB, the CouchDB container may not have started yet. Although depends_on ensures the container starts, it does not wait for the application inside to be ready. Restart the peer after CouchDB is fully up.

Restart a peer after CouchDB is ready
$ docker restart peer0.org1.fjordtrade.com

Stopping and Restarting the Network

To stop all containers without deleting data volumes, use the down command on each compose file.

Stop all services (preserving data)
$ cd ~/fjordtrade-network/docker
$ docker compose -f docker-compose-ca.yaml down
$ docker compose -f docker-compose-peer-org2.yaml down
$ docker compose -f docker-compose-peer-org1.yaml down
$ docker compose -f docker-compose-orderer.yaml down

To restart, launch them again in the same order: orderers first, then peers, then CAs. The named volumes persist the ledger data and CouchDB state across restarts.

To completely reset the network and remove all data, add the -v flag to each down command. This deletes the named volumes, requiring you to rejoin channels and reinstall chaincode.

Summary

This part created all Docker Compose configuration files for the FjordTrade blockchain network and launched every container. Here is what was accomplished.

Environment file: Created .env with shared variables for Fabric image tags, CouchDB version, and project naming used across all compose files.

Orderer cluster: Wrote docker-compose-orderer.yaml defining three Raft orderer nodes with TLS enabled, genesis block volume mounts, MSP identity configuration, and unique host port mappings (7050, 8050, 9050). Verified Raft leader election in logs.

Org1 peers: Wrote docker-compose-peer-org1.yaml with two peer nodes (peer0, peer1) and two CouchDB sidecar instances. Configured gossip cross-bootstrap, CouchDB state database integration, Docker socket mounting for chaincode, and TLS settings.

Org2 peers: Wrote docker-compose-peer-org2.yaml mirroring Org1’s structure with Org2-specific hostnames, crypto paths, and port allocations (9051, 10051 for peers; 7984, 8984 for CouchDB).

Certificate authorities: Wrote docker-compose-ca.yaml with CA servers for both organizations, mounting the CA certificates and keys from the crypto material tree.

Network launch: Started all 13 containers in dependency order (orderers first, then org peers with CouchDB, then CAs). Verified that all containers are running, the Raft cluster elected a leader, peers started successfully, CouchDB is accessible, and all containers are connected to the shared Docker bridge network.

What Comes Next

In Part 5: Creating a Channel and Joining Peers to the Network, you will use the running orderer cluster to create the fjordtradechannel application channel using the channel creation transaction generated in Part 3. All four peers will join the channel, anchor peers will be updated for cross-organization gossip discovery, and block synchronization will be verified across both organizations. By the end of Part 5, Org1 and Org2 peers will share a common ledger and be ready for chaincode deployment.