Canton LocalNet Guide
Canton LocalNet Guide

Get your Daml project running on Canton Network LocalNet in your system.

Canton Builder Tool or by using the cn-quickstart Stack.

Not sure where to start?

"I have a .dar built from my Daml contracts with dpm, and I need a Canton LocalNet right now to deploy and demo my project."

Canton Builder Tool →

"I want to understand LocalNet properly and host a sample backend, frontend, Keycloak, using PQS to index, etc."

LocalNet Deployment Guide →

"I want a already built Canton dApp first with backend, auth, frontend on LocalNet to actually learn the stack."

cn-quickstart ↗
Part A

Canton Builder Tool

One command to run and deploy on a Canton Network LocalNet in your system. Built for anyone who needs a local Canton Network without waiting for DevNet whitelisting giving three validators, wallet UIs, Canton Coin, Scan UI, the whole official Splice LocalNet stack.

Install

macOS / Linux (WSL 2 on Windows):

curl -fsSL https://raw.githubusercontent.com/canton-network-devs/Canton-Builder-Tool/main/install.sh | bash

Then reload your shell:

source ~/.zshrc    # zsh
source ~/.bashrc   # bash

Requirements

  • Docker Desktop with ≥ 8 GB memory allocated
  • curl, jq, git via brew (macOS) or apt (Linux)
  • macOS or Linux only. Windows: use WSL 2.

The installer handles PATH setup and /etc/hosts entries for *.localhost domains.

Commands

canton builder start                        # download bundle + boot LocalNet
canton builder stop                         # stop containers (data preserved)
canton builder status                       # health check + port reference
canton builder deploy ./my-app-0.0.1.dar    # upload your DAR to both participants
canton builder logs                         # tail all logs
canton builder logs <service>               # tail one service
canton builder reset                        # wipe everything, start clean

What starts

ServiceURLCredential
App User Wallet UIwallet.localhost:2000app-user
App Provider Wallet UIwallet.localhost:3000app-provider
Scan UIscan.localhost:4000
SV UIsv.localhost:4000sv
App Provider JSON APIlocalhost:3975
App User JSON APIlocalhost:2975
SV JSON APIlocalhost:4975
App Provider Ledger API (gRPC)localhost:3901
App User Ledger API (gRPC)localhost:2901
SV Ledger API (gRPC)localhost:4901
PostgreSQLlocalhost:5432

First run

On canton builder start, the tool:

  1. Downloads the official Splice LocalNet bundle from the Splice GitHub release, cached at ~/.canton-builder/bundle/
  2. Pulls the Canton/Splice Docker images (~5 min, also cached)
  3. Boots the full network using the official LocalNet compose configuration

Subsequent runs skip steps 1 and 2 entirely and boot in ~30 seconds.

Deploying your DAR

Build your Daml project with dpm build, then:

canton builder deploy ./your-project/.daml/dist/your-project-0.0.1.dar

Uploads your DAR to both the App Provider and App User participants, retrieves your package ID, and prints the template ID format for API calls. Once deployed, use the JSON Ledger API to create contracts, exercise choices, and query state.

What it is / isn't

Is: a CLI wrapping the official Splice LocalNet: the same Docker Compose configuration Ships with every Splice release, invoked with the exact commands from the official docs. No custom compose files, no approximations.

Isn't: a replacement for cn-quickstart. Quickstart is a full developer project template with a reference app, Java backend, and React frontend. This tool is just the network layer to bring your own Daml project.

Troubleshooting

First run is slow

Normal: the Splice bundle and Docker images download on first run. Everything is cached after that.

Containers crash on startup

Docker memory. Docker Desktop → Settings → Resources → Memory → set to 8 GB minimum.

*.localhost domains don't resolve

echo "127.0.0.1  wallet.localhost scan.localhost sv.localhost" | sudo tee -a /etc/hosts

Weird state / things not working

canton builder reset
canton builder start

See what's failing

canton builder logs
canton builder logs canton
canton builder logs splice

Re-download the bundle (if corrupted or upgrading)

rm -rf ~/.canton-builder/bundle
canton builder start

Upgrading LocalNet version

