Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.optimism.io/llms.txt

Use this file to discover all available pages before exploring further.

op-geth reaches end-of-support on 2026-05-31 and will not support the L1 Glamsterdam hardfork. This tutorial runs op-reth (the primary supported execution client) via Docker. See the op-geth deprecation notice for the full migration plan.
This tutorial runs an OP Stack node using the official op-reth and op-node Docker images in a single docker-compose.yml. No source build required.
OP Mainnet requires a one-time pre-Bedrock state import. OP Sepolia does not. If you point this tutorial at OP Mainnet on a fresh datadir, op-reth will fail at startup with Op-mainnet has been launched without importing the pre-Bedrock state. Before running OP Mainnet, follow the op-reth sync-op-mainnet guide to either restore from a pre-synced snapshot or run the minimal-bootstrap import. OP Sepolia bootstraps via snap sync with no extra steps — start there if you’re just testing the setup.

Dependencies

  • Docker
  • Docker Compose (v2)
  • An L1 execution RPC endpoint (Ethereum mainnet for OP Mainnet, or Ethereum Sepolia for OP Sepolia).
  • An L1 Beacon API endpoint for the same L1 chain. Needed by op-node to fetch blob data post-Ecotone.

Quick start

1

Create a working directory

mkdir op-stack-node && cd op-stack-node
2

Generate the JWT secret

Both containers share a JWT secret over a bind mount:
openssl rand -hex 32 > jwt.txt
3

Create the .env file

Configure your network and L1 endpoints. Pick one network block (Sepolia or Mainnet) and fill in your L1 RPC + Beacon URLs:
cat > .env <<'EOF'
# --- Network: OP Sepolia (default) ---
OP_RETH_CHAIN=optimism_sepolia
OP_NODE_NETWORK=op-sepolia
OP_RETH_SEQUENCER=https://sepolia-sequencer.optimism.io

# --- Network: OP Mainnet (uncomment to use instead) ---
# OP_RETH_CHAIN=optimism
# OP_NODE_NETWORK=op-mainnet
# OP_RETH_SEQUENCER=https://mainnet-sequencer.optimism.io

# --- L1 endpoints (required) ---
L1_RPC_URL=https://your-l1-rpc-endpoint
L1_RPC_KIND=basic
L1_BEACON_URL=https://your-l1-beacon-endpoint
EOF
L1_RPC_KIND valid values: alchemy, quicknode, infura, parity, nethermind, debug_geth, erigon, basic, any. Use basic if unsure.
OP_RETH_CHAIN and OP_NODE_NETWORK accept any superchain-registry chain (e.g. unichain / unichain-mainnet, soneium / soneium-mainnet). For each chain, point L1_RPC_URL / L1_BEACON_URL at the corresponding L1 (Ethereum Mainnet or Sepolia) and update OP_RETH_SEQUENCER to that chain’s sequencer endpoint.
4

Create docker-compose.yml

