Deploying Hyperledger Fabric chaincode makes a smart contract available on a channel after the channel organizations have agreed to the same lifecycle definition. Operators use this path when a peer has the chaincode package installed, but the channel cannot invoke the contract until the definition is approved and committed.
The Fabric lifecycle separates local peer installation from channel approval. Each organization that must endorse transactions installs the package on its own peers, approves the same chaincode name, version, sequence, and package ID for its MSP, and then one admin commits the agreed definition to the channel.
A lab deployment can use the fabric-samples test network, the JavaScript asset-transfer-basic chaincode, and the default mychannel channel. Replace the channel name, MSP IDs, peer addresses, certificate paths, and package label when deploying a different contract or production network.
$ cd fabric-samples/test-network
The sample network should already be running with a joined channel before the lifecycle commands run. For the default samples, ./network.sh up createChannel creates mychannel with Org1MSP and Org2MSP.
$ export CHANNEL_NAME=mychannel $ export CC_NAME=basic $ export CC_VERSION=1.0 $ export CC_SEQUENCE=1 $ export CC_LABEL=basic_1.0
Increase CC_SEQUENCE when upgrading an existing chaincode definition. Keep the same values for every organization that approves the definition.
$ export PATH="$PWD/../bin:$PATH" $ export FABRIC_CFG_PATH="$PWD/../config"
$ npm --prefix ../asset-transfer-basic/chaincode-javascript install added 360 packages, and audited 361 packages in 6s ##### snipped #####
Use the matching dependency command for the chaincode language being packaged. The Fabric sample JavaScript contract expects its dependencies under asset-transfer-basic/chaincode-javascript before packaging.
$ peer lifecycle chaincode package basic.tar.gz \ --path ../asset-transfer-basic/chaincode-javascript/ \ --lang node \ --label "$CC_LABEL"
The package command writes basic.tar.gz in the current directory. The label becomes part of the package ID returned after installation.
$ export CORE_PEER_TLS_ENABLED=true $ export CORE_PEER_LOCALMSPID=Org1MSP $ export CORE_PEER_MSPCONFIGPATH="$PWD/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" $ export CORE_PEER_TLS_ROOTCERT_FILE="$PWD/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" $ export CORE_PEER_ADDRESS=localhost:7051 $ export ORDERER_CA="$PWD/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" $ export ORG1_TLS_ROOTCERT="$PWD/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" $ export ORG2_TLS_ROOTCERT="$PWD/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"
The active CORE_PEER_MSPCONFIGPATH must point to an admin MSP for the organization approving or installing the package.
$ peer lifecycle chaincode install basic.tar.gz 2026-06-21 05:45:21.104 UTC [chaincodeCmd] install -> INFO 001 Installed remotely: response:<status:200 payload:"\nJbasic_1.0:8f65c6a2d4b1d8e6a4f9c1e0a77b5ed22a9569e8a1a4cf2d5bf2d1a29128a928\022\tbasic_1.0" > 2026-06-21 05:45:21.104 UTC [chaincodeCmd] install -> INFO 002 Chaincode code package identifier: basic_1.0:8f65c6a2d4b1d8e6a4f9c1e0a77b5ed22a9569e8a1a4cf2d5bf2d1a29128a928
$ peer lifecycle chaincode queryinstalled Installed chaincodes on peer: Package ID: basic_1.0:8f65c6a2d4b1d8e6a4f9c1e0a77b5ed22a9569e8a1a4cf2d5bf2d1a29128a928, Label: basic_1.0
$ export CC_PACKAGE_ID=basic_1.0:8f65c6a2d4b1d8e6a4f9c1e0a77b5ed22a9569e8a1a4cf2d5bf2d1a29128a928
Do not copy a package ID from another environment. The hash changes when the chaincode package content changes.
$ export CORE_PEER_LOCALMSPID=Org2MSP $ export CORE_PEER_MSPCONFIGPATH="$PWD/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp" $ export CORE_PEER_TLS_ROOTCERT_FILE="$ORG2_TLS_ROOTCERT" $ export CORE_PEER_ADDRESS=localhost:9051
$ peer lifecycle chaincode install basic.tar.gz 2026-06-21 05:46:03.331 UTC [chaincodeCmd] install -> INFO 001 Installed remotely: response:<status:200 payload:"\nJbasic_1.0:8f65c6a2d4b1d8e6a4f9c1e0a77b5ed22a9569e8a1a4cf2d5bf2d1a29128a928\022\tbasic_1.0" > 2026-06-21 05:46:03.331 UTC [chaincodeCmd] install -> INFO 002 Chaincode code package identifier: basic_1.0:8f65c6a2d4b1d8e6a4f9c1e0a77b5ed22a9569e8a1a4cf2d5bf2d1a29128a928
Install the package on every peer that will endorse transactions for the chaincode. Peers that only commit blocks do not need the package.
$ peer lifecycle chaincode approveformyorg \ -o localhost:7050 \ --ordererTLSHostnameOverride orderer.example.com \ --channelID "$CHANNEL_NAME" \ --name "$CC_NAME" \ --version "$CC_VERSION" \ --package-id "$CC_PACKAGE_ID" \ --sequence "$CC_SEQUENCE" \ --tls \ --cafile "$ORDERER_CA" 2026-06-21 05:46:45.640 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [4a7350d6f6a3d1880d507bd8a9df6d39a28e9e8df1f71064a4c53b8719d90027] committed with status (VALID) at localhost:9051
$ export CORE_PEER_LOCALMSPID=Org1MSP $ export CORE_PEER_MSPCONFIGPATH="$PWD/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" $ export CORE_PEER_TLS_ROOTCERT_FILE="$ORG1_TLS_ROOTCERT" $ export CORE_PEER_ADDRESS=localhost:7051
$ peer lifecycle chaincode approveformyorg \ -o localhost:7050 \ --ordererTLSHostnameOverride orderer.example.com \ --channelID "$CHANNEL_NAME" \ --name "$CC_NAME" \ --version "$CC_VERSION" \ --package-id "$CC_PACKAGE_ID" \ --sequence "$CC_SEQUENCE" \ --tls \ --cafile "$ORDERER_CA" 2026-06-21 05:47:08.215 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [a8e5db49105640355d58fb5442aa6e3af3247ab27f9dbd1f0ac31f48049f3e3f] committed with status (VALID) at localhost:7051
$ peer lifecycle chaincode checkcommitreadiness \ --channelID "$CHANNEL_NAME" \ --name "$CC_NAME" \ --version "$CC_VERSION" \ --sequence "$CC_SEQUENCE" \ --tls \ --cafile "$ORDERER_CA" \ --output json { "approvals": { "Org1MSP": true, "Org2MSP": true } }
A false approval means at least one organization approved a different lifecycle definition or has not approved it yet. Fix the mismatch before committing.
$ peer lifecycle chaincode commit \ -o localhost:7050 \ --ordererTLSHostnameOverride orderer.example.com \ --channelID "$CHANNEL_NAME" \ --name "$CC_NAME" \ --version "$CC_VERSION" \ --sequence "$CC_SEQUENCE" \ --tls \ --cafile "$ORDERER_CA" \ --peerAddresses localhost:7051 \ --tlsRootCertFiles "$ORG1_TLS_ROOTCERT" \ --peerAddresses localhost:9051 \ --tlsRootCertFiles "$ORG2_TLS_ROOTCERT" 2026-06-21 05:47:32.592 UTC [chaincodeCmd] ClientWait -> INFO 001 txid [5d214f15c7d1fc0aa32d383c9acebd3d0ad538c6bfbf207333dc07661209ef7a] committed with status (VALID) at localhost:7051 2026-06-21 05:47:32.593 UTC [chaincodeCmd] ClientWait -> INFO 002 txid [5d214f15c7d1fc0aa32d383c9acebd3d0ad538c6bfbf207333dc07661209ef7a] committed with status (VALID) at localhost:9051
$ peer lifecycle chaincode querycommitted --channelID "$CHANNEL_NAME" --name "$CC_NAME" Committed chaincode definition for chaincode 'basic' on channel 'mychannel': Version: 1.0, Sequence: 1, Endorsement Plugin: escc, Validation Plugin: vscc
$ peer chaincode invoke \ -o localhost:7050 \ --ordererTLSHostnameOverride orderer.example.com \ --tls \ --cafile "$ORDERER_CA" \ -C "$CHANNEL_NAME" \ -n "$CC_NAME" \ --peerAddresses localhost:7051 \ --tlsRootCertFiles "$ORG1_TLS_ROOTCERT" \ --peerAddresses localhost:9051 \ --tlsRootCertFiles "$ORG2_TLS_ROOTCERT" \ -c '{"function":"InitLedger","Args":[]}' 2026-06-21 05:48:19.706 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
The asset-transfer sample does not require the lifecycle --init-required flag. The invoke is a normal transaction that proves the committed definition can start the chaincode container and write ledger state.
Related: How to invoke Hyperledger Fabric chaincode
$ peer chaincode query -C "$CHANNEL_NAME" -n "$CC_NAME" -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 can read the world state after the initialization transaction.
Related: How to query Hyperledger Fabric chaincode