Skip to content
On this page

L2-15: Run Your Own Node (Running an Ethereum Node)

1. Problem

The vast majority of Ethereum developers (and virtually all regular users) rely on third-party RPC providers like Infura, Alchemy, or QuickNode to interact with Ethereum. While convenient, these services introduce centralized dependency: your DApp only works when this provider is online and not rate-limiting your API key. More fundamentally, you place yourself in the position of "trusting a company to tell you the state of the blockchain" -- the balance your MetaMask wallet displays is, in reality, what Infura tells it.

Running your own Ethereum full node means you have your own "source of truth for the blockchain" -- your node independently verifies every block, every transaction, every state change. Your RPC calls don't need an API key, are not subject to rate limits, and have no monthly call quotas. You can read on-chain data with the lowest possible latency (local network vs. internet) and maintain control over your data privacy.

This challenge (refer to scripts/run-node.md) requires completing the full process of setting up an Ethereum full node from scratch -- including installing and configuring execution and consensus clients, JWT authentication, sync monitoring, and Dockerized deployment.

2. Why

Self-hosting a node represents a shift in identity from "Web3 user" to "Web3 infrastructure operator." When you run your own node, you truly understand Ethereum's two-layer architecture: the Execution Layer (EL, handling transaction execution and the EVM) and the Consensus Layer (CL, handling PoS consensus and block finality). These two layers communicate via the Engine API (using JWT authentication) -- this is not an abstract textbook concept, but the --authrpc.jwtsecret parameter that you configure by hand.

Running a node is also a necessary step toward becoming a professional blockchain engineer. In MEV searching, transaction simulation, high-frequency DeFi strategies, and L2 sequencer development, having your own high-availability node is an infrastructure prerequisite. Your node provides more than just RPC access -- it also maintains a complete mempool (pending transaction pool), provides transaction tracing APIs, and can execute complex eth_call simulations (including state overrides).

More importantly, self-hosted nodes are a contribution to network decentralization. Every new full node that joins makes the Ethereum network more censorship-resistant and diverse. When enough people run their own nodes, the risk that "a single RPC provider can decide what blockchain state you see" ceases to exist.

3. Solution

Core Architecture: Execution Client + Consensus Client

Ethereum Full Node
├── Execution Client (EL)     <- Geth / Nethermind / Besu / Erigon
│   ├── Maintains world state (account balances, contract storage)
│   ├── Executes EVM transactions
│   ├── Manages the transaction pool (mempool)
│   ├── Provides JSON-RPC API (eth_call, eth_sendRawTransaction...)
│   └── Communicates with CL via Engine API (port 8551)

└── Consensus Client (CL)     <- Lighthouse / Prysm / Teku / Nimbus
    ├── Tracks the PoS chain (Beacon Chain)
    ├── Participates in block validation and finality
    ├── Notifies EL of new blocks via Engine API
    └── Provides Beacon API (port 5052)

Step 1: Install Execution Client Geth

bash
# Ubuntu/Debian
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install -y ethereum

# macOS
brew install ethereum

# Or compile from source
git clone https://github.com/ethereum/go-ethereum.git
cd go-ethereum
make geth
sudo cp build/bin/geth /usr/local/bin/

Step 2: Install Consensus Client Lighthouse

bash
curl -LO https://github.com/sigp/lighthouse/releases/latest/download/lighthouse-linux-x86_64
chmod +x lighthouse-linux-x86_64
sudo mv lighthouse-linux-x86_64 /usr/local/bin/lighthouse

Step 3: Configure JWT Secret (Authentication between EL and CL)

bash
sudo mkdir -p /var/lib/ethereum
openssl rand -hex 32 | sudo tee /var/lib/ethereum/jwt.hex > /dev/null
# This jwt.hex file must be readable by both EL and CL
# EL reads it to verify CL's Engine API calls
# CL reads it to sign its Engine API requests to EL

Step 4: Start the Execution Client