Edit ~/.canton-builder/.env and change IMAGE_TAG:

IMAGE_TAG=0.5.11

Then reset and restart:

canton builder reset
rm -rf ~/.canton-builder/bundle
canton builder start
Want the full picture instead?
Part B

Deploy your Daml project on Canton LocalNet

You showed up at a hackathon (or a bootcamp). You've written some Daml, built your .dar, and now you're staring at the Canton docs wondering where to even begin. This is the walkthrough that fills that gap of real validators, a real synchronizer, real wallets, no DevNet whitelisting, no waiting. macOS and Linux only; Windows users can use WSL 2.

What you're actually running

Canton LocalNet is a full Canton Network running in Docker. It's built into the cn-quickstart repository from Digital Asset, which pairs LocalNet with a reference licensing app you'll partially replace with your own project.

RoleWhat it actually isWhy you care
SynchronizerSequences and orders transactionsAlready running inside LocalNet
ValidatorRuns a participant node + walletLocalNet gives you three of them
PartyA named identity on the ledger like an addressYour Daml contracts sit between parties

LocalNet gives you three validators: app-provider, app-user, and sv (Super Validator). Treat app-provider as your app or company and app-user as a customer; sv runs the infrastructure. For most hackathon projects you'll deploy your DAR to app-provider and have parties on both app-provider and app-user interact as a multi-party workflow.

About Keycloak: it's the authentication/OAuth2 layer in the stack. Enable it during setup (make setup will ask). It's required for the wallet UI and Canton Coin transfers. Just say yes and move on.

Prerequisites checklist

  • Docker Desktop installed and running (docker info returns something, not an error)
  • Docker Desktop has at least 8 GB of memory allocated (Settings → Resources → Memory)
  • Git installed
  • dpm installed and in your PATH as you need it to build your project
  • Your Daml project has a daml.yaml and source files ready
  • Docker Hub account, and you're logged in via docker login
Memory is not optional. Under 8 GB and containers crash mid-startup, what looks like a mystery bug is usually just an OOM kill. Run dpm version first; if it errors, your dpm bin isn't on PATH.

1Clone cn-quickstart

git clone https://github.com/digital-asset/cn-quickstart.git
cd cn-quickstart

Now move into the quickstart directory, this is where you'll live for the rest of the guide:

cd quickstart
Almost everything from here runs from inside quickstart/. The Makefile lives here — if a make command errors with "no Makefile found," you're in the wrong directory.

2Configure LocalNet

make setup

Answer the prompts exactly like this:

PromptAnswer
Enable Observability?n
Enable OAuth2?y
Party hintjust press Enter
Enable TEST MODE?n

This writes a .env.local file. You can re-run make setup any time you need to change these.

3Add your Daml project to the quickstart

This is the part nobody explains. Here's how your contracts actually get into LocalNet.

3a · Where to put your Daml files

The quickstart's Daml code lives in quickstart/daml/licensing/: the reference licensing app. You have two options.

Option A: add alongside the existing app (recommended for hackathons). Create a new directory for your project inside daml/:

quickstart/
  daml/
    licensing/          ← the existing reference app, leave this alone
    your-project/       ← create this
      daml/
        YourModule.daml
      daml.yaml

You keep the reference app intact for reference and add your own project as a separate package.

Option B: replace the existing app. Delete the contents of daml/licensing/ and drop your project files there, updating paths in the steps below accordingly. Only do this if you're confident you won't need the reference app.

3b · Set up your daml.yaml

If you built your project with dpm, you already have a daml.yaml just make sure its SDK version matches the quickstart's.

cat .env | grep DAML_SDK_VERSION
# daml.yaml
sdk-version: 3.x.x   # should match what you just found

If they don't match, update your daml.yaml to the quickstart's SDK version.

3c · Register your package in multi-package.yaml

The quickstart builds all Daml packages together via quickstart/daml/multi-package.yaml. Add your project:

packages:
  - licensing       # existing reference app
  - your-project    # add this line (relative to daml/)

3d · Build everything

make build

Compiles all Daml packages, generates Java bindings for the backend, and builds the frontend. Your compiled DAR lands at:

