DevNet is the practice and early-upgrade network. It is open to validator operators, but your validator node’s egress IP still must be allowlisted by the Super Validator operators. DevNet is reset periodically, around every 3 months, and receives upgrades before TestNet and MainNet.
This guide uses the official Docker Compose validator deployment. The Docker Compose deployment runs the validator node, participant, wallet UI, and CNS/ANS UI.
Official reference requirements say a minimal experiment VM needs around 1 CPU and 6GB RAM, while a low-activity production validator needs around 2 CPUs and 8GB RAM. The validator and participant containers are both included in these requirements.
Recommended for DevNet:
2 vCPU
8GB RAM preferred
50GB disk
Ubuntu 22.04 or 24.04
Static public egress IP
If using a 4GB RAM VPS, create swap:
fallocate -l 8G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
free -h
Docker Compose deployment requires Linux or macOS, Docker Compose 2.26.0 or newer, curl, and jq.
sudo apt update
sudo apt install -y curl jq tar ca-certificates
curl -fsSL https://get.docker.com | sh
docker compose version
curl --version
jq --version
Optional but recommended for sequencer checks:
sudo apt install -y wget
wget -O /tmp/grpcurl.deb https://github.com/fullstorydev/grpcurl/releases/download/v1.9.3/grpcurl_1.9.3_linux_amd64.deb
sudo apt install -y /tmp/grpcurl.deb
grpcurl --version
NETWORK_NAME="devnet"
INFO_URL="https://docs.dev.global.canton.network.sync.global/info"
SEED_SCAN_URL="https://scan.sv-1.dev.global.canton.network.sync.global"
SPONSOR_SV_URL="https://sv.sv-1.dev.global.canton.network.sync.global"
You can use another DevNet sponsor SV if your sponsor gives you a different SPONSOR_SV_URL. The sponsor URL must start with sv., not scan..
Run this on the same server that will run the validator:
curl -sSL http://checkip.amazonaws.com
Send this IP to your sponsor/SV operator. The official docs say this must be run from the same egress IP that will deploy the validator.
The allowlist usually takes 2–7 days to propagate across SV operators.
Check Scan endpoints:
CURL='curl -fsS -m 5 --connect-timeout 5'
echo "Egress IP:"
curl -sSL http://checkip.amazonaws.com
echo "Checking Scan endpoints..."
for url in $($CURL "$SEED_SCAN_URL/api/scan/v0/scans" | jq -r '.scans[].scans[].publicUrl'); do
echo -n "$url: "
$CURL "$url/api/scan/version" | jq -r '.version' || echo "FAILED"
done
Check Sequencer endpoints:
CURL='curl -fsS -m 5 --connect-timeout 5'
echo "Checking Sequencer endpoints..."
for url in $($CURL "$SEED_SCAN_URL/api/scan/v0/dso-sequencers" | jq -r '.domainSequencers[].sequencers[].url | sub("https://"; "")'); do
echo -n "$url: "
grpcurl --max-time 10 "$url:443" grpc.health.v1.Health/Check || echo "FAILED"
done
Healthy sequencers return:
{
"status": "SERVING"
}
The default validator configuration requires access to at least 2/3 of SVs for Scan and 2/3 of SVs for Sequencers. For example, if there are 14 SVs, the minimum is 10/14.
Practical rule:
Start only when:
Scan OK >= 10/14
Sequencer OK >= 10/14
Better:
Sequencer OK >= 11/14 or 12/14
You can use the below auto script:
cat > /tmp/check_canton_devnet.sh <<'EOF'
#!/usr/bin/env bash
set -u
NETWORK="DevNet"
EXPECTED_IP="88.99.39.235"
SCAN_URLS="
https://scan.sv-1.dev.global.canton.network.c7.digital
https://scan.sv-1.dev.global.canton.network.cumberland.io
https://scan.sv-2.dev.global.canton.network.cumberland.io
https://scan.sv.dev.global.canton.network.digitalasset.com
https://scan.sv-1.dev.global.canton.network.digitalasset.com
https://scan.sv-2.dev.global.canton.network.digitalasset.com
https://scan.sv-1.dev.global.canton.network.fivenorth.io
https://scan.sv-1.dev.global.canton.network.sync.global
https://scan.sv-1.dev.global.canton.network.lcv.mpch.io
https://scan.sv-1.dev.global.canton.network.mpch.io
https://scan.sv-1.dev.global.canton.network.orb1lp.mpch.io
https://scan.sv-1.dev.global.canton.network.proofgroup.xyz
https://scan.sv.dev.global.canton.network.sv-nodeops.com
https://scan.sv-1.dev.global.canton.network.tradeweb.com
"
SEQUENCER_HOSTS="
sequencer-1.sv-1.dev.global.canton.network.c7.digital
sequencer-1.sv-1.dev.global.canton.network.cumberland.io
sequencer-1.sv-2.dev.global.canton.network.cumberland.io
sequencer-1.sv.dev.global.canton.network.digitalasset.com
sequencer-1.sv-1.dev.global.canton.network.digitalasset.com
sequencer-1.sv-2.dev.global.canton.network.digitalasset.com
sequencer-1.sv-1.dev.global.canton.network.fivenorth.io
sequencer-1.sv-1.dev.global.canton.network.sync.global
sequencer-1.sv-1.dev.global.canton.network.lcv.mpch.io
sequencer-1.sv-1.dev.global.canton.network.mpch.io
sequencer-1.sv-1.dev.global.canton.network.orb1lp.mpch.io
sequencer-1.sv-1.dev.global.canton.network.proofgroup.xyz
sequencer-1.sv.dev.global.canton.network.sv-nodeops.com
sequencer-1.sv-1.dev.global.canton.network.tradeweb.com
"
echo "============================================================"
echo "Canton $NETWORK whitelist check"
echo "Expected IP: $EXPECTED_IP"
echo "============================================================"
echo
if ! command -v curl >/dev/null 2>&1; then
echo "ERROR: curl is not installed"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is not installed. Install with: sudo apt update && sudo apt install -y jq"
exit 1
fi
if ! command -v grpcurl >/dev/null 2>&1; then
echo "ERROR: grpcurl is not installed"
exit 1
fi
CURRENT_IP="$(curl -4 -sSL http://checkip.amazonaws.com | tr -d '[:space:]')"
echo "Current egress IP: $CURRENT_IP"
echo
if [ "$CURRENT_IP" != "$EXPECTED_IP" ]; then
echo "ERROR: Current egress IP does not match submitted $NETWORK IP."
echo "You must run this script from the DevNet server/VM using IP $EXPECTED_IP."
exit 1
fi
echo "IP check: OK"
echo
echo "============================================================"
echo "Checking Scan endpoints"
echo "============================================================"
SCAN_OK=0
SCAN_FAIL=0
SCAN_TOTAL=0
for url in $SCAN_URLS; do
SCAN_TOTAL=$((SCAN_TOTAL + 1))
code=$(curl -4 -sS -o /tmp/canton_scan_version.json -w "%{http_code}" \
--connect-timeout 10 --max-time 20 \
"$url/api/scan/version" 2>/tmp/canton_scan_error.txt)
if [ "$code" = "200" ]; then
version=$(jq -r '.version // "no version"' /tmp/canton_scan_version.json 2>/dev/null)
echo "OK $url version=$version"
SCAN_OK=$((SCAN_OK + 1))
else
err=$(cat /tmp/canton_scan_error.txt 2>/dev/null)
if [ -n "$err" ]; then
echo "FAIL $url HTTP=$code error=$err"
else
echo "FAIL $url HTTP=$code"
fi
SCAN_FAIL=$((SCAN_FAIL + 1))
fi
done
echo
echo "Scan OK: $SCAN_OK / $SCAN_TOTAL"
echo "Scan FAIL: $SCAN_FAIL / $SCAN_TOTAL"
echo
echo "============================================================"
echo "Checking Sequencer endpoints"
echo "============================================================"
SEQ_OK=0
SEQ_FAIL=0
SEQ_TOTAL=0
for host in $SEQUENCER_HOSTS; do
SEQ_TOTAL=$((SEQ_TOTAL + 1))
grpcurl --max-time 10 "$host:443" grpc.health.v1.Health/Check > /tmp/canton_seq_result.json 2>/tmp/canton_seq_error.txt
if grep -q '"status": "SERVING"' /tmp/canton_seq_result.json 2>/dev/null; then
echo "OK $host SERVING"
SEQ_OK=$((SEQ_OK + 1))
else
err=$(cat /tmp/canton_seq_error.txt 2>/dev/null)
if [ -n "$err" ]; then
echo "FAIL $host error=$err"
else
echo "FAIL $host"
cat /tmp/canton_seq_result.json 2>/dev/null
fi
SEQ_FAIL=$((SEQ_FAIL + 1))
fi
done
echo
echo "Sequencer OK: $SEQ_OK / $SEQ_TOTAL"
echo "Sequencer FAIL: $SEQ_FAIL / $SEQ_TOTAL"
echo
echo "============================================================"
echo "Summary for $NETWORK"
echo "============================================================"
echo "Expected IP: $EXPECTED_IP"
echo "Current IP: $CURRENT_IP"
echo "Scan OK: $SCAN_OK / $SCAN_TOTAL"
echo "Sequencer OK: $SEQ_OK / $SEQ_TOTAL"
echo
if [ "$SCAN_OK" -ge 9 ] && [ "$SEQ_OK" -ge 9 ]; then
echo "RESULT: Likely enough SV connectivity for default validator config."
else
echo "RESULT: Not enough SV connectivity yet. Wait for allowlist adoption or contact sponsor."
fi
EOF
chmod +x /tmp/check_canton_devnet.sh
/tmp/check_canton_devnet.sh
Docker Compose validator deployment uses .localhost subdomains such as wallet.localhost.
sudo tee -a /etc/hosts >/dev/null <<'EOF'
127.0.0.1 json-ledger-api.localhost
127.0.0.1 grpc-ledger-api.localhost
127.0.0.1 validator.localhost
127.0.0.1 app-provider.localhost
127.0.0.1 participant.localhost
127.0.0.1 wallet.localhost
127.0.0.1 ans.localhost
127.0.0.1 keycloak.localhost
127.0.0.1 host.docker.internal
EOF
Always read the active version and migration ID dynamically. Do not hardcode an old version.
mkdir -p ~/canton-devnet
cd ~/canton-devnet
SPLICE_VERSION="$(curl -fsSL "$INFO_URL" | jq -r '.synchronizer.active.version')"
MIGRATION_ID="$(curl -fsSL "$INFO_URL" | jq -r '.synchronizer.active.migration_id')"
echo "SPLICE_VERSION=$SPLICE_VERSION"
echo "MIGRATION_ID=$MIGRATION_ID"
curl -fL -o "${SPLICE_VERSION}_splice-node.tar.gz" \
"https://github.com/digital-asset/decentralized-canton-sync/releases/download/v${SPLICE_VERSION}/${SPLICE_VERSION}_splice-node.tar.gz"
tar xzf "${SPLICE_VERSION}_splice-node.tar.gz"
cd splice-node/docker-compose/validator
export IMAGE_TAG="$SPLICE_VERSION"
The Docker Compose docs require exporting IMAGE_TAG before running the validator.
DevNet is the only network where you can self-generate an onboarding secret through an API call. This self-served secret is valid for 1 hour.
TMP_SECRET="$(mktemp)"
HTTP_CODE="$(curl -sS -o "$TMP_SECRET" -w "%{http_code}" \
-X POST "$SPONSOR_SV_URL/api/sv/v0/devnet/onboard/validator/prepare")"
if [ "$HTTP_CODE" != "200" ]; then
echo "FAILED to get onboarding secret. HTTP_CODE=$HTTP_CODE"
cat "$TMP_SECRET"
rm -f "$TMP_SECRET"
exit 1
fi
ONBOARDING_SECRET="$(cat "$TMP_SECRET")"
rm -f "$TMP_SECRET"
echo "Secret length: ${#ONBOARDING_SECRET}"
Do not share the secret publicly.
The party hint becomes the prefix of the validator administrator Party ID. It must use this format and cannot be changed later.
PARTY_HINT="yourcompany-validator-1"
Format:
<organization>-<function>-<enumerator>
Example:
PARTY_HINT="johnwiard-validator-1"
cd ~/canton-devnet/splice-node/docker-compose/validator
SPLICE_VERSION="$(curl -fsSL "$INFO_URL" | jq -r '.synchronizer.active.version')"
MIGRATION_ID="$(curl -fsSL "$INFO_URL" | jq -r '.synchronizer.active.migration_id')"
export IMAGE_TAG="$SPLICE_VERSION"
./start.sh \
-s "$SPONSOR_SV_URL" \
-o "$ONBOARDING_SECRET" \
-p "$PARTY_HINT" \
-m "$MIGRATION_ID" \
-w
The official start.sh format is:
./start.sh -s "<SPONSOR_SV_URL>" -o "<ONBOARDING_SECRET>" -p "<party_hint>" -m "<MIGRATION_ID>" -w
docker compose ps
docker inspect splice-validator-participant-1 \
--format 'PARTICIPANT RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} Status={{.State.Status}} Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'
docker inspect splice-validator-validator-1 \
--format 'VALIDATOR RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} Status={{.State.Status}} Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'
Expected:
participant healthy
validator healthy
nginx healthy
wallet UI healthy
ans UI healthy
postgres healthy
If unhealthy:
docker compose logs --tail=200 validator
docker compose logs --tail=200 participant
Common DevNet issues:
403 / PermissionDenied / timeout:
Your egress IP is not fully allowlisted or SV connectivity is unstable.
validator health: starting:
Validator did not finish onboarding.
participant RestartCount increasing:
Participant is unstable, often due to sequencer connectivity or low RAM.
OOMKilled=true or kernel OOM:
Add swap or upgrade RAM.
Secret expired:
Generate a new DevNet onboarding secret.
Do not expose wallet publicly. Use SSH tunnel:
ssh -N -L 8080:127.0.0.1:80 root@YOUR_SERVER_IP
Open:
http://wallet.localhost:8080
Username:
administrator
The official docs say the wallet UI is available at wallet.localhost, and the validator administrator username is administrator.