bash
geth \
  --mainnet \                          # Mainnet (use --sepolia for testnet)
  --datadir /data/ethereum \           # Blockchain data storage path
  --http \                             # Enable HTTP JSON-RPC
  --http.api eth,net,engine,admin \    # Exposed API namespaces
  --http.addr 0.0.0.0 \               # Listen on all interfaces (allow external access)
  --authrpc.jwtsecret /var/lib/ethereum/jwt.hex \  # JWT secret path
  --syncmode snap \                    # Snap sync (recommended)
  --cache 4096                         # Memory cache (MB)

Key parameter explanations:

  • --syncmode snap: Snap sync -- downloads the latest state snapshot rather than replaying all transactions from genesis. Sync time is approximately 12-24 hours
  • --http.api eth,net,engine,admin: engine is required for CL communication, eth is the standard RPC, net and admin are operational APIs
  • --cache 4096: Allocates 4 GB of memory cache for the state trie; higher cache = faster sync

Step 5: Start the Consensus Client

bash
lighthouse bn \
  --network mainnet \                           # Mainnet
  --datadir /data/lighthouse \                  # Beacon chain data
  --http \                                      # Enable Beacon API
  --execution-endpoint http://localhost:8551 \  # EL Engine API address
  --execution-jwt /var/lib/ethereum/jwt.hex \  # JWT secret path
  --checkpoint-sync-url https://sync-mainnet.beaconcha.in  # Checkpoint sync for faster sync

--checkpoint-sync-url is the key acceleration parameter: it starts Beacon chain sync from a trusted checkpoint (rather than from genesis), reducing CL sync time from days to 2-3 hours. Checkpoints are provided by community-trusted nodes (beaconcha.in is the infrastructure behind the beaconcha.in explorer).

Step 6: Verify Sync Status

bash
# Check EL sync progress
geth attach --exec "eth.syncing"
# Returns false -> fully synced
# Returns { currentBlock: ..., highestBlock: ... } -> syncing in progress

# Check CL sync status
curl http://localhost:5052/eth/v1/node/syncing
# {"data":{"head_slot":"...","sync_distance":"..."}}

Hardware Requirements

ComponentMinimumRecommended
CPU4 cores8+ cores
RAM16 GB32 GB
Storage2 TB SSD (NVMe)4 TB NVMe
Network25 Mbps100+ Mbps unmetered

NVMe SSD is critical -- SATA SSD's random IOPS are insufficient and will cause extremely slow sync speeds. Storage grows at approximately 2.5 GB per week (Ethereum state growth); 4 TB provides about 2-3 years of headroom.

Docker Compose Quick Deployment

yaml
version: '3.8'
services:
  geth:
    image: ethereum/client-go:latest
    volumes:
      - /data/geth:/root/.ethereum
      - /var/lib/ethereum/jwt.hex:/jwt.hex
    command: >
      --mainnet --http --http.addr 0.0.0.0
      --http.vhosts="*" --http.api eth,net,web3
      --authrpc.jwtsecret /jwt.hex
      --syncmode snap --cache 4096
    ports:
      - "8545:8545"     # HTTP RPC
      - "30303:30303"   # P2P TCP
      - "30303:30303/udp"  # P2P UDP

  lighthouse:
    image: sigp/lighthouse:latest
    volumes:
      - /data/lighthouse:/root/.lighthouse
      - /var/lib/ethereum/jwt.hex:/jwt.hex
    command: >
      lighthouse bn --network mainnet
      --execution-endpoint http://geth:8551
      --execution-jwt /jwt.hex
      --checkpoint-sync-url https://sync-mainnet.beaconcha.in
    ports:
      - "5052:5052"
bash
docker compose up -d      # Start
docker compose logs -f    # View logs

4. Pitfalls Encountered

  • Sync time far exceeds expectations: Snap sync documentation says 12 hours, but in practice it may take 2-3 days -- depending on hardware IOPS and peer quality
  • Running out of storage space: Mainnet state grows at approximately 2.5 GB per week -- a 2 TB drive may be insufficient after 12-18 months; when the disk is full, Geth crashes rather than degrading gracefully
  • JWT configuration errors: EL and CL pointing --authrpc.jwtsecret to different files, or the file permissions are unreadable -- the two cannot communicate and the node stalls
  • Firewall / P2P reachability: Port 30303 (TCP/UDP) not open -> peers have only outbound connections, no inbound connections -- slower discovery, slower sync
  • Security assumptions of snap sync: Snap sync does not verify historical state before the sync point -- it trusts that the snapshot point is correct
  • RPC exposure risk: --http.addr 0.0.0.0 exposes RPC to the public internet -- without firewall protection, anyone can use your node (potentially causing DoS)