quickstart/daml/your-project/.daml/dist/your-project-<version>.dar
Build errors? 99% of the time it's an SDK version mismatch or a missing dependency in your daml.yaml. If your project depends on Splice DARs (Canton Coin interfaces etc.), check daml/dars/: the quickstart bundles them there.

4Start LocalNet

In a second terminal, start log collection and keep it running the whole time:

cd quickstart
make capture-logs

Back in your first terminal:

make start

This spins up the whole Canton Network locally around five minutes the first time. Once the flood of log output settles, you're up.

5Upload your DAR to LocalNet

LocalNet is running, but your contracts aren't on it yet, you need to upload your DAR to the participant nodes that will use it.

5a · Get an admin token

The JSON API needs an auth token. Grab the App Provider's:

export PROVIDER_ADMIN_TOKEN=$(curl -fsS \
  "http://keycloak.localhost:8082/realms/AppProvider/protocol/openid-connect/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'client_id=app-provider-validator' \
  -d 'client_secret=6m12QyyGl81d9nABWQXMycZdXho6ejEX' \
  -d 'grant_type=client_credentials' \
  -d 'scope=openid' | jq -r .access_token)

echo $PROVIDER_ADMIN_TOKEN   # should print a long JWT string, not empty
Token expired? Just re-run the command for a fresh one. A "Cannot iterate over null" error from jq usually means Keycloak is still starting wait a minute and retry.

5b · Upload your DAR

# Replace the path with your actual DAR file path
curl -X POST http://localhost:3975/v2/packages \
  -H "Authorization: Bearer $PROVIDER_ADMIN_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @./daml/your-project/.daml/dist/your-project-0.0.1.dar

A {} response means success — your contracts are deployed on the App Provider's participant node.

If your contracts involve multiple parties across both App Provider and App User, upload to the App User node too:

export USER_ADMIN_TOKEN=$(curl -fsS \
  "http://keycloak.localhost:8082/realms/AppUser/protocol/openid-connect/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'client_id=app-user-validator' \
  -d 'client_secret=6m12QyyGl81d9nABWQXMycZdXho6ejEX' \
  -d 'grant_type=client_credentials' \
  -d 'scope=openid' | jq -r .access_token)

curl -X POST http://localhost:2975/v2/packages \
  -H "Authorization: Bearer $USER_ADMIN_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @./daml/your-project/.daml/dist/your-project-0.0.1.dar

5c · Get your package ID

You'll need this to create contracts via the API:

dpm damlc inspect-dar ./daml/your-project/.daml/dist/your-project-0.0.1.dar

Find the line with your project name and no -dalf extension, it looks like your-project-0.0.1-<64-char-hex-string>. That hex string is your package ID:

export PACKAGE_ID=$(dpm damlc inspect-dar ./daml/your-project/.daml/dist/your-project-0.0.1.dar \
  | grep "your-project-0.0.1-" | grep -v "dalf" | tail -1 | awk '{print $2}' | tr -d '"')
echo $PACKAGE_ID

6Discover your party IDs

Parties in Canton have long identifier strings, not just names. Grab them:

# App Provider party
APP_PROVIDER_PARTY=$(curl -s -H "Authorization: Bearer $PROVIDER_ADMIN_TOKEN" \
  http://localhost:3975/v2/parties | \
  jq -r '.partyDetails[] | select(.party | startswith("app_provider_quickstart-")) | .party')
echo "Provider: $APP_PROVIDER_PARTY"
# App User party
APP_USER_PARTY=$(curl -s -H "Authorization: Bearer $USER_ADMIN_TOKEN" \
  http://localhost:2975/v2/parties | \
  jq -r '.partyDetails[] | select(.party | startswith("app_user_quickstart-")) | .party')
echo "User: $APP_USER_PARTY"

You'll get back something like app_provider_quickstart-1::122045abc... — that full string is the party identifier, used everywhere a party is needed in API calls.

7Create a contract

Now the good part of creating a contract on LocalNet via the JSON Ledger API. This example assumes a template like:

template MyContract
  with
    provider : Party
    user : Party
    someData : Text
  where
    signatory provider
    observer user

Create it:

curl -X POST http://localhost:3975/v2/commands/submit-and-wait \
  -H "Authorization: Bearer $PROVIDER_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"commands\": [{
      \"CreateCommand\": {
        \"template_id\": \"${PACKAGE_ID}:YourModule:MyContract\",
        \"create_arguments\": {
          \"provider\": \"$APP_PROVIDER_PARTY\",
          \"user\": \"$APP_USER_PARTY\",
          \"someData\": \"hello from hackathon\"
        }
      }
    }],
    \"act_as\": [\"$APP_PROVIDER_PARTY\"],
    \"read_as\": [\"$APP_PROVIDER_PARTY\"]
  }"

A response containing a transaction_id means your contract is live on LocalNet.

Template ID format: <packageId>:<ModuleName>:<TemplateName> — note the colons, not dots.

8Tap Canton Coin and test transfers

LocalNet has full wallet functionality, with test CC available via the Wallet UI's Tap option.

Open the wallet UIs

  • App User Wallet — wallet.localhost:2000
  • App Provider Wallet — wallet.localhost:3000

Log in with app-user / abc123 or app-provider / abc123. If those show invalid, log into the main admin Keycloak (admin / admin) and set a new password from the Users tab.

Tap CC (App User)

Find the Tap button, add an amount in USD (say, 500), it shows up in the balance almost immediately, converted into CC.

Send CC between parties

In the App User wallet: click Send → paste the App Provider's party ID from Step 6 → enter an amount → confirm. The transfer appears in both wallets real Canton Coin mechanics, privacy preserving, mediated by the local synchronizer.

9Interact via Daml Shell

A REPL for your LocalNet great for exercising choices interactively without writing curl commands.

make shell

You'll drop into an interactive shell connected to the App Provider's participant, where you can query active contracts, exercise choices, and inspect ledger state.

daml> import DA.List
daml> -- your Daml expressions here

Resetting and starting clean

Things will break. That's fine here's how to wipe and start fresh:

make stop           # stop all containers
make clean-all       # remove data, volumes, everything
make build           # rebuild
make start            # start fresh
make clean-all nukes all LocalNet ledger data, party registrations, and CC balances, a full reset. Use it whenever you hit a weird state you can't debug.

Port reference

Bookmark this as you'll refer to it constantly.

ServiceURL / PortWhat it is
App User JSON APIlocalhost:2975Submit commands, query as App User
App Provider JSON APIlocalhost:3975Submit commands, query as App Provider
SV JSON APIlocalhost:4975Super Validator, rarely needed directly
App User Ledger API (gRPC)localhost:2901Lower-level gRPC access
App Provider Ledger API (gRPC)localhost:3901Lower-level gRPC access
App User Wallet UIwallet.localhost:2000Wallet for App User
App Provider Wallet UIwallet.localhost:3000Wallet for App Provider
Scan UIscan.localhost:4000Network transaction explorer
Keycloakkeycloak.localhost:8082Auth server

Common issues

SymptomFix
"Container failed to start"Almost always a memory issue: 8 GB minimum, 12 GB if you can spare it
"401 Unauthorized" on API callsToken expired: re-run the token export from Step 5a
"Empty reply from server" on DAR uploadParticipant isn't ready yet: wait 30 seconds and retry
"409 Conflict" on DAR uploadYou already uploaded that DAR: it's idempotent, the package is already there
Wallet UI blank / can't log inKeycloak still starting: wait, refresh, or make stop && make clean-all && make start
make build fails with SDK mismatchMatch your daml.yaml SDK version to DAML_SDK_VERSION in the quickstart's .env

Quick reference: the commands you'll use most

make install-daml-sdk    # install the pinned Daml SDK
make setup                # configure LocalNet
make build                # compile everything (Daml + backend + frontend)
make capture-logs         # start log collection (separate terminal)
make start                 # start LocalNet
make stop                  # stop LocalNet
make clean-all             # full reset
make canton-console        # open Canton Console (admin REPL)
make shell                 # open Daml Shell (contract REPL)
make help                  # see all available commands