Running as a service
A node should start on boot, restart after a crash and run without a terminal. This page shows two ways to do that: a systemd service for the binary, and Docker Compose for containers.
Both examples run on Mainnet. For Hoodi, replace mainnet with hoodi and use a Hoodi checkpoint
sync endpoint.
systemd
1. Create a dedicated user
Run Grandine as its own unprivileged user, not as root and not as your login user. If the node is
ever compromised, the damage stays inside that user's files.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin grandine
sudo mkdir -p /var/lib/grandine
sudo chown grandine:grandine /var/lib/grandine
sudo chmod 700 /var/lib/grandine
Grandine appends the network name to --data-dir, so with --data-dir /var/lib/grandine the Mainnet
data lives in /var/lib/grandine/mainnet.
2. Share the JWT secret
The execution client and Grandine must both read the JWT secret. If they run as different users, put
the secret in a group that both belong to. This example assumes the execution client runs as the user
geth:
sudo groupadd --system ethereum
sudo usermod -aG ethereum grandine
sudo usermod -aG ethereum geth
sudo mkdir -p /var/lib/ethereum
openssl rand -hex 32 | tr -d "\n" | sudo tee /var/lib/ethereum/jwt.hex > /dev/null
sudo chown root:ethereum /var/lib/ethereum/jwt.hex
sudo chmod 640 /var/lib/ethereum/jwt.hex
3. Write the unit file
Create /etc/systemd/system/grandine.service:
[Unit]
Description=Grandine Ethereum consensus client
Documentation=https://docs.grandine.io/
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=grandine
Group=grandine
ExecStart=/usr/local/bin/grandine \
--network mainnet \
--data-dir /var/lib/grandine \
--eth1-rpc-urls http://127.0.0.1:8551 \
--jwt-secret /var/lib/ethereum/jwt.hex \
--checkpoint-sync-url CHECKPOINT-URL
# Restart after a crash, but not in a tight loop.
Restart=on-failure
RestartSec=10
# Grandine saves its chain on SIGTERM. Give it time before systemd kills it.
TimeoutStopSec=300
LimitNOFILE=65536
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/grandine
[Install]
WantedBy=multi-user.target
The ProtectHome and ReadWritePaths settings stop Grandine from writing outside its data
directory. If you keep keystores or other files elsewhere, make sure the grandine user can read them.
Note:
--checkpoint-sync-urlis only used when the database is empty, so it is safe to keep it in the unit. On later starts Grandine logsskipping checkpoint sync: existing database foundand continues from its own data.
4. Start and enable the service
sudo systemctl daemon-reload
sudo systemctl enable --now grandine
Check that it runs:
sudo systemctl status grandine
Follow the logs:
sudo journalctl -fu grandine
Show the logs since the last boot, or for a time range:
sudo journalctl -u grandine -b
sudo journalctl -u grandine --since "1 hour ago"
journald rotates the logs for you. To limit how much disk they use, set SystemMaxUse= in
/etc/systemd/journald.conf. See Logging for log levels.
Keep the options in a file
Long command lines are hard to maintain. --args-file loads options from a YAML file, with long option
names as keys. Options given on the command line win over the file.
Create /etc/grandine/mainnet.yaml:
network: mainnet
data-dir: /var/lib/grandine
eth1-rpc-urls:
- http://127.0.0.1:8551
jwt-secret: /var/lib/ethereum/jwt.hex
checkpoint-sync-url: CHECKPOINT-URL
metrics: true
A true value turns on a flag, false leaves it off, and a list repeats the option once per value.
Then use:
ExecStart=/usr/local/bin/grandine --args-file /etc/grandine/mainnet.yaml
Adding validators
To run the integrated validator client, add your keystore options to ExecStart or to the args file,
for example:
keystore-dir: /var/lib/grandine/keys
keystore-password-file: /var/lib/grandine/secrets/password.txt
suggested-fee-recipient: "FEE-RECIPIENT-ADDRESS"
Quote execution addresses in YAML so they stay strings. Make the keystores and password files
readable only by the grandine user:
sudo chown -R grandine:grandine /var/lib/grandine/keys /var/lib/grandine/secrets
sudo chmod 700 /var/lib/grandine/keys /var/lib/grandine/secrets
sudo chmod 600 /var/lib/grandine/keys/* /var/lib/grandine/secrets/*
See Validator client and Managing validator keys.
A separate validator client
To run the validator client as its own service, for example against a beacon node on another machine, create a second unit with its own user and data directory:
[Unit]
Description=Grandine validator client
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=grandine-vc
Group=grandine-vc
ExecStart=/usr/local/bin/grandine vc \
--network mainnet \
--data-dir /var/lib/grandine-vc \
--beacon-node-urls http://BEACON-NODE-HOST:5052 \
--keystore-dir /var/lib/grandine-vc/keys \
--keystore-password-file /var/lib/grandine-vc/secrets/password.txt \
--suggested-fee-recipient FEE-RECIPIENT-ADDRESS
Restart=on-failure
RestartSec=10
TimeoutStopSec=120
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/grandine-vc
[Install]
WantedBy=multi-user.target
The options come after vc. The beacon node behind --beacon-node-urls can be grandine bn or any
other consensus client.
Warning: Never run the same keys in the beacon node's integrated validator client and in a separate
grandine vc, and never give two validator clients the same data directory. Either can get your validators slashed. If you move keys from the beacon node to a separate validator client, stop the beacon node, remove the keys from it, export the slashing protection data, import it into the new validator client and only then start it. See Slashing protection.
Exit codes and restarts
Grandine exits with status 1 when it fails and 2 when the command line is invalid. Many run-time
errors do not reach systemd at all: Grandine restarts its runtime inside the process and logs
application runtime failed. Errors that stop the process include a detected doppelganger. With
Restart=on-failure, systemd starts Grandine again after RestartSec, and doppelganger protection
runs again on the next start.
Docker Compose
The Compose file below runs Geth and Grandine on Mainnet. The same pattern works for other execution clients.
1. Prepare the directory
mkdir -p ~/eth-node/jwt
cd ~/eth-node
openssl rand -hex 32 | tr -d "\n" > jwt/jwt.hex
2. Write docker-compose.yml
services:
geth:
image: ethereum/client-go:stable
restart: unless-stopped
stop_grace_period: 3m
command:
- --mainnet
- --datadir=/data
- --authrpc.addr=0.0.0.0
- --authrpc.port=8551
- --authrpc.vhosts=*
- --authrpc.jwtsecret=/jwt/jwt.hex
ports:
- "30303:30303/tcp"
- "30303:30303/udp"
volumes:
- ./geth-data:/data
- ./jwt:/jwt:ro
grandine:
image: sifrai/grandine:stable
restart: unless-stopped
stop_grace_period: 2m
depends_on:
- geth
command:
- --network=mainnet
- --eth1-rpc-urls=http://geth:8551
- --jwt-secret=/jwt/jwt.hex
- --checkpoint-sync-url=CHECKPOINT-URL
- --http-address=0.0.0.0
- --metrics
- --metrics-address=0.0.0.0
ports:
- "9000:9000/tcp"
- "9000:9000/udp"
- "9001:9001/udp"
- "127.0.0.1:5052:5052"
- "127.0.0.1:5054:5054"
volumes:
- ./grandine-data:/root/.grandine
- ./jwt:/jwt:ro
Replace CHECKPOINT-URL with a Mainnet checkpoint sync endpoint.
About this file:
restart: unless-stoppedrestarts a container after a crash or a reboot, unless you stopped it.stop_grace_periodgives each client time to shut down cleanly. Docker's default is 10 seconds, which is too short.- The Engine API (8551) is reachable only inside the Compose network, not from the host.
--http-address=0.0.0.0makes the Beacon Node API listen inside the container. The port mapping127.0.0.1:5052:5052publishes it on the host's loopback interface only. Do the same for the metrics port 5054.- The peer-to-peer ports 9000 (TCP and UDP) and 9001 (UDP) are published on all interfaces so that peers can reach the node.
./grandine-dataholds Grandine's data in./grandine-data/mainnet.
3. Start the stack
docker compose up -d
docker compose logs -f grandine
Check the sync status from the host:
curl -s http://127.0.0.1:5052/eth/v1/node/syncing
Adding validators in Compose
Mount the keystores and the password file read-only and add the options:
command:
# ... the options above, plus:
- --keystore-dir=/keys
- --keystore-password-file=/secrets/password.txt
- --suggested-fee-recipient=FEE-RECIPIENT-ADDRESS
volumes:
- ./grandine-data:/root/.grandine
- ./jwt:/jwt:ro
- ./validator_keys:/keys:ro
- ./secrets:/secrets:ro
The slashing protection database lives in ./grandine-data/mainnet/validator. Back it up along with
the rest of the data directory; see Slashing protection.
To update the images, see Updating Grandine.
Command line options
--args-file: no default. YAML file with options, keyed by long option name.--data-dir: default$HOME/.grandine. Parent directory for data; the network name is appended.--http-address: default127.0.0.1. Address the Beacon Node API listens on.--http-port: default5052. Port of the Beacon Node API.--metrics: default disabled. Serve Prometheus metrics.--metrics-address: default127.0.0.1. Address of the metrics endpoint.--metrics-port: default5054. Port of the metrics endpoint.--beacon-node-urls(vconly): required. Beacon Node API URLs for the separate validator client.
See the Command line reference for the full list.