Docker Deployment
NOOSChain ships one Linux container image containing the compiled Node.js node, the noos operator CLI, SQL migrations, and the native HashiCorp Raft, Nervos SMT, and Wasmtime binaries. PostgreSQL remains a separate service.
The Node.js process owns the native child-process lifecycle. HashiCorp Raft is supervised as the consensus engine, Nervos SMT is supervised as the state-root engine, and Wasmtime is started locally when contract preflight or execution needs it. Nervos and Wasmtime communicate over stdin/stdout and expose no network ports.
Build The Local Image
npm run docker:buildThis produces nooschain:local. The tag means the image was built locally; it is not a production release tag.
The multi-stage build includes:
dist/index.jsand the compilednoosCLI;- all SQL files under
dist/db/migrations; /usr/local/bin/hashicorp-raft;/usr/local/bin/noos-state-root-sidecar-nervos;/usr/local/bin/noos-contract-executor-wasmtime;- production Node.js dependencies only.
The final process runs as the non-root nooschain user under tini so signals are forwarded and child processes are reaped.
Edit The Local Configuration
Compose uses the checked-in development examples by default. To keep local changes outside those examples, copy them into the ignored docker/config directory:
Copy-Item docker/env/development-validator.env.example docker/config/validator.env
Copy-Item docker/env/development-observer.env.example docker/config/observer.env
$env:NOOS_VALIDATOR_ENV_FILE = "./config/validator.env"
$env:NOOS_OBSERVER_ENV_FILE = "./config/observer.env"The paths are resolved relative to docker/compose.local.yaml. Edit the files directly, use the admin GUI configuration editor, or use the packaged helper:
docker compose -f docker/compose.local.yaml run --rm cli `
config edit --profile development-validator --file /config/validator.env
docker compose -f docker/compose.local.yaml run --rm cli `
config validate --profile development-validator --file /config/validator.envKeep the two PowerShell environment variables set when running Compose. In CI or another shell, set the equivalent variables in that process environment.
Validate And Start Compose
Validate interpolation without starting services:
npm run docker:configStart PostgreSQL, migrations, and the validator:
npm run docker:up
docker compose -f docker/compose.local.yaml psThe startup order is PostgreSQL health, the one-shot validator migration, then the validator. Migrations are not run automatically by every node process.
Stop containers without deleting persistent data:
npm run docker:downDeleting volumes is intentionally not part of the ordinary stop command.
Automatic Raft Block Production
Pending transactions do not commit until the Raft leader proposes a block. Enable periodic, non-empty proposals on every validator that may become leader:
RAFT_AUTO_BUILD_BLOCKS=true
RAFT_BLOCK_INTERVAL_MS=5000
RAFT_MAX_TRANSACTIONS_PER_BLOCK=100Each validator may use the same settings. Only the current leader proposes; followers skip the interval tick, observers never start the timer, and a new block is not created when the mempool is empty. The adapter also prevents two interval ticks from proposing concurrently. Production profiles leave this disabled until the operator explicitly selects an interval.
Confirm the live setting through GET /api/consensus/status. The response includes autoBuildBlocks and blockIntervalMs.
Local Services
| Service | Purpose |
|---|---|
postgres | Local PostgreSQL containing separate validator and observer databases. |
migrate-validator | Applies validator database migrations and exits. |
validator | HashiCorp validator with Nervos and Wasmtime available locally. |
migrate-observer | Optional observer migration job. |
observer | Optional non-voting observer. |
cli | On-demand operator and maintenance CLI. |
The default stack publishes only the validator NOOSChain API on host port 9001. HashiCorp sidecar HTTP and Raft TCP ports remain inside the private Compose network.
Start The Observer
docker compose -f docker/compose.local.yaml --profile observer up -dThe observer does not bootstrap a second cluster. It starts with HASHICORP_GO_RAFT_BOOTSTRAP=false and must be added to live runtime membership as a non-voter.
Inspect and save the operator plan:
docker compose -f docker/compose.local.yaml exec validator `
noos governance operator-membership plan --jsonReview the output, place only approved actions in docker/config/approved-plan.json, and execute the normal guarded workflow through the mounted CLI service:
docker compose -f docker/compose.local.yaml run --rm cli `
governance operator-membership execute --plan-file /config/approved-plan.json --dry-run
docker compose -f docker/compose.local.yaml run --rm cli `
governance operator-membership execute --plan-file /config/approved-plan.json --yesFiles under docker/config are mounted into the on-demand CLI service.
Run The CLI
The image exposes the compiled CLI as noos.
Run against the active validator environment:
docker compose -f docker/compose.local.yaml exec validator noos node health --json
docker compose -f docker/compose.local.yaml exec validator noos raft-go health --json
docker compose -f docker/compose.local.yaml exec validator noos state-root preflight --json
docker compose -f docker/compose.local.yaml exec validator noos contracts runtime preflight --jsonRun an isolated API-based CLI container:
docker compose -f docker/compose.local.yaml run --rm cli node health --jsonEdit or validate a host configuration file through docker/config:
docker compose -f docker/compose.local.yaml run --rm cli `
config validate --profile production-validator --file /config/validator.envUse exec validator for commands that depend on that node's local database or loopback-only sidecars.
Persistence
The local stack creates distinct volumes for:
- PostgreSQL;
- validator HashiCorp Raft data;
- observer HashiCorp Raft data;
- validator Nervos checkpoints;
- observer Nervos checkpoints.
Never share Raft or checkpoint volumes between nodes. Nervos checkpoint files are paired with trusted metadata in PostgreSQL. Back up the database and checkpoint volume as related artifacts and run state-root readiness after a restore.
Wasmtime needs no persistent volume. Contract packages and state remain in NOOSChain/PostgreSQL; the child process receives bounded execution inputs over stdin and returns deterministic effects.
Ports And Firewall
The local Compose network uses:
| Node | NOOSChain API | Sidecar HTTP | Raft TCP |
|---|---|---|---|
| Validator | 9001 | 9101 | 10001 |
| Observer | 9002 | 9102 | 10002 |
Only 9001 is published by the default stack. Enabling the observer profile also publishes 9002. Sidecar and Raft ports are exposed only to other containers on the private bridge network.
Nervos SMT and Wasmtime open no ports and need no firewall rules.
For separate production servers:
- expose the public API through a TLS reverse proxy, normally on
443; - keep sidecar HTTP on a management/private network;
- allow Raft TCP only between approved Raft hosts;
- keep PostgreSQL private;
- use reachable private IPs or private DNS names instead of loopback addresses.
Native Sidecar Paths
Docker environment files must use the packaged absolute paths:
HASHICORP_GO_RAFT_BINARY_PATH=/usr/local/bin/hashicorp-raft
NOOS_STATE_ROOT_SIDECAR_COMMAND=/usr/local/bin/noos-state-root-sidecar-nervos
NOOS_CONTRACT_WASMTIME_SIDECAR_PATH=/usr/local/bin/noos-contract-executor-wasmtime
NOOS_STATE_ROOT_CHECKPOINT_DIR=/var/lib/nooschain/state-root-checkpointsProduction smart-contract nodes should use:
NOOS_SMART_CONTRACTS_ENABLED=true
NOOS_CONTRACT_RUNTIME_PREFLIGHT=requiredAn incompatible Wasmtime protocol, ABI, executor version, Wasmtime version, host-import set, or fuel policy fails required production preflight. Active Nervos nodes similarly fail closed when the sidecar or checkpoint contract is not ready.
Security Defaults
The local node services use:
- a non-root user;
- a read-only root filesystem;
- dropped Linux capabilities;
no-new-privileges;- a writable
/tmptmpfs; - explicit Raft and checkpoint volumes;
- no Docker socket mount.
The development examples disable TLS and use a known development operator token. Do not expose them to the public internet.
Production examples under docker/env are incomplete placeholders. Supply real secrets through the deployment secret manager, mount certificates read-only under /run/nooschain/certs, use private addresses, and validate the result before deployment.
Health And Readiness
GET /health is a shallow liveness check. It does not prove database, consensus, or sidecar readiness.
Before routing production traffic, run:
noos node health --json
noos raft-go health --json
noos state-root readiness --json
noos state-root preflight --json
noos contracts runtime preflight --json
noos production preflight --profile validatorProduction Images
Production servers should pull a prebuilt, verified image rather than compile source locally. Use the same Dockerfile and source revision as local testing, but build with version and revision metadata, run smoke gates, publish to a registry, and deploy by immutable digest.
registry.example.org/nooschain@sha256:<digest>Build a multi-architecture OCI archive without publishing it:
$env:NOOSCHAIN_RELEASE_IMAGE="registry.example.org/nooschain"
$env:NOOSCHAIN_VERSION="0.1.0"
npm run docker:release-buildAfter verification, publish the same version with BuildKit provenance and SBOM attestations:
npm run docker:release-pushThe release script rejects local and latest as release versions. Signing the published digest and enforcing registry admission policy remain explicit release-operator steps.
Set Compose's image without editing the file:
NOOSCHAIN_IMAGE=registry.example.org/nooschain@sha256:<digest>Roll observers first where possible, run the one-shot migration job, and roll validators one at a time without losing quorum. Verify Raft membership, Nervos readiness, Wasmtime preflight, replay, sync, and chain health after each step.
Smoke Test
The opt-in Docker smoke test builds the image, verifies all native binaries, starts PostgreSQL and the validator, checks API/CLI health, runs Nervos and Wasmtime preflight, generates a temporary development admin identity, initializes genesis, and submits two signed transactions through the packaged CLI. Each transaction is committed through HashiCorp Raft, and the test requires the proposal response to contain the locally applied block before it verifies the transaction as executed. It removes the temporary key material and only its uniquely named Compose project and volumes.
npm run docker:smokeThe smoke test requires Docker with network access for base images and build dependencies. It is intentionally separate from the fast TypeScript test suite.
Two-Validator Transaction Test
Run the isolated two-voter HashiCorp Raft test:
npm run docker:test-two-validatorsThis test builds the image and starts:
- PostgreSQL with separate databases for
validator-aandvalidator-b; - two validator containers with separate Raft and Nervos checkpoint volumes;
- two HashiCorp sidecars bootstrapped with both nodes as voters;
- on-demand CLI containers connected to each validator.
It generates a temporary development genesis and admin key, waits for a stable leader, and submits ten signed REGISTER_ORGANIZATION transactions through the leader's packaged CLI. Every transaction is committed in its own block. After each proposal, the test waits until both validators report the transaction as executed at the same height.
The final assertions require:
- exactly two runtime Raft voters;
- ten executed transactions on both validators;
- height
10on both validators; - identical latest block hashes and Nervos state roots;
- successful proposal responses containing the committed transaction.
The test uses a uniquely named Compose project and removes its temporary keys, containers, network, and volumes. It does not modify the ordinary docker:up stack.
Add A Non-Voting Observer
Run the same ten-transaction scenario with an observer:
npm run docker:test-two-validators-one-observerThis variant uses the same image for three node containers. validator-a and validator-b remain the only voters. The observer receives its own PostgreSQL database, Raft data volume, and Nervos checkpoint volume.
Because a running observer is read-only, the test applies migrations and initializes its genesis state through a one-shot maintenance container before starting observer-a. After startup, the current leader adds it to HashiCorp Raft with AddNonvoter.
The test additionally requires:
- runtime membership of exactly two voters and one non-voter on all nodes;
- all ten transactions to execute on the observer;
- the observer's height, block hash, and state root to match both validators;
- the observer to reject a block-proposal request.