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 →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.
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.
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
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.
| What | Requirement | What drives it |
|---|---|---|
| Java | Current JDK LTS (25) | Any OpenJDK build of the current LTS. The Docker image bundles one. |
| Memory | 8 GB minimum for the JVM heap | Add headroom for each additional network run in the same process. |
| Disk space | Set by the network and the storage approach | A 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 type | SSD, and NVMe for a validator, mining pool or public RPC node | State access during sync is random rather than sequential, which a spinning disk cannot keep up with. |
| CPU | Highest while syncing, lower at the chain tip | Size 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/era1Where 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.
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.
| Role | Keying | Why |
|---|---|---|
| Archive, deep history for applications | hash | Deduplicates identical subtrees, so full history costs less than storing it twice |
| Tip server, feeding other nodes | path | Locality at the tip, which is what a serving node reads most |
| Public RPC relay, pruned | path | Bounded disk and cheap online pruning, with no history to keep |
| Resource-light node | path | Smallest working set, for a machine that is not a server |
| Validator | path | Only needs the tip, and wants predictable disk over time |
| Mining pool | path | Same, 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.
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.
| Endpoint | Port | Returns |
|---|---|---|
| /metrics | 9545 | Prometheus exposition format: chain height, peer count, sync state |
| /health | 8545 | Liveness. Whether the process is up and responding |
| /readiness | 8545 | Readiness. Whether the node is synced enough to serve |
| /buildinfo | 8545 | Version, 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.
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.
systemd
A unit file keeps the node running across reboots, with journald capturing its output alongside every other service on the host.
launchd
A launch agent or daemon, depending on whether the node should run for one user or for the machine.
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.targetStopping, 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.
