ClusterInfrastructureFull-chain live test:Bring up the stack (3-node cluster + Caddy) → upload via the cluster proxy → automatic multi-replica across three nodes → gateway rendering → /artifact friendly path → still readable after stopping a node → tear down. All with real curl / ipfs-cluster-ctl assertions, not relying on "it should work".
This isdeployment e2e— verifying "whether the cluster itself is correct". Agent publish pipeline(
:9097token write entry +skills/publish-artifact/publish.sh+ default expiry) for an independent e2e see kubo-publish-e2e skill /make publish-e2e。
This project no longer has v1 single-node; it is unified on Cluster; "single-node" = a cluster running 1 node (see
docs/MULTI_HOST_DEPLOYMENT.md§3.4). This runbook targets thesingle-host three-containertrial form (docker-compose.cluster.yml)。
Conventions and prerequisites
REPO=$(git rev-parse --show-toplevel)
COMPOSE="docker compose -f docker-compose.cluster.yml" # 单机 3 节点 + Caddy
PROXY=http://localhost:9095 # cluster IPFS 代理 —— Agent 上传打这里
GW=http://localhost:8080 # ipfs0 原生网关 —— /ipfs/<CID>
ART=http://localhost:8088 # Caddy 反代 —— /artifact/<CID>(重写到 /ipfs)
CTL="docker exec cl-cluster0 ipfs-cluster-ctl" # 集群管理
- Image version pinning:
kubo v0.42.0/ipfs-cluster v1.1.6/caddy:2-alpine。 - Secrets:
.env(CLUSTER_SECRET) andruntime/private/swarm.key(private network), both gitignored. If missing, the script will stop you. - Runtime data is in
runtime/(entirely gitignored), survives restarts; onlydown -vclears it. - Docs: for deployment see
docs/SINGLE_HOST_DEPLOYMENT.md, for onboarding seedocs/AGENT_INTEGRATION_GUIDE.md, for boundaries/ops seedocs/CAPABILITIES_AND_OPERATIONS.md。
0. Bring up the stack
⚠️ Must check port occupancy before bringing up the stack:
8080/9094/9095/8088If it is occupied by another local container (e.g. a previous single-nodekubo-poc, or another service),upwill reportport is already allocated. Firstdocker psto take a look, and stop any conflicts first.
cd "$REPO"
make secrets # 幂等生成 .env + swarm.key(已存在则跳过)
$COMPOSE up -d # 起 3×(kubo+cluster) + caddy
# 等 cluster 代理就绪(最多 ~150s;首次要拉镜像会更久)
for i in $(seq 1 150); do curl -fsS -X POST "$PROXY/api/v0/version" >/dev/null 2>&1 && { echo "proxy ready"; break; }; sleep 1; done
# 等三 peer 成形(应为 3)
for i in $(seq 1 60); do n=$($CTL --enc=json peers ls 2>/dev/null | grep -o '"peername"' | wc -l | tr -d ' '); [ "${n:-0}" -ge 3 ] && break; sleep 2; done
$CTL peers ls # 直观确认 cluster0/1/2 都在
1. One-click e2e (recommended)
Coverage: ① three peers formed ② upload via :9095 ③ replicas=3 ④ gateway rendering ⑤ /artifact path ⑥ still readable after stopping one node.
make e2e # = ./e2e/run-cluster.sh(跑完自动 down)
make e2e-keep # = ./e2e/run-cluster.sh --keep(保留集群继续手测)
Non-zero exit code means failure; at the end it prints PASS=N FAIL=M, and generates a self-contained HTML report runtime/e2e/<时间戳>/report.html(can be sent to people/archived directly). On failure, do not tear down the stack; follow the troubleshooting commands in §3 to check the logs.
2. Manual step-by-step (for demo / troubleshooting)
# ② 上传一个 HTML(务必走 :9095 cluster 代理,pin 才会全集群生效)
echo '<!doctype html><meta charset=utf-8><h1>HELLO_CLUSTER</h1>' > /tmp/demo.html
CID=$(curl -fsS -F "file=@/tmp/demo.html" "$PROXY/api/v0/add?cid-version=1&pin=true" \
| grep -o '"Hash":"[^"]*"' | sed 's/.*:"//;s/"//')
echo "CID=$CID"
# ③ 副本分布:应三节点 PINNED
$CTL status "$CID"
# ④ 原生网关渲染:200 + text/html + 正文
curl -fsSI "$GW/ipfs/$CID" | grep -iE 'HTTP/|content-type'
curl -fsS "$GW/ipfs/$CID"
# ⑤ /artifact 友好路径(经 Caddy):等价但前缀友好
curl -fsSI "$ART/artifact/$CID" | grep -iE 'HTTP/|content-type'
curl -fsS "$ART/artifact/$CID"
# ⑥ 故障容忍:停一个 kubo,仍能从其余节点读
docker stop cl-ipfs1
curl -s -o /dev/null -w 'after stop: %{http_code}\n' "$GW/ipfs/$CID" # 仍 200
docker start cl-ipfs1
Browser demo: directly open
http://localhost:8088/artifact/<CID>. Directory site (relative assets): usewrap-with-directory=trueto upload, visit/artifact/<dirCID>/index.html, relative assets are resolved as well. Seedocs/AGENT_INTEGRATION_GUIDE.md。
3. Troubleshooting
$COMPOSE ps # 容器状态(kubo 应 healthy)
docker logs cl-ipfs0 2>&1 | tail -30
docker logs cl-cluster0 2>&1 | tail -30
docker exec cl-ipfs0 ipfs swarm peers # kubo 私有 mesh 互联数(应 >=2)
$CTL peers ls
docker exec cl-caddy caddy validate --config /etc/caddy/Caddyfile
4. Tear down
$COMPOSE down # 停容器,数据保留在 runtime/
# $COMPOSE down -v # 连卷一起清(runtime/ 是 bind mount,需另删目录)
# rm -rf runtime/cluster # 彻底重置集群身份/数据(下次全新起)
Pitfalls encountered (must know)
- Private network must disable AutoConf: Once Kubo v0.42 detects
swarm.key, it will refuse to use the default mainnet autoconf URL, and the daemon crashes and restarts immediately on startup。scripts/init-cluster.d/001-config.shalready hasipfs config --json AutoConf.Enabled false。 - cluster proxy 502 =
ipfsproxy's node address not configured correctly:ipfsproxyThe component has aindependent ofipfshttp'snode_multiaddress, configuring onlyCLUSTER_IPFSHTTP_NODEMULTIADDRESSis not enough, the proxy still dials the default127.0.0.1:5001→ 502. Both must point to the local kubo (in composeCLUSTER_IPFSPROXY_NODEMULTIADDRESSis already configured). - Don't rely on mDNS discovery in a private network: The official example's kubo connects to the public network and relies on the public DHT for mutual discovery; we
swarm.keyThe private network cuts off this path, and mDNS multicast is unreliable on Docker bridge networks. This project switches to adeterministic approach: a seed nodeipfs0+ the remaining nodes bootstrap to its PeerID (scripts/init-cluster.ddone automatically). Kubo nodes must be interconnected for replicas to actually replicate. - Uploads must go through :9095, not some kubo's :5001: content uploaded by directly connecting to 5001 is cluster unaware and not replicated。
- Port occupation: this project's early single-node
kubo-pococcupies8080/5001; if it or another container is running before starting the cluster, it willport is already allocated. First usedocker psto investigate and stop it. - e2e script
set -euo pipefail+grep:grepreturning non-zero with no match will cause the script to exit as well—counting-type pipelines need|| trueas a fallback. - A bash variable immediately followed by a Chinese full-width character:
"$pinned(…"will merge the multibyte first byte into the variable name and triggerset -u"unbound variable"; variable names must use curly braces"${pinned}(…"。 /ipfsThe namespace cannot be renamed: the Kubo gateway is fixed at/ipfs、/ipns, there is no configuration option to change it to/artifact. A friendly prefix is achieved via Caddy reverse proxyuri replace /artifact/ /ipfs/(caddy/Caddyfile), and this layer will also carry LB/TLS/auth/short links in the future.- Replica recovery vs backup: multiple replicas only guard against hardware failure; deleting a pin will be synchronously deleted across the whole cluster,does not guard against accidental deletion—backups are still needed
runtime/。
Related:kubo-publish-e2e(publishing pipeline e2e) · docs/SINGLE_HOST_DEPLOYMENT.md · docs/MULTI_HOST_DEPLOYMENT.md · docs/CAPABILITIES_AND_OPERATIONS.md · docs/AGENT_INTEGRATION_GUIDE.md