How to run Hyperledger Fabric chaincode as an external service

Running Hyperledger Fabric chaincode as an external service lets a peer connect to a long-running smart contract process instead of building and launching the chaincode container itself. Operators use this pattern when chaincode must run under a separate runtime, deployment pipeline, or platform such as Kubernetes.

The Fabric external builder reads a lifecycle package that contains endpoint metadata instead of chaincode source. For the asset-transfer sample, metadata.json marks the package as external and connection.json tells the peer how to reach the chaincode server.

The sample path uses fabric-samples, the default mychannel channel, and TLS disabled between the peer and chaincode service. Enable TLS and run one service instance per organization or peer group before using the same pattern outside a disposable test network.

Steps to run Hyperledger Fabric chaincode as an external service:

  1. Change to the external asset-transfer chaincode directory.
    $ cd fabric-samples/asset-transfer-basic/chaincode-external

    The sample connection.json address is asset-transfer-basic.org1.example.com:9999, which must match the server address in chaincode.env.

  2. Check the external chaincode package metadata.
    $ cat metadata.json
    {
        "type": "external",
        "label": "basic_1.0"
    }

    The type value tells the peer external builder to claim the package instead of treating it as normal source chaincode.

  3. Check the peer connection metadata.
    $ cat connection.json
    {
      "address": "asset-transfer-basic.org1.example.com:9999",
      "dial_timeout": "10s",
      "tls_required": false
    }

    TLS is disabled only for the sample. For production, use a TLS-enabled connection.json with the chaincode server certificate and client authentication material expected by the peer.

  4. Create the code archive that carries the peer connection metadata.
    $ tar cfz code.tar.gz connection.json
  5. Create the external chaincode lifecycle package.
    $ tar cfz asset-transfer-basic-external.tgz metadata.json code.tar.gz
  6. List the package contents.
    $ tar tzf asset-transfer-basic-external.tgz
    metadata.json
    code.tar.gz
  7. Change to the Fabric test network directory.
    $ cd ../../test-network
  8. Start the test network and create the channel.
    $ ./network.sh up createChannel -c mychannel -ca
    Creating channel 'mychannel'.
    ##### snipped #####
    Channel 'mychannel' joined

    The test-network peer configuration includes the ccaas_builder external builder. For a custom peer, add an externalBuilders entry under the chaincode section of the peer core.yaml before installing the package.
    Related: How to run the Hyperledger Fabric test network

  9. Add the Fabric binaries and sample config directory to the current shell.
    $ export PATH="$PWD/../bin:$PATH"
    $ export FABRIC_CFG_PATH="$PWD/../config/"
  10. Load the test-network identity helper functions.
    $ . ./scripts/envVar.sh
  11. Set the Org1 admin environment.
    $ setGlobals 1
  12. Install the external chaincode package on the Org1 peer.
    $ peer lifecycle chaincode install ../asset-transfer-basic/chaincode-external/asset-transfer-basic-external.tgz
    2026-06-21 06:44:12.918 UTC [chaincodeCmd] install -> INFO 001 Installed remotely: response:<status:200 payload:"\nJbasic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3\022\tbasic_1.0" >
    2026-06-21 06:44:12.918 UTC [chaincodeCmd] install -> INFO 002 Chaincode code package identifier: basic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3
  13. Set the Org2 admin environment.
    $ setGlobals 2
  14. Install the same external chaincode package on the Org2 peer.
    $ peer lifecycle chaincode install ../asset-transfer-basic/chaincode-external/asset-transfer-basic-external.tgz
    2026-06-21 06:44:39.807 UTC [chaincodeCmd] install -> INFO 001 Installed remotely: response:<status:200 payload:"\nJbasic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3\022\tbasic_1.0" >
    2026-06-21 06:44:39.807 UTC [chaincodeCmd] install -> INFO 002 Chaincode code package identifier: basic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3

    Install the same package on every endorsing peer that should connect to this external service endpoint.

  15. Set the Org1 admin environment again.
    $ setGlobals 1
  16. Query the installed package ID from the Org1 peer.
    $ peer lifecycle chaincode queryinstalled \
      --peerAddresses localhost:7051 \
      --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
    Installed chaincodes on peer:
    Package ID: basic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3, Label: basic_1.0
  17. Save the package ID returned by the peer.
    $ export CHAINCODE_ID=basic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3

    The package ID depends on the package content. Do not reuse an ID from another network or from a rebuilt package.

  18. Set the package ID in the chaincode server environment file.
    chaincode.env
    CHAINCODE_SERVER_ADDRESS=asset-transfer-basic.org1.example.com:9999
    CHAINCODE_ID=basic_1.0:ecfc83f251b7c2d9ef376bc3fc20fc6b9744c0fc0a8923092af6542af94790c3

    The CHAINCODE_ID value must match the installed package ID. The CHAINCODE_SERVER_ADDRESS value must match connection.json.

  19. Return to the external chaincode directory.
    $ cd ../asset-transfer-basic/chaincode-external
  20. Build the external chaincode service image.
    $ docker build -t hyperledger/asset-transfer-basic .
  21. Start the external chaincode service container.
    $ docker run --detach --rm \
      --name asset-transfer-basic.org1.example.com \
      --hostname asset-transfer-basic.org1.example.com \
      --env-file chaincode.env \
      --network=fabric_test \
      hyperledger/asset-transfer-basic
    0a7a63cb01a0d91a2bd38e4eb8b3630d25ca8f4785cf42d63f9bd2ce77988952
  22. Confirm that the chaincode service container is running.
    $ docker ps --filter name=asset-transfer-basic.org1.example.com
    CONTAINER ID   IMAGE                              COMMAND                  CREATED          STATUS          PORTS      NAMES
    0a7a63cb01a0   hyperledger/asset-transfer-basic   "chaincode-external"     4 seconds ago    Up 3 seconds    9999/tcp   asset-transfer-basic.org1.example.com
  23. Return to the test network directory.
    $ cd ../../test-network
  24. Set the Org2 admin environment again.
    $ setGlobals 2
  25. Approve the external chaincode definition for Org2.
    $ peer lifecycle chaincode approveformyorg \
      -o localhost:7050 \
      --ordererTLSHostnameOverride orderer.example.com \
      --tls \
      --cafile "$PWD/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" \
      --channelID mychannel \
      --name basic \
      --version 1.0 \
      --package-id "$CHAINCODE_ID" \
      --sequence 1
    2026-06-21 06:45:48.231 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [a0adf34f0ac82e51f4e7a10f2b354c74897a561638c3bcb415a85d65c3ab33b0] committed with status (VALID) at localhost:9051
  26. Set the Org1 admin environment again.
    $ setGlobals 1
  27. Approve the external chaincode definition for Org1.
    $ peer lifecycle chaincode approveformyorg \
      -o localhost:7050 \
      --ordererTLSHostnameOverride orderer.example.com \
      --tls \
      --cafile "$PWD/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" \
      --channelID mychannel \
      --name basic \
      --version 1.0 \
      --package-id "$CHAINCODE_ID" \
      --sequence 1
    2026-06-21 06:46:09.514 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [bf2f44a54a01e93009c7e733a7b969ab173093fcd3b48d8c21806f5fb0e8bb38] committed with status (VALID) at localhost:7051
  28. Check whether both organizations are ready to commit the definition.
    $ peer lifecycle chaincode checkcommitreadiness \
      --channelID mychannel \
      --name basic \
      --version 1.0 \
      --sequence 1 \
      --tls \
      --cafile "$PWD/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" \
      --output json
    {
            "approvals": {
                    "Org1MSP": true,
                    "Org2MSP": true
            }
    }

    A false approval means at least one organization has not approved this exact name, version, sequence, and package ID.

  29. Commit the external chaincode definition to the channel.
    $ peer lifecycle chaincode commit \
      -o localhost:7050 \
      --ordererTLSHostnameOverride orderer.example.com \
      --tls \
      --cafile "$PWD/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" \
      --channelID mychannel \
      --name basic \
      --peerAddresses localhost:7051 \
      --tlsRootCertFiles "$PWD/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" \
      --peerAddresses localhost:9051 \
      --tlsRootCertFiles "$PWD/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" \
      --version 1.0 \
      --sequence 1
    2026-06-21 06:46:33.119 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [3b8bfb8d8f593c1ec9c1e32b1a90d351cc03ccbd97c2a6bb44e720dc6125dd70] committed with status (VALID) at localhost:7051
    2026-06-21 06:46:33.120 UTC [chaincodeCmd] ClientWait -> INFO 002 txid [3b8bfb8d8f593c1ec9c1e32b1a90d351cc03ccbd97c2a6bb44e720dc6125dd70] committed with status (VALID) at localhost:9051
  30. Query the committed external chaincode definition.
    $ peer lifecycle chaincode querycommitted --channelID mychannel --name basic
    Committed chaincode definition for chaincode 'basic' on channel 'mychannel':
    Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc
  31. Invoke the external chaincode service through the peer.
    $ peer chaincode invoke \
      -o localhost:7050 \
      --ordererTLSHostnameOverride orderer.example.com \
      --tls \
      --cafile "$PWD/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" \
      -C mychannel \
      -n basic \
      --peerAddresses localhost:7051 \
      --tlsRootCertFiles "$PWD/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" \
      --peerAddresses localhost:9051 \
      --tlsRootCertFiles "$PWD/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" \
      -c '{"function":"InitLedger","Args":[]}'
    2026-06-21 06:47:14.322 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

    The successful invoke proves the peers can reach the external chaincode service and commit a transaction through the normal endorsement path.
    Related: How to invoke Hyperledger Fabric chaincode

  32. Query sample ledger state from the external chaincode.
    $ peer chaincode query -C mychannel -n basic -c '{"Args":["GetAllAssets"]}'
    [{"Key":"asset1","Record":{"ID":"asset1","color":"blue","size":5,"owner":"Tomoko","appraisedValue":300}},
    {"Key":"asset2","Record":{"ID":"asset2","color":"red","size":5,"owner":"Brad","appraisedValue":400}},
    ##### snipped #####
    {"Key":"asset6","Record":{"ID":"asset6","color":"white","size":15,"owner":"Michel","appraisedValue":800}}]

    The query confirms that the committed chaincode definition is using the external server to read world state.
    Related: How to query Hyperledger Fabric chaincode