Run with systemd
systemd isn't available on Windows or macOS. Use Run with Docker Compose instead.
The examples use this layout:
/usr/local/bin/ # beacon-chain
/opt/besu/ # Besu
/var/lib/ethereum/
├── jwt.hex # shared by the execution client and beacon node
├── execution/ # execution client data
└── beacon/ # beacon node data
Before you begin
- Prysm’s binaries are installed with the Binaries tab of Install Prysm.
- Besu, unpacked into
/opt/besu: it ships as a folder. It needs Java 25. See Besu's installation guide.
- The
erigonbinary is in/usr/local/bin. See Erigon's installation guide.
- The
ethrexbinary is in/usr/local/bin. See Ethrex's installation guide.
- The
gethbinary is in/usr/local/bin. See Geth's installation guide.
- Nethermind, unpacked into
/opt/nethermind: it ships as a folder. See Nethermind's installation guide.
- The
rethbinary is in/usr/local/bin. See Reth's installation guide.
- The
mev-boostbinary is in/usr/local/bin. Download it from MEV-Boost's releases.
Set up the services
-
Create one unprivileged user per process, the data directories, and the JWT secret. Only the execution client and beacon node can read the secret, through a shared
ethereumgroup:Create the unprivileged users and the data directories. Geth makes its IPC socket readable by its own user only, so the beacon node runs as the
executionuser too:# Create one system user per service: no password, no home folder, no login shell.# Each service runs as its own user.for u in execution beacon; dosudo useradd --system --no-create-home --shell /usr/sbin/nologin $udone# Create the data directories. Each one is readable and writable only by its service's user.sudo install -d -m 0755 /var/lib/ethereumsudo install -d -m 0700 -o execution -g execution /var/lib/ethereum/executionsudo install -d -m 0700 -o beacon -g beacon /var/lib/ethereum/beacon# Create an "ethereum" group with the execution client and beacon node in it.sudo groupadd --system ethereumsudo usermod -aG ethereum executionsudo usermod -aG ethereum beacon# Create the JWT secret: 32 random bytes, written as hex.# Only root can change it, and only the "ethereum" group can read it.openssl rand -hex 32 | tr -d "\n" | sudo tee /var/lib/ethereum/jwt.hex > /dev/nullsudo chown root:ethereum /var/lib/ethereum/jwt.hexsudo chmod 0640 /var/lib/ethereum/jwt.hexgenesis.sszis the network's genesis state. Prysm ships mainnet's, but not the testnet ones. -
Create the execution client service:
/etc/systemd/system/execution.service[Unit]Description=Ethereum execution clientWants=network-online.targetAfter=network-online.target[Service]User=executionGroup=executionExecStart=/opt/besu/bin/besu \--network=mainnet \--data-path=/var/lib/ethereum/execution \--engine-jwt-secret=/var/lib/ethereum/jwt.hexRestart=on-failureRestartSec=5TimeoutStopSec=600NoNewPrivileges=trueProtectSystem=strictReadWritePaths=/var/lib/ethereum/executionProtectHome=truePrivateTmp=true[Install]WantedBy=multi-user.target--externalclstops Erigon from running its own built-in consensus client—open port 42069 (TCP and UDP) for its history downloads.--fullkeeps Reth from running an archive node. Its default log location is in a home directory, which the unit blocks, hence--log.file.directory.Nethermind writes its logs next to its binary by default, which the unit makes read-only, hence
--Init.LogDirectory.Ethrex keeps its data in a subfolder named after the network.
-
Create the MEV-Boost service:
/etc/systemd/system/mev-boost.service[Unit]Description=MEV-BoostWants=network-online.targetAfter=network-online.target[Service]DynamicUser=trueExecStart=/usr/local/bin/mev-boost \-mainnet \-relay-check \-relay=https://0xa15b52576bcbf1072f4a011c0f99f9fb6c66f3e1ff321f11f461d15e31b1cb359caa092c71bbded0bae5b5ea401aab7e@aestus.live \-relay=https://0xa7ab7a996c8584251c8f925da3170bdfd6ebc75d50f5ddc4050a6fdc77f2a3b5fce2cc750d0865e05d7228af97d69561@agnostic-relay.net \-relay=https://0xb0b07cd0abef743db4260b0ed50619cf6ad4d82064cb4fbec9d3ec530f7c5e6793d9f286c4e082c0244ffb9f2658fe88@bloxroute.regulated.blxrbdn.com \-relay=https://0x8c4ed5e24fe5c6ae21018437bde147693f68cda427cd1122cf20819c30eda7ed74f72dece09bb313f2a1855595ab677d@global.titanrelay.xyz \-relay=https://0x8c4ed5e24fe5c6ae21018437bde147693f68cda427cd1122cf20819c30eda7ed74f72dece09bb313f2a1855595ab677d@regional.titanrelay.xyz \-relay=https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net \-relay=https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.moneyRestart=on-failureRestartSec=5TimeoutStopSec=30NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=true[Install]WantedBy=multi-user.targetMEV-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. It stores nothing, so
DynamicUser=truelets systemd run it under a throwaway user. It listens on127.0.0.1:18550. -
Create the beacon node service:
/etc/systemd/system/beacon.service[Unit]Description=Prysm beacon nodeWants=network-online.target execution.serviceAfter=network-online.target execution.service[Service]User=beaconGroup=beaconExecStart=/usr/local/bin/beacon-chain \--mainnet \--datadir=/var/lib/ethereum/beacon \--execution-endpoint=http://127.0.0.1:8551 \--jwt-secret=/var/lib/ethereum/jwt.hex \--checkpoint-sync-url=https://sync-mainnet.beaconcha.in \--beacon-db-pruning \--disable-log-colors \--accept-terms-of-useRestart=on-failureRestartSec=5TimeoutStopSec=120NoNewPrivileges=trueProtectSystem=strictReadWritePaths=/var/lib/ethereum/beaconProtectHome=truePrivateTmp=true[Install]WantedBy=multi-user.targethttps://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.--postpone-shutdown-for-proposalsmakes a stop or restart, wait until no connected validator has a block to propose soon, which can take about 13 minutes; that's whyTimeoutStopSec=900. If the file still shows<YOUR_WALLET_ADDRESS>, enter an address you control under Options above, or replace it in the file.--accept-terms-of-useis needed because a service can't answer the first-run prompt. -
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
/var/lib/ethereum/validator/wallet-password.txtevery time it starts, and only thevalidatoruser can read that file:printf 'New wallet password: ' && read -rs PW && echoprintf 'Confirm password: ' && read -rs PW2 && echoif [ "$PW" = "$PW2" ]; then sudo install -m 0600 -o validator -g validator /dev/null /var/lib/ethereum/validator/wallet-password.txt && printf '%s' "$PW" | sudo tee /var/lib/ethereum/validator/wallet-password.txt > /dev/null; else echo "Passwords don't match: run these lines again."; fiunset PW PW2Then import the keys as the
validatoruser from a copy it can read. Replace<PATH_TO_VALIDATOR_KEYS>with the path to thevalidator_keysfolder: the directory containing all thekeystore-m_*.jsonfiles created in the previous step.sudo cp -r <PATH_TO_VALIDATOR_KEYS> /var/lib/ethereum/validator/importsudo chown -R validator:validator /var/lib/ethereum/validator/importsudo -u validator /usr/local/bin/validator accounts import \--mainnet \--keys-dir=/var/lib/ethereum/validator/import \--wallet-dir=/var/lib/ethereum/validator/wallet \--wallet-password-file=/var/lib/ethereum/validator/wallet-password.txt \--accept-terms-of-usesudo rm -r /var/lib/ethereum/validator/importIt 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.
-
Create the validator service:
/etc/systemd/system/validator.service[Unit]Description=Prysm validator clientWants=network-online.target beacon.serviceAfter=network-online.target beacon.service[Service]User=validatorGroup=validatorExecStart=/usr/local/bin/validator \--mainnet \--datadir=/var/lib/ethereum/validator \--wallet-dir=/var/lib/ethereum/validator/wallet \--wallet-password-file=/var/lib/ethereum/validator/wallet-password.txt \--beacon-rpc-provider=127.0.0.1:4000 \--suggested-fee-recipient=<YOUR_WALLET_ADDRESS> \--enable-doppelganger \--disable-log-colors \--accept-terms-of-useRestart=on-failureRestartSec=5TimeoutStopSec=120NoNewPrivileges=trueProtectSystem=strictReadWritePaths=/var/lib/ethereum/validatorProtectHome=truePrivateTmp=true[Install]WantedBy=multi-user.target -
Start the services:
# Make systemd read the unit files you just created.sudo systemctl daemon-reload# Start the services at every boot (enable), and right now (--now).# They start in the order set by the After= lines in the units.sudo systemctl enable --now execution beaconRestart=on-failurein each unit restarts the service after a crash.Check that they're running:
systemctl is-active execution beaconEvery service should be
active.Each service logs to the journal. To follow one, run its line:
sudo journalctl -fu execution # follow the execution clientsudo journalctl -fu beacon # follow the beacon nodeUntil 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=[127.0.0.1:4000]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:sudo journalctl -u execution -n 50 --no-pager -
"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:
sudo journalctl -fu validatorKeep all your services running; once active, your validator attests and proposes automatically.
After you edit a unit, run this before restarting it:
sudo systemctl daemon-reload
If you move a data directory, update ReadWritePaths= too, or the service fails with read-only file system.
Logs. To look back through a service's logs, filter by time or by level:
sudo journalctl -u beacon --since "1 hour ago" # the last hour
sudo journalctl -u beacon -p warning -b # warnings and errors since boot
The journal rotates by itself. To cap its size, set SystemMaxUse=2G in /etc/systemd/journald.conf and run:
sudo systemctl restart systemd-journald
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.
Troubleshooting
status=217/USER.
The User= in the unit doesn't exist. Create it as in step 1.
permission denied reading jwt.hex.
The service user isn't in the ethereum group, or the file isn't root:ethereum 0640. Test with:
sudo -u beacon cat /var/lib/ethereum/jwt.hex
permission denied or read-only file system in the data directory.
The directory belongs to another user, often after running a binary by hand as root. Fix it:
sudo chown -R beacon:beacon /var/lib/ethereum/beacon
sudo chown -R execution:execution /var/lib/ethereum/beacon
Also check that ReadWritePaths= in the unit points to the data directory.
The service restarts in a loop. Read the last error:
sudo journalctl -u beacon -n 50 --no-pager
A misspelled or removed flag stops Prysm at startup; compare with the beacon-chain reference.
Remove everything
These commands undo this page.
# Stop the services, and stop them from starting at boot.
sudo systemctl disable --now execution beacon
Once the validator service is stopped, your validator misses its duties and loses a little ETH every epoch until it runs again, here or on another machine.
If you run these keys elsewhere, export their slashing protection history before deleting the data. The other validator client needs it to avoid signing something that contradicts what this one already signed:
sudo -u validator /usr/local/bin/validator --disable-ephemeral-log-file \
slashing-protection-history export \
--datadir=/var/lib/ethereum/validator \
--slashing-protection-export-dir=/tmp/slashing-protection
sudo cat /tmp/slashing-protection/slashing_protection.json > ~/slashing_protection.json
This saves slashing_protection.json in your home folder. If the export fails with this error, the validator client never reached the beacon node, so it never signed anything, and there's nothing to export:
genesis validators root is empty, perhaps you are not connected to your beacon node
Then delete the rest:
# Delete the unit files, and make systemd forget them.
sudo rm -f /etc/systemd/system/execution.service
sudo rm -f /etc/systemd/system/beacon.service
sudo systemctl daemon-reload
sudo systemctl reset-failed
# Delete all the data: chain data, JWT secret.
sudo rm -r /var/lib/ethereum
# Delete the system users and the "ethereum" group.
sudo userdel execution
sudo userdel beacon
sudo groupdel ethereum
The binaries in /usr/local/bin emain; delete them yourself if you no longer need them.
The binaries in /usr/local/bin and the client’s folder in /opt remain; delete them yourself if you no longer need them.