Run with Docker Compose
Before you begin
- Linux: Docker Engine with the Compose plugin. See Docker's guides to install Docker Engine and install the Compose plugin. Docker Engine starts at boot, and the node starts with it.
- macOS: Docker Desktop for Mac. Docker Desktop starts when you sign in, not at boot, and the node only runs while the Mac is awake. In Docker Desktop’s settings, turn on Start Docker Desktop when you sign in to your computer, and under Resources, set the memory to the amount listed in System requirements.
Run the commands on this page in a terminal. This must print v2 or later:
- Docker Desktop for Windows with the WSL 2 backend.
- A Linux distribution in WSL, such as Ubuntu. See Microsoft's guide to install WSL. In Docker Desktop's settings, turn on WSL integration for that distribution.
Run every command on this page in that distribution's terminal (for example, open Ubuntu from the Start menu), not in PowerShell. The node's folder then lives on WSL's Linux file system; an execution client's database under C:\ would be far too slow.
Docker Desktop starts when you sign in to Windows, not at boot. In its settings, turn on Start Docker Desktop when you sign in to your computer; after a restart, the node stays down until someone signs in. WSL 2 also gets only part of the machine’s memory by default; set memory in .wslconfig to what System requirements lists.
This must print v2 or later:
docker compose version
Set up the services
-
Create a folder for the node and go into it. The compose file goes here, and the services keep everything they store in their
datafolder, one folder per network, as in the Quickstart. Docker creates them on first start:mkdir -p ~/ethereum-docker && cd ~/ethereum-dockergenesis.sszis the network's genesis state. Prysm ships mainnet's, but not the testnet ones. -
Save the compose file as
~/ethereum-docker/docker-compose.yml:docker-compose.ymlservices:jwt-init:container_name: jwt-initimage: alpine:3.22volumes:- ./data/jwt:/jwtcommand:- sh- -c- test -f /jwt/jwt.hex || od -An -tx1 -N32 /dev/urandom | tr -d ' \n' > /jwt/jwt.hexbesu:container_name: besuimage: hyperledger/besu:26.9.0restart: unless-stoppedstop_grace_period: 10mdepends_on:jwt-init:condition: service_completed_successfullycommand:- --network=mainnet- --data-path=/data- --engine-jwt-secret=/jwt/jwt.hex- --engine-host-allowlist=*- --rpc-http-host=0.0.0.0volumes:- ./data/mainnet/besu:/data- ./data/jwt:/jwt:roports:- "30303:30303/tcp" # P2P- "30303:30303/udp" # P2P discoverybeacon:container_name: beaconimage: gcr.io/offchainlabs/prysm/beacon-chain:v7.3.1restart: unless-stoppedstop_grace_period: 2mdepends_on:jwt-init:condition: service_completed_successfullybesu:condition: service_startedcommand:- --mainnet- --datadir=/data- --execution-endpoint=http://besu:8551- --jwt-secret=/jwt/jwt.hex- --checkpoint-sync-url=https://sync-mainnet.beaconcha.in- --beacon-db-pruning- --http-host=0.0.0.0- --accept-terms-of-usevolumes:- ./data/mainnet/beacon:/data- ./data/jwt:/jwt:roports:- "13000:13000/tcp" # libp2p- "13000:13000/udp" # libp2p (QUIC)- "12000:12000/udp" # discv5- "127.0.0.1:3500:3500" # beacon REST API, this machine onlyOn every start,
jwt-initcreates the JWT secret indata/jwtif it isn't there yet, then exits. The services that use the secret wait for it. The Engine API (8551) stays inside the Docker network.The Engine API goes through the IPC socket in the
ipcvolume, which Docker manages and only the two containers mount.Only the peer-to-peer ports are published to the internet; ports are published to the internet; the beacon API is published only to
127.0.0.1.The validator client reaches the beacon node over gRPC (4000) inside the Docker network.
stop_grace_period: 15mgives--postpone-shutdown-for-proposalstime to wait for a proposal; Docker's default of 10 seconds would kill the node mid-write.Both Prysm services send fees to the address you entered under Options above. If the file still shows
<YOUR_WALLET_ADDRESS>, enter an address you control there, or replace it in the file.https://sync-mainnet.beaconcha.inis one of several community checkpoint sync endpoints; any one you trust works.Syncing from genesis replays every block and takes days, not minutes. See Sync from genesis.
--beacon-db-pruningdeletes blocks and states older than about 5 months, the oldest data other nodes can still ask for, so the database stops growing. Leave it out if you need the full history.MEV-Boost uses the working relays from the EthStaker relay list for this network. Relays differ in what they filter and how they share profits; the list describes each one, so remove any you don't want to use. MEV-Boost listens on port 18550 inside the Docker network only;
-addr=0.0.0.0:18550lets thebeaconcontainer reach it.Erigon's image runs as UID
1000, so its data folder must belong to that ID. Before the first start, hand it over with a one-off container:docker compose run --rm --no-deps --user root --entrypoint chown erigon -R 1000:1000 /data--externalclstops Erigon from running its own built-in consensus client. Port 42069 is for its history downloads.--fullkeeps Reth from running an archive node, which needs far more disk than a staking node.Ethrex keeps its data in a subfolder named after the network.
Prysm is pinned to v7.3.1, the current release. To update later, change the version in the
image:lines and rundocker compose up -dagain: it downloads the new images and recreates only the containers that changed. -
Create validator keys. Download the EthStaker deposit CLI, ideally onto a machine that has never been connected to the internet. Generate a mnemonic and your validator keys:
./deposit new-mnemonic --chain=mainnetThe CLI asks how many validators to create and for a withdrawal address. Use an address you control: withdrawals and the stake itself go there when you exit. It also asks you to choose a keystore password; you'll need it in the next step to import the keys into Prysm. Write the 24-word mnemonic down on paper and keep it offline. Anyone who has it controls your validator and can withdraw funds. The CLI writes a
validator_keysfolder containing adeposit_data-*.jsonfile and onekeystore-m_*.jsonper validator. Copy that folder to this machine. -
Import your keys into Prysm. Choose a password for the new wallet. The validator client reads it from
data/mainnet/validator/wallet-password.txtevery time it starts:printf 'New wallet password: ' && read -rs PW && echoprintf 'Confirm password: ' && read -rs PW2 && echoif [ "$PW" = "$PW2" ]; then printf '%s' "$PW" > data/mainnet/validator/wallet-password.txt && chmod 0600 data/mainnet/validator/wallet-password.txt; else echo "Passwords don't match: run these lines again."; fiunset PW PW2Then import the keys with a one-off container;
--no-depskeeps the node itself stopped for now. Replace<PATH_TO_VALIDATOR_KEYS>with the path to thevalidator_keysfolder: the directory containing all thekeystore-m_*.jsonfiles created in the previous step.docker compose run --rm --no-deps -v <PATH_TO_VALIDATOR_KEYS>:/keys:ro \validator accounts import \--mainnet \--keys-dir=/keys \--wallet-dir=/data/wallet \--wallet-password-file=/data/wallet-password.txt \--accept-terms-of-useIf the
validator_keysfolder is on the Windows side, use its WSL path, such as/mnt/c/Users/<YOUR_WINDOWS_USER>/validator_keys.It asks for the password of the keys you're importing:
Enter the password for your imported accounts:This is the keystore password you chose in the deposit CLI when creating your validator keys. Once the keys are imported, the validator client no longer needs it.
-
Start the services:
docker compose up -drestart: unless-stoppedrestarts a container after a crash or reboot, unless you stopped it yourself.List the services:
docker compose psAll services should be
running.Until both the beacon node and the execution client are synced, the validator client logs this warning every slot. That's expected:
WARN fallback: No responsive beacon node found tried=[beacon:4000]Follow its logs with
docker compose logs -f validator.Check the beacon node's sync status:
curl -s http://127.0.0.1:3500/eth/v1/node/syncingThe node is fully running once the response shows all three of these as
false:{"data": {"head_slot": "…","sync_distance": "0","is_syncing": false,"is_optimistic": false,"el_offline": false}}-
"el_offline": false: the beacon node reaches the execution client. This should be the case as soon as both services are up. If it staystrue, read the execution client's logs, for example withdocker compose logs --tail 50 gethfor Geth. -
"is_syncing": false: the beacon node is synced.From a checkpoint, this takes a few minutes: the node starts from a recent finalized state instead of replaying the chain.
From genesis, this takes days: the node replays every block since the start of the chain.
sync_distanceshows how many slots it still has to go. -
"is_optimistic": false: the execution client is synced. This takes the longest, from a few hours to a few days depending on your execution client and the network. Until then, the beacon node follows the chain but can't check the blocks’ transactions.
-
-
Make your deposit. Deposit only once the sync check at the end of the previous step shows all three values as
false.Upload
deposit_data-*.jsonon the Staking Launchpad and follow its checks before depositing 32 ETH.Only use the official launchpadScam sites copy the launchpad and send your deposit to their own contract. Type the address yourself or use a bookmark; never follow links from ads, search results, chats, or emails.
- Launchpad: https://launchpad.ethereum.org/
Upload
deposit_data-*.jsonon the EthStaker console and deposit 32 Hoodi ETH. Never send real ETH to a testnet deposit contract.Only use the official consoleScam sites copy the console and send your deposit to their own contract. Type the address yourself or use a bookmark; never follow links from ads, search results, chats, or emails.
- Deposit page: https://hoodi.console.ethstaker.org/deposit
The beacon chain then processes the deposit and puts your validator in the activation queue. Activation takes anywhere from hours to weeks depending on the queue. The validator client logs each stage:
Waiting for deposit to be observed by beacon nodeValidator deposited, entering activation queue after finalizationWaiting for activation... Check validator queue status in a block explorerValidator activatedFollow them with:
docker compose logs -f validatorKeep all your services running; once active, your validator attests and proposes automatically.
Logs.
docker compose logs -f beacon # follow the beacon node
docker compose logs --since 1h # every service, the last hour
Everyday commands.
docker compose stop # stop everything
docker compose up -d # start it again
docker compose restart beacon # restart one service
docker compose down # remove the containers
docker compose down keeps your data in ~/ethereum-docker/data.
Never run the validator service on two machines, or next to another validator client, with the same keys. Both would sign, and the keys would be slashed.
Prysm also keeps the most recent debug logs in <datadir>/logs/: the current file plus one older one, at most about 500 MB. To turn that off, add --disable-ephemeral-log-file.
Check that it works
Restart the machine once, then run the checks from Start the services again: everything should come back by itself. On macOS and Windows, sign in first so Docker Desktop starts.
Troubleshooting
A container restarts in a loop. Read the last error:
docker compose logs --tail 50 beacon
A misspelled or removed flag stops Prysm at startup; compare with the beacon-chain reference.