5. Why the Pitfalls Exist

The linear growth of storage is determined by Ethereum's "account model" characteristics. Unlike the UTXO model (where Bitcoin's unspent outputs can be pruned), Ethereum must persist every account's state (balance, nonce, contract storage). Even if an account has been unused for years, its storage slots still occupy disk space. When the disk reaches 100% full, Geth's LevelDB database reports I/O errors, the node process terminates -- you must manually clean up or expand capacity.

The most common cause of JWT configuration errors is path mismatch -- the EL launch script uses /var/lib/ethereum/jwt.hex, the CL launch script uses /etc/ethereum/jwt.hex, and the two point to different files (one exists, the other may be empty or have different content). Another common issue is Docker volume mounting -- the container-internal path /jwt.hex must be correctly mapped to the host path.

P2P reachability affects sync speed because Ethereum's discovery protocol (discv5) is bidirectional -- your node not only needs to find other nodes, but other nodes also need to be able to find your node. By default, after startup your node queries bootstrap nodes to get an initial peer list. If port 30303 is unreachable (NAT/firewall), other nodes cannot actively connect to you, and you can only rely on outbound connections to discovered peers -- this significantly reduces peer pool diversity.

Snap sync is a trade-off between security and speed: full sync (--syncmode full) replays every transaction from genesis -- this takes weeks and terabytes of space, but verifies the entire history. Snap sync starts from a recent "safe point" -- this "safe point" is hardcoded in Geth's source code by the Geth team; you are trusting that the Geth team has not maliciously inserted an incorrect state snapshot.

6. How to Resolve the Pitfalls

Configure automated disk monitoring to get warned before the disk fills up:

bash
# crontab: check disk usage daily
0 9 * * * df -h /data/ethereum | mail -s "Disk Report" admin@example.com

Or use Prometheus + Grafana monitoring (Geth exports Prometheus metrics on port 6060).

Use --checkpoint-sync-url to accelerate CL sync, but cross-validate checkpoint trustworthiness from multiple sources. Both beaconcha.in and ethstaker.cc provide public checkpoint sync endpoints.

Ensure JWT consistency: Use environment variables or config files to uniformly manage the JWT path:

bash
export JWT_PATH=/var/lib/ethereum/jwt.hex
geth --authrpc.jwtsecret $JWT_PATH ...
lighthouse bn --execution-jwt $JWT_PATH ...

Configure NAT traversal to resolve P2P reachability:

bash
geth --nat extip:<your-public-ip>  # Manually specify public IP
# Or enable UPnP on your router

RPC security hardening: In production, do not expose the personal and admin namespaces; use a reverse proxy (Nginx) to add an authentication layer:

bash
geth --http.api eth,net --ws.api eth,net
# Only expose the necessary API namespaces
# personal: account management (never expose)
# admin: node management (local access only)
# txpool: transaction pool contents (potentially sensitive)

Establish a backup and recovery plan: Periodically back up datadir/chaindata (use geth export to export snapshots) to recover from disk failure or data corruption.

7. Key Technical Points

PointDescription
EL + CL dual-client architectureExecution Layer (Geth) + Consensus Layer (Lighthouse) = fully validating node
Engine APIEL port 8551, communicates with CL via JWT authentication
JWT secretopenssl rand -hex 32 to generate; EL and CL must use the same file
Snap sync~12-24 hours, downloads state snapshot rather than replaying all historical transactions
Full syncWeeks, replays all transactions from genesis, fully verifies all history
Checkpoint sync--checkpoint-sync-url starts from a trusted checkpoint; CL sync in only 2-3 hours
P2P port30303 TCP/UDP, needs bidirectional reachability to maximize peer pool
RPC port8545 (HTTP); namespace control limits exposed functionality
Dockerizedethereum/client-go + sigp/lighthouse official images; compose up and done

Built with AiAda