AN EXECUTION CLIENT
FOR THE EVM ECOSYSTEM

LinuxmacOSWindowsDocker
Operations

Operating a Fukuii node

What a node needs to be configured, watched, supervised and verified in production. Everything on this page ships in the binary: there is no agent to install alongside it and no plugin to enable before it will report what it is doing. Where a choice exists, it is usually decided by the role the node plays rather than by preference.

docker run ghcr.io/fukuii-project/fukuii-cli
Scala 3PekkoJVMRocksDB
Operations

Running Fukuii

Everything below ships in the binary. No sidecar, no plugin, no configuration required before the node will tell you what it is doing.

Observability

Fukuii reports what it is doing.

  • Prometheus metrics + Grafana dashboards
  • /health · /readiness · /buildinfo endpoints
  • ResilientRollingFileAppender for disk pressure

Behavior under load

Sync does not starve the RPC.

  • Isolated Pekko dispatchers: sync, rpc, general
  • Bootstrap checkpoints skip startup peer-wait
  • Log rotation survives disk pressure

Supply chain

You can verify what you deployed.

  • Cosign-signed container images on GHCR
  • Build provenance attestation
  • CycloneDX SBOM on every release

Aggregation gateways

Fukuii works as an upstream for RPC aggregation.

  • dRPC-Provider gRPC bridge
  • An RPC-relay deployment profile
  • A Scala implementation behind the aggregation layer

Runbooks, alerting guidance, and upgrade procedures live in the operator documentation.

Operator docs →
Configuration

Where a setting comes from

Configuration layers in a fixed order. A shipped profile provides the base, a configuration file overrides it, the environment overrides that, and a command-line argument wins over everything. Nothing merges unpredictably: for any given setting there is one answer and one place it came from.

The profile layer is what makes a network selection a single argument. Naming a network resolves to a set of values that would otherwise have to be supplied by hand, and the layers above exist so a deployment can differ from that base without maintaining a fork of it. Put the stable decisions in a file, the per-host ones in the environment, and keep the command line for the argument that changes between runs.

Several at once

More than one network in one process

Running two networks in a single process is only safe if nothing is shared by accident, and the thing usually shared by accident is observability. A metrics registry or a log handle held once for the whole process works fine until a second node appears, at which point two nodes report into one set of counters and neither number means anything.

So the registry belongs to the instance rather than to the process. Each network gets its own metrics, its own health state and its own storage, and each is supervised separately, which is what lets one host run an archive node for one network and a pruned node for another without the two interfering or being mistaken for each other on a dashboard.

Supply chain

Verifying a release

Every release is signed and inventoried, so the artifact running in production can be traced back to the build that produced it. This is the whole of Fukuii's supply-chain posture, in one place rather than asserted across several pages.

  • Cosign keyless signatures on every container image published to ghcr.io/fukuii-project/fukuii-cli
  • Build provenance attestation, tying an artifact to the workflow that built it
  • A CycloneDX SBOM on every release artifact, listing every dependency that shipped
  • The running node reports its own version, commit and provenance at /buildinfo
History

Keeping history instead of pruning it

An archive node retains historical state rather than pruning it, which is what a block explorer or an analytics service needs. The cost is disk. ERA1 frozen segments allow that history to be imported in bulk rather than replayed from genesis, and shared between operators instead of every node fetching it independently.

Node requirements, and what determines each one
WhatRequirementWhat drives it
JavaCurrent JDK LTS (25)Any OpenJDK build of the current LTS. The Docker image bundles one.
Memory8 GB minimum for the JVM heapAdd headroom for each additional network run in the same process.
Disk spaceSet by the network and the storage approachA pruned Ethereum mainnet node is on the order of a terabyte and grows with the chain. Ethereum Classic is substantially smaller. An archive node is a multiple of the pruned figure on either.
Disk typeSSD, and NVMe for a validator, mining pool or public RPC nodeState access during sync is random rather than sequential, which a spinning disk cannot keep up with.
CPUHighest while syncing, lower at the chain tipSize for the sync, then scale down on observed usage rather than on the peak.

Importing frozen history

ERA1 segments are a fixed, content-addressed format, so a segment fetched from one operator is the same as a segment fetched from any other. Importing them is bounded disk work rather than an open-ended replay against peers.

# Import frozen history in bulk instead of replaying it from genesis
fukuii import-era1 \
  --network=etc \
  --data-path=/var/lib/fukuii \
  --era1-dir=/var/lib/fukuii/era1

Where the segments come from

