Skip to content

Run a standard Koinos node

This guide operates the official koinos/koinos Docker Compose deployment directly.

The result is a standard mainnet node with the required Koinos services and a private JSON-RPC endpoint for health checks. It does not enable block production, REST, gRPC, or historical indexes.

The commands below use:

  • /opt/koinos for the selected release or commit and its configuration files;
  • /var/lib/koinos for persistent node data;
  • a dedicated Linux user that operates Docker.

Choose different absolute paths before starting if those do not match your host, then use the same paths throughout the procedure.

Quick path for a new node

After Docker is installed and the selected koinos/koinos release or commit is checked out at /opt/koinos, the complete first-start path is:

cd /opt/koinos
test ! -e .env
test ! -e config
cp env.example .env
cp -R config-example config
sudoedit .env
docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail 50 chain p2p block_store

In .env, set BASEDIR=/var/lib/koinos, JSONRPC_INTERFACE=127.0.0.1, JSONRPC_PORT=8080, and COMPOSE_PROFILES=jsonrpc. Do not use this new-node shortcut over an existing .env or config/; follow the update procedure instead.

When you intend to stop the node:

cd /opt/koinos
docker compose stop
docker compose ps

Read the numbered procedure before the first production installation. The quick path does not replace the detailed synchronization and health checks.

1. Prepare the host

Review Node requirements. Install Docker Engine and the Compose plugin using the official Ubuntu instructions. Do not substitute Docker Desktop instructions on a production server.

Ubuntu 22.04 LTS and 24.04 LTS are the verified production path in this guide. The upstream project also supports Docker Compose v2 through Docker Desktop on macOS and Windows. Those platforms are useful for local evaluation, but the Linux users, paths, firewall, service management, and recovery commands below do not apply to them unchanged. Follow the official Docker Desktop installation and keep platform-specific data separate from a production node.

Confirm the installed tools, time synchronization, and available disk space:

docker --version
docker compose version
curl --version
jq --version
timedatectl status
df -h /var/lib

Install curl and jq from the Ubuntu repositories if either command is missing:

sudo apt-get update
sudo apt-get install -y curl jq

Create the data directory and make the node operator its owner. Replace koinos with the real operator account:

sudo install -d -m 750 -o koinos -g koinos /var/lib/koinos

Do not continue until time synchronization is active and the data filesystem has the planned capacity.

2. Select the deployment version

The documentation was verified against commit 8216746. The latest immutable repository release at verification time was v2.2.1, while the newer verified commit referenced more recent microservice image tags.

Clone the official repository and check out the exact release or commit you have selected:

sudo git clone https://github.com/koinos/koinos.git /opt/koinos
sudo chown -R koinos:koinos /opt/koinos
cd /opt/koinos
git checkout 821674672e699bf56e94d7c0e8bce122e83d1482

Record the output of git rev-parse HEAD. Do not operate a production node from an unrecorded moving master checkout or floating latest images.

3. Prepare the official configuration

For a new node, copy the configuration supplied by the selected official bundle:

cd /opt/koinos
cp env.example .env
cp -R config-example config

Open .env in your usual text editor and review these values:

Setting Standard node value Reason
BASEDIR /var/lib/koinos persistent node data
P2P_INTERFACE 0.0.0.0 accept peers when 8888/tcp is allowed
JSONRPC_INTERFACE 127.0.0.1 keep the health API private
JSONRPC_PORT 8080 local health endpoint used below
COMPOSE_PROFILES jsonrpc add private JSON-RPC only

Leave RabbitMQ, its administration port, REST, and gRPC on loopback. Keep the image tags supplied by the selected revision.

Do not use the all profile

all also starts block_producer. Block production has separate key and irreversible on-chain prerequisites.

If this is an existing node, do not overwrite .env or config/. Follow the update and rollback procedure instead.

4. Validate and start the node

First ask Compose to render the configuration. Read any error before starting services:

cd /opt/koinos
docker compose config

Start the configured standard node and display its state:

docker compose up -d
docker compose ps

The expected services are amqp, chain, mempool, block_store, p2p, and jsonrpc. The block_producer container must not be present.

5. Verify synchronization and health

Watch the services responsible for receiving and applying blocks:

docker compose logs --tail 100 --follow chain p2p block_store

During the initial synchronization, these messages are expected:

Message Meaning
Requesting blocks ... from peer ... P2P is downloading historical blocks
Sync block progress - Height: ... Block Store is saving synchronized blocks
Sync progress - Height: ... Chain is replaying blocks to rebuild current state
Block applied - Height: ... Chain is applying blocks near the current head

The “block time remaining” displayed with Sync progress is the difference between the historical block timestamp and the current chain head. It is not a wall-clock completion estimate.

In another terminal, verify that every expected service is running, the producer is absent, and no container restarts or is recreated during a 30-second interval:

(
set -euo pipefail
required_services=(amqp chain mempool block_store p2p jsonrpc)
running_services="$(docker compose ps --status running --services)"
for service in "${required_services[@]}"; do
  printf '%s\n' "$running_services" | grep -qx "$service"
done
! printf '%s\n' "$running_services" | grep -qx block_producer

snapshot_containers() {
  for service in "${required_services[@]}"; do
    container_id="$(docker compose ps --all -q "$service")"
    if [[ -z "$container_id" ]]; then
      printf 'ERROR: missing container for %s\n' "$service" >&2
      return 1
    fi
    status="$(docker inspect --format '{{.State.Status}}' "$container_id")"
    if [[ "$status" != running ]]; then
      printf 'ERROR: %s status is %s\n' "$service" "$status" >&2
      return 1
    fi
    restart_count="$(
      docker inspect --format '{{.RestartCount}}' "$container_id"
    )"
    printf '%s %s %s\n' "$service" "$container_id" "$restart_count"
  done
}

containers_before="$(snapshot_containers)"
sleep 30
containers_after="$(snapshot_containers)"
printf '%s\n' "$containers_after"
test "$containers_before" = "$containers_after"
)

The final lines record service, container ID, and restart count. If the command fails, a required service is missing, is not running, restarted, or was recreated during the interval. Inspect docker compose ps --all and the last 100 service log lines before continuing. A historical non-zero restart count is acceptable when it remains stable and the service is healthy.

Fetch the local and independently operated mainnet heads, calculate their ages, and display their heights:

local_head="$(curl --fail --silent --show-error http://127.0.0.1:8080/ \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"chain.get_head_info","params":{},"id":1}')"
public_head="$(curl --fail --silent --show-error \
  https://api.koinos.io/jsonrpc \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"chain.get_head_info","params":{},"id":1}')"

now_ms="$(($(date +%s) * 1000))"
local_height="$(printf '%s' "$local_head" |
  jq -er '.result.head_topology.height | tonumber')"
public_height="$(printf '%s' "$public_head" |
  jq -er '.result.head_topology.height | tonumber')"
local_time="$(printf '%s' "$local_head" |
  jq -er '.result.head_block_time | tonumber')"
public_time="$(printf '%s' "$public_head" |
  jq -er '.result.head_block_time | tonumber')"
local_age="$(((now_ms - local_time) / 1000))"
public_age="$(((now_ms - public_time) / 1000))"

printf 'local height=%s age=%ss\n' "$local_height" "$local_age"
printf 'public height=%s age=%ss\n' "$public_height" "$public_age"
test "$local_height" -gt 0
test "$local_age" -ge -30
test "$local_age" -le 300

During initial synchronization the local age can exceed 300 seconds and the local height can lag substantially. Repeat the check until the local head is fresh and close to the public height. Do not use a public height alone as proof: the local values must come from 127.0.0.1. The small negative tolerance permits ordinary clock and block-timestamp skew; a larger negative age requires a time-synchronization investigation.

Verify that the local height continues to advance:

height_before="$(curl --fail --silent --show-error \
  http://127.0.0.1:8080/ \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"chain.get_head_info","params":{},"id":1}' |
  jq -er '.result.head_topology.height | tonumber')"
sleep 30
height_after="$(curl --fail --silent --show-error \
  http://127.0.0.1:8080/ \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"chain.get_head_info","params":{},"id":1}' |
  jq -er '.result.head_topology.height | tonumber')"
printf 'height before=%s after=%s\n' "$height_before" "$height_after"
test "$height_after" -gt "$height_before"

Finally, verify gossip and peer activity separately. The public P2P RPC only reports whether gossip is enabled; it does not return a peer count:

curl --fail --silent --show-error http://127.0.0.1:8080/ \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"p2p.get_gossip_status","params":{},"id":1}' |
  jq -e '.result.enabled == true'

peer_lines="$(docker compose logs --since 2m --no-color p2p |
  awk '
    /Connected peers:/ { in_peers=1; next }
    in_peers && / - \/.*\/p2p\// { print; next }
    in_peers { in_peers=0 }
  ')"
test -n "$peer_lines"
printf '%s\n' "$peer_lines"

The p2p service emits its “Connected peers” list once per minute in the selected version. Only addresses after the Connected peers marker count; the node's own My address entry does not. Wait two minutes and investigate P2P configuration, firewall, DNS, and seed reachability if no peer line appears.

The node is ready only when:

  • all expected containers remain running without a restart loop;
  • the local head is no more than five minutes old, approaches a trusted mainnet height, and advances across the 30-second observation;
  • P2P gossip is enabled and the P2P logs show at least one connected peer;
  • logs do not show recurring database, verification, or connectivity errors;
  • the data filesystem retains safe free space.

Check data growth directly:

df -h /var/lib/koinos
du -sh /var/lib/koinos/*

Stop and restart safely

Stop cleanly before host maintenance or a consistent data backup:

cd /opt/koinos
docker compose stop
docker compose ps

Wait until no Koinos service is running. Start the same configured services again with:

docker compose up -d
docker compose ps

A plain restart does not pull newer images and may not apply every Compose configuration change.

Next: