| name | midnight-deploy |
| description | Deploy and run Midnight smart contracts on Preprod, Preview, or Standalone (local Docker) networks. Use this skill when deploying contracts, understanding the tNight/DUST fee flow, starting proof servers, or setting up local infrastructure for testing. Triggers on: "deploy contract", "preprod", "preview", "standalone", "proof server", "tNight", "DUST", "faucet", "midnight infrastructure", "start local node", "npm run preprod", "npm run standalone". Always use when the user asks how to run or deploy a Midnight dApp for the first time.
|
| license | MIT |
| metadata | {"author":"mashharuki","version":"2.1.0","midnight-js-version":"4.0.4","compact-toolchain-version":"0.30.0","reference":"midnightntwrk/example-counter"} |
Midnight Deploy Guide
Source of truth: midnightntwrk/example-counter
Quick Overview — 3 Target Networks
| Network | Description | Start command (from counter-cli/) |
|---|
| Preprod | Public testnet ✅ recommended | npm run preprod-ps |
| Preview | Public preview testnet | npm run preview-ps |
| Standalone | Fully local (Docker) | npm run standalone |
Prerequisites
node --version
docker compose version
compact compile --version
Install Compact toolchain (first time)
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/env
compact update 0.30.0
Step 1: Build the Contract
cd contract
npm run compact
npm run build
npm run test
cd ..
Expected compact output:
Compiling 1 circuits:
circuit "increment" (k=10, rows=29)
First run downloads zero-knowledge parameters (~500MB, one-time only).
Step 2: Run on Preprod (Recommended)
Option A — auto-start proof server (simplest)
cd counter-cli
npm run preprod-ps
This pulls the proof server Docker image (midnightntwrk/proof-server:8.0.3), starts it, then launches the CLI.
Option B — manual proof server
Terminal 1:
cd counter-cli
docker compose -f proof-server.yml up
Terminal 2:
cd counter-cli
npm run preprod
Making *-ps scripts reuse an already-running proof server
proof-server.yml hard-binds host port 6300. If a *-ps script always spins up a brand-new DockerComposeEnvironment unconditionally, running it twice (or running it while a manually-started proof server is already up) fails on the port conflict instead of just reusing the existing server. Check first with a plain TCP connect probe before deciding whether to start Docker at all:
import net from 'node:net';
export const isProofServerRunning = (proofServerUrl: string): Promise<boolean> => {
const { hostname, port } = new URL(proofServerUrl);
return new Promise((resolve) => {
const socket = net.createConnection({ host: hostname, port: Number(port), timeout: 1000 });
socket.once('connect', () => { socket.destroy(); resolve(true); });
socket.once('timeout', () => { socket.destroy(); resolve(false); });
socket.once('error', () => resolve(false));
});
};
if (await isProofServerRunning(config.proofServer)) {
console.log(`Proof server already running at ${config.proofServer}; reusing it.`);
await run(config, logger);
} else {
const dockerEnv = new DockerComposeEnvironment(path.resolve(currentDir, '..'), 'proof-server.yml')
.withWaitStrategy('proof-server-1', Wait.forLogMessage('Actix runtime found; starting in Actix runtime', 1).withStartupTimeout(300_000));
await run(config, logger, dockerEnv);
}
Devcontainer / VS Code Remote Containers: proof server unreachable at 127.0.0.1:6300
When the CLI itself runs inside a devcontainer, proof-server.yml's default bridge network makes the proof server a sibling container rather than nested docker-in-docker — from inside the devcontainer, 127.0.0.1:6300 refers to the devcontainer's own loopback, not the proof server's, even though docker compose up reports success. Detect the devcontainer (process.env.REMOTE_CONTAINERS === 'true') and use a variant compose file that shares the devcontainer's network namespace instead of the default bridge network:
services:
proof-server:
image: "midnightntwrk/proof-server:8.0.3"
network_mode: "container:${DEVCONTAINER_HOST_ID}"
export const isDevcontainer = (): boolean => process.env.REMOTE_CONTAINERS === 'true';
const dockerEnv = new DockerComposeEnvironment(
path.resolve(currentDir, '..'),
isDevcontainer() ? 'proof-server.devcontainer.yml' : 'proof-server.yml',
).withEnvironment({ DEVCONTAINER_HOST_ID: process.env.HOSTNAME ?? '' });
Step 3: Create / Restore Wallet
The CLI uses a headless wallet (not a browser wallet like Lace).
[1] Create a new wallet ← generates a 64-char hex seed
[2] Restore from seed ← enter previously saved seed
Save the seed — it cannot be recovered if lost.
Your unshielded address is displayed:
Unshielded Address (send tNight here):
mn_addr_preprod1...
Step 4: Get tNight from Faucet
Visit: https://faucet.preprod.midnight.network/
- Paste your unshielded address (
mn_addr_preprod1...)
- Request tNight tokens
- The CLI detects incoming funds automatically
For Preview: https://faucet.preview.midnight.network/
Step 5: Wait for DUST Generation
After receiving tNight, the CLI automatically:
- Registers your NIGHT UTXOs for dust generation
- Waits for DUST to appear
DUST is the non-transferable fee token required for all Midnight transactions.
It is generated over time from registered NIGHT UTXOs.
✓ Registering 1 NIGHT UTXO(s) for dust generation
✓ Waiting for dust tokens to generate
✓ Configuring providers
Once DUST is available, the contract menu appears:
[1] Deploy a new counter contract
[2] Join an existing counter contract
[3] Monitor DUST balance
[4] Exit
Step 6: Deploy or Join
Deploy new contract
Choose [1]. After proving + balancing + submission:
✓ Deploying counter contract
Contract deployed at: <contract-address>
Save the contract address to rejoin in future sessions.
Join existing contract
Choose [2], enter the saved contract address.
Step 7: Interact
[1] Increment counter ← submits a real tx
[2] Display current value ← reads from blockchain
[3] Exit
Standalone (Full Local Stack)
Runs the entire Midnight stack locally via Docker Compose.
cd counter-cli
npm run standalone
Docker images used (standalone.yml):
| Service | Image |
|---|
| Proof Server | midnightntwrk/proof-server:8.0.3 |
| Indexer | midnightntwrk/indexer-standalone:4.0.0 |
| Node | midnightntwrk/midnight-node:0.22.3 |
Endpoints (standalone):
- Node:
http://127.0.0.1:9944
- Indexer:
http://127.0.0.1:8088/api/v3/graphql
- Proof Server:
http://127.0.0.1:6300
- NetworkId:
undeployed
Network Endpoints Reference
{ networkId: 'preprod',
indexer: 'https://indexer.preprod.midnight.network/api/v3/graphql',
indexerWS: 'wss://indexer.preprod.midnight.network/api/v3/graphql/ws',
node: 'https://rpc.preprod.midnight.network',
proofServer: 'http://127.0.0.1:6300' }
{ networkId: 'preview',
indexer: 'https://indexer.preview.midnight.network/api/v3/graphql',
indexerWS: 'wss://indexer.preview.midnight.network/api/v3/graphql/ws',
node: 'https://rpc.preview.midnight.network',
proofServer: 'http://127.0.0.1:6300' }
{ networkId: 'undeployed',
indexer: 'http://127.0.0.1:8088/api/v3/graphql',
indexerWS: 'ws://127.0.0.1:8088/api/v3/graphql/ws',
node: 'http://127.0.0.1:9944',
proofServer: 'http://127.0.0.1:6300' }
DUST Monitor
While waiting for DUST, choose [3] to monitor:
[10:20:03 PM] DUST: 471,219,000,000,000 (1 coins, 0 pending) | NIGHT: 1 UTXOs, 1 registered | ✓ ready to deploy
Status meanings:
accruing... — DUST is generating
waiting for generation... — UTXOs registered, DUST not yet arrived
no NIGHT registered — need to fund wallet first
⚠ locked by pending tx — restart the app to release locked coins
Troubleshooting
| Issue | Solution |
|---|
compact: command not found | source $HOME/.local/bin/env |
connect ECONNREFUSED 127.0.0.1:6300 | Start proof server: docker compose -f proof-server.yml up |
| Proof server hangs on Mac ARM | Docker Desktop → Settings → General → Virtual Machine Options → Docker VMM |
docker compose up reports success but proof server still unreachable at 127.0.0.1:6300 from inside a devcontainer | You're in a sibling container, not nested docker-in-docker — use a network_mode: container:<devcontainer id> compose variant (see "Devcontainer" section above) |
*-ps script fails to bind port 6300 on second run | A proof server is already running — check with a TCP probe before starting Docker (see "Making *-ps scripts reuse" above) instead of always spinning up a new container |
Failed to clone intent | Known wallet SDK bug — already worked around in example-counter |
| DUST balance 0 after failed deploy | Restart the app to release locked DUST coins |
| Wallet shows 0 balance after faucet | Wait for sync; check you sent to the unshielded address |
Could not find a working container runtime | Start Docker Desktop |
Tests fail with Cannot find module | Build first: cd contract && npm run compact && npm run build |
Deploy Checklist
References