The format is one decision and the transport is another. Segments can arrive over plain HTTP from an operator publishing them, peer to peer from everyone who already holds them, or as a prepared copy of a data directory taken from a node that is already synced. The peer-to-peer route matters most where few nodes serve state, because it turns every operator who has already imported the history into a source for the next one instead of concentrating the load on whoever is willing to serve it.

The transport has no bearing on what is trusted. Whichever route the segments take, they are checked against a known anchor before being accepted, so a source cannot become trustworthy by being fast or convenient.

Storage

Choosing a storage approach

Storage is a selection rather than a fixed backend. How trie nodes are keyed, whether state is pruned, and how much history is retained are all chosen for the role the node plays, because an archive node and a pruned RPC relay want opposite things from the same database.

Keying is the decision with the largest consequences. Hash keying deduplicates identical subtrees, which is what makes deep history affordable. Path keying groups nodes by their position in the trie, which gives locality at the chain tip and makes pruning cheap enough to run online. Neither is better; they are optimized for different reads.

Storage approach by operator role
RoleKeyingWhy
Archive, deep history for applicationshashDeduplicates identical subtrees, so full history costs less than storing it twice
Tip server, feeding other nodespathLocality at the tip, which is what a serving node reads most
Public RPC relay, prunedpathBounded disk and cheap online pruning, with no history to keep
Resource-light nodepathSmallest working set, for a machine that is not a server
ValidatorpathOnly needs the tip, and wants predictable disk over time
Mining poolpathSame, with the chain tip held stable under load

The approach is a property of the data directory, so a host can run an archive node for one network and a pruned node for another without the two interfering.

Monitoring

Metrics and health

Metrics are served in Prometheus exposition format on their own port, separate from the JSON-RPC surface, so monitoring can reach them without being granted RPC access.

Endpoints exposed by a running node
EndpointPortReturns
/metrics9545Prometheus exposition format: chain height, peer count, sync state
/health8545Liveness. Whether the process is up and responding
/readiness8545Readiness. Whether the node is synced enough to serve
/buildinfo8545Version, commit hash and build provenance of the running binary

Liveness and readiness are different questions

Liveness asks whether the process is up and answering. Readiness asks whether it is far enough along to be given work, and for a node that means consensus rather than uptime: a node still syncing is alive and running correctly, and would serve stale answers if a load balancer sent traffic to it. Keeping the two separate is what lets an orchestrator restart a hung node without also removing a healthy one that is merely still catching up.

Scraping it

scrape_configs:
  - job_name: fukuii
    metrics_path: /metrics
    static_configs:
      - targets: ["localhost:9545"]

Garbage collection is measured, not assumed

The usual objection to running a node on the JVM is collector pauses. Rather than argue about it, Fukuii exports GC pause time and the fraction of wall-clock time spent collecting, sampled during state healing and reported alongside the sync metrics. A fraction climbing toward 0.2 means the sync is GC-bound, and that it is worth changing the heap before changing the disk.

That turns a runtime property into an operational reading. The alternative is discovering it from a slow sync and guessing at the cause.

Supervision

Running as a service

A node that only runs while a terminal is open is not deployed. Each platform has its own supervisor, and Fukuii is an ordinary long-running process to all of them.

LINUX

systemd

A unit file keeps the node running across reboots, with journald capturing its output alongside every other service on the host.

MACOS

launchd

A launch agent or daemon, depending on whether the node should run for one user or for the machine.

WINDOWS

service

Registered as a Windows Service so it starts without an interactive login, which is what an unattended host needs.

A systemd unit

Run the node as its own unprivileged user with its data directory outside the home tree. The file-descriptor limit matters: a node holds many peer sockets and RocksDB file handles open at once, and the default limit is low enough to bite under load.

[Unit]
Description=Fukuii
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=fukuii
Group=fukuii
ExecStart=/usr/local/bin/fukuii \
  --network=etc \
  --data-path=/var/lib/fukuii \
  --metrics-enabled \
  --metrics-port=9545
Restart=on-failure
RestartSec=10
# The node holds many peer sockets and RocksDB file handles at once
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Stopping, and being stopped

A supervisor that can restart a node will eventually restart one in the middle of a write. Shutdown is therefore ordered rather than abrupt where it can be: work in flight is allowed to finish and the database is closed properly, so an ordinary restart leaves nothing to repair.

When that is not possible, because the process was killed outright or the machine lost power, the node detects on the next start that its last shutdown was unclean and acts on it rather than assuming its data directory is intact. The failure mode worth avoiding is not a crash; it is a node that comes back up believing a half-written state is complete.