Skip to main content
op-geth has reached end-of-support (2026-05-31) and does not support the now-active Karst hardfork, so op-geth nodes can no longer follow the canonical chain. 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

2

Generate the JWT secret

Both containers share a JWT secret over a bind mount:
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:
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

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

Follow logs with:

Verification

Check the node is alive and advancing:
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):
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:

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

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:
3

Extract into the datadir

The bind-mounted directory is ./reth-data (relative to your docker-compose.yml):
--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

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