services:
  op-reth:
    image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-reth:v2.2.5
    container_name: op-reth
    ports:
      - "8545:8545"          # JSON-RPC HTTP
      - "8546:8546"          # JSON-RPC WebSocket
      - "9001:9001"          # Prometheus metrics
      - "30303:30303"        # P2P TCP
      - "30303:30303/udp"    # P2P UDP
    volumes:
      - ./reth-data:/data
      - ./jwt.txt:/jwt.txt:ro
    command:
      - node
      - --chain=${OP_RETH_CHAIN}
      - --datadir=/data
      - --http
      - --http.addr=0.0.0.0
      - --http.port=8545
      - --ws
      - --ws.addr=0.0.0.0
      - --ws.port=8546
      - --authrpc.addr=0.0.0.0
      - --authrpc.port=8551
      - --authrpc.jwtsecret=/jwt.txt
      - --rollup.sequencer=${OP_RETH_SEQUENCER}
      - --metrics=0.0.0.0:9001
    restart: unless-stopped

  op-node:
    image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node:v1.18.2
    container_name: op-node
    depends_on:
      - op-reth
    ports:
      - "9545:7000"          # JSON-RPC HTTP (host 9545 → container 7000; macOS uses 7000 for AirPlay)
      - "7300:7300"          # Prometheus metrics
      - "9222:9222"          # P2P TCP
      - "9222:9222/udp"      # P2P UDP
    volumes:
      - ./jwt.txt:/jwt.txt:ro
    command:
      - op-node
      - --l1=${L1_RPC_URL}
      - --l1.rpckind=${L1_RPC_KIND}
      - --l1.beacon=${L1_BEACON_URL}
      - --l2=ws://op-reth:8551
      - --l2.jwt-secret=/jwt.txt
      - --network=${OP_NODE_NETWORK}
      - --syncmode=execution-layer
      - --l2.enginekind=reth
      - --rpc.addr=0.0.0.0
      - --rpc.port=7000
      - --metrics.enabled
      - --metrics.addr=0.0.0.0
      - --metrics.port=7300
    restart: unless-stopped
The op-reth datadir is bind-mounted from ./reth-data on the host so you can inspect / restore from a snapshot directly (see Bootstrap from a snapshot below).Image tags shown are the latest as of writing. For the current tags, see the op-reth releases and op-node releases.
5

Start the node

docker compose up -d
Follow logs with:
docker compose logs -f

Verification

Check the node is alive and advancing:
curl -s -X POST -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  http://localhost:8545
Expected: {"jsonrpc":"2.0","id":1,"result":"0x..."} with the current block in hex. Run again after a minute — the number should increase as sync progresses. For op-node sync status (unsafe, safe, and finalized heads in one call):
curl -s -X POST -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"optimism_syncStatus","params":[],"id":1}' \
  http://localhost:9545 | jq .
During the initial EL-driven sync (--syncmode=execution-layer), unsafe_l2 tracks the tip via libp2p gossip while safe_l2 and finalized_l2 stay at 0. This is expected — op-node defers L1 derivation until op-reth finishes its staged sync. Once the EL catches up, derivation begins and the safe/finalized heads start advancing.
You can also tail op-node logs directly:
docker compose logs op-node | grep -E "Sync progress|Finished EL sync"

Bootstrap from a snapshot

Syncing from scratch is fine for OP Sepolia (~30–50 GB, hours) but slow for OP Mainnet (~700 GB, days). For Mainnet — or anytime you’d rather skip the initial sync — bootstrap op-reth from a pre-synced snapshot.
For OP Mainnet, a snapshot also handles the pre-Bedrock state requirement (see the warning at the top of this page) in one step.
1

Stop the containers

docker compose down
rm -rf reth-data       # only if you previously synced and want to start clean
2

Download a snapshot

Browse datadirs.optimism.io for the snapshot matching your network and pick a recent file. Then download and verify the SHA256:
curl -fLO https://datadirs.optimism.io/<snapshot-file>.tar.zst

# Verify checksum against the value on the index page
sha256sum <snapshot-file>.tar.zst   # Linux
shasum -a 256 <snapshot-file>.tar.zst   # macOS
3

Extract into the datadir

The bind-mounted directory is ./reth-data (relative to your docker-compose.yml):
mkdir -p reth-data
tar -I zstd -xvf <snapshot-file>.tar.zst -C reth-data --strip-components=1
--strip-components=1 removes the top-level wrapping directory inside the tarball. Run tar -tf <snapshot-file>.tar.zst | head -3 first to confirm — if files are already at the archive root, omit --strip-components.
4

Start the node

docker compose up -d
docker compose logs -f op-reth
op-reth will recognize the existing datadir on startup and pick up from the snapshot’s tip — latest_block should be the snapshot’s height, not 0. It will then catch up from that tip to current (minutes for Sepolia, hours for Mainnet, depending on snapshot age).

Next steps