Install MEV Sidecar - Rootless Docker
Install the MEV Sidecar as a rootless Docker container running under a dedicated fastlane user. This guide assumes a Monad node is running and synchronized on the validator's machine, and that Validator Setup has been completed.
This guide prioritizes isolation and supply chain verification. The sidecar only needs access to monad-bft's mempool socket, so the socket is relocated out of its default location (which sits alongside unrelated validator files) into a dedicated IPC directory accessible only to a separate fastlane user. The container itself runs read-only, with all Linux capabilities dropped and resource limits applied. Every release is pinned by image digest after three independent checks: GPG signature on the release manifest, cosign image signature, and SLSA provenance attestation.
The MEV Sidecar requires monad-bft v0.12.6 or later. If you are running an older version, the sidecar will connect but tx_received will remain at 0. Upgrade monad-bft before proceeding.
Dependencies
Install Docker and the rootless-docker extras.
curl -fsSL https://get.docker.com | sh
sudo apt update
sudo apt install -y docker-ce-rootless-extras uidmap dbus-user-session
Install cosign and gh, it will be used to verify container images.
# cosign. apt where packaged (Ubuntu 25.04+, Debian 13+); fall back to the
# upstream binary on older releases (Ubuntu 22.04/24.04 LTS, Debian 12).
sudo apt install -y cosign || {
sudo curl -L -o /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
sudo chmod +x /usr/local/bin/cosign
}
# gh from GitHub's apt repo. distro gh predates `gh attestation verify` (2.49+).
(type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& sudo mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& sudo apt update \
&& sudo apt install gh -y
Install other utility tools.
sudo apt install -y acl curl gnupg systemd-container
Create a dedicated user
Create the fastlane user to scope the sidecar permissions.
# Dedicated user for the sidecar
sudo useradd -m -s /bin/bash fastlane
# Create .env (replace with testnet if needed)
echo "SIDECAR_NETWORK=mainnet" | sudo tee /home/fastlane/.env
sudo chown fastlane:fastlane /home/fastlane/.env
# IPC directory
sudo install -d -o monad -g monad -m 2750 /var/run/monad-ipc
# ACLs: grant fastlane traverse on the dir + default rw on new sockets created inside
sudo setfacl -m u:fastlane:rx /var/run/monad-ipc
sudo setfacl -d -m u:fastlane:rw /var/run/monad-ipc
# Delegate cpu/cpuset cgroup controllers to user.slice so the rootless container can apply CPU pinning and limits
[Service]
Delegate=cpu cpuset io memory pids
EOF
sudo systemctl daemon-reload
# Enable lingering
sudo loginctl enable-linger fastlane
Update monad config
monad-bft and monad-rpc need to point the mempool IPC socket to a segregated location, which the fastlane user can safely access. This means changing the --mempool-ipc-path flag for monad-bft and the --ipc-path flag for monad-rpc to point at /var/run/monad-ipc/mempool.sock.
A [Service] drop-in that sets ExecStart= replaces the existing command, it does not extend it. The new ExecStart= line must contain every argument the service was originally launched with, not just the flag you want to change. The examples below use one mainnet validator's specific flag set, your set will differ.
View your full original units before editing, and copy the entire ExecStart= block from each (it usually spans multiple lines via \ continuations):
systemctl cat monad-bft
systemctl cat monad-rpc
The captured block can be pasted into the override as-is with its line continuations, or collapsed onto a single ExecStart= line. Lines starting with # inside the command are stripped by systemd as comments, so do not use them to annotate the ExecStart body.
sudo systemctl edit monad-bft
In the editor that opens, paste the override below. The ExecStart=... line must match your original monad-bft ExecStart (from systemctl cat monad-bft), with --mempool-ipc-path changed to /var/run/monad-ipc/mempool.sock. A complete override on a typical mainnet validator looks like this, your paths and flag set might differ:
[Service]
# /var/run is tmpfs, recreate the IPC dir + ACLs on every boot, before monad-bft
# binds its socket.
ExecStartPre=+/bin/bash -c '\
install -d -o monad -g monad -m 2750 /var/run/monad-ipc && \
setfacl -m u:fastlane:rx /var/run/monad-ipc && \
setfacl -d -m u:fastlane:rw /var/run/monad-ipc'
# UMask 0007 keeps the socket's group bits at rwx, so when the kernel inherits
# the directory's default ACL onto the new socket, the file-level mask stays at
# rwx instead of being clamped to r-x, letting u:fastlane:rw take effect.
UMask=0007
ExecStart=
ExecStart=/usr/local/bin/monad-node \
--secp-identity /home/monad/monad-bft/config/id-secp \
--bls-identity /home/monad/monad-bft/config/id-bls \
--node-config /home/monad/monad-bft/config/node.toml \
--forkpoint-config /home/monad/monad-bft/config/forkpoint/forkpoint.toml \
--wal-path /home/monad/monad-bft/wal \
--mempool-ipc-path /var/run/monad-ipc/mempool.sock \
--control-panel-ipc-path /home/monad/monad-bft/controlpanel.sock \
--statesync-ipc-path /home/monad/monad-bft/statesync.sock \
--ledger-path /home/monad/monad-bft/ledger \
--triedb-path /dev/triedb \
--otel-endpoint "http://127.0.0.1:4317" \
--statesync-sq-thread-cpu 8 \
--record-metrics-interval-seconds 1 \
--validators-path /home/monad/monad-bft/config/validators/validators.toml \
--persisted-peers-path /home/monad/monad-bft/config/peers.toml \
--keystore-password "${KEYSTORE_PASSWORD}"
Save and exit the editor.
sudo systemctl edit monad-rpc
In the editor that opens, paste the override below. The ExecStart=... line must match your original monad-rpc ExecStart (from systemctl cat monad-rpc), with --ipc-path changed to /var/run/monad-ipc/mempool.sock. A complete override on a typical mainnet validator looks like this, your paths and flag set might differ:
[Service]
ExecStart=
ExecStart=/usr/local/bin/monad-rpc \
--ipc-path /var/run/monad-ipc/mempool.sock \
--triedb-path /dev/triedb \
--otel-endpoint "http://0.0.0.0:4317" \
--allow-unprotected-txs \
--node-config /home/monad/monad-bft/config/node.toml
Save and exit the editor.
sudo systemctl daemon-reload
# Restart all monad services
sudo systemctl restart monad-bft monad-execution monad-rpc
# Confirm all is running properly
sudo systemctl status monad-bft monad-execution monad-rpc --no-pager -l
Install the sidecar
Run the following commands as the fastlane user.
sudo machinectl shell fastlane@
Bootstrap rootless docker.
# --force allows installation alongside an existing rootful Docker daemon
dockerd-rootless-setuptool.sh install --force
systemctl --user enable --now docker
# Make every fastlane shell talk to the rootless daemon
cat >> ~/.bashrc <<'EOF'
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
EOF
. ~/.bashrc
# Sanity check, should report "rootless" in SecurityOptions
docker info --format '{{.Name}} {{.SecurityOptions}}'
Import FastLane's release public key.
curl -sL https://fastlane.xyz/keys/sidecar-release.asc | gpg --import
Create the compose file.
nano /home/fastlane/compose.yml
Paste the following in the editor that opens, and save.
networks:
default:
driver: bridge
services:
fastlane-sidecar:
image: ghcr.io/fastlane-labs/fastlane-sidecar:${SIDECAR_VERSION}@${SIDECAR_DIGEST}
container_name: fastlane-sidecar
restart: unless-stopped
# Isolation
read_only: true
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
pids_limit: 256
# Resource limits
mem_limit: 512m
memswap_limit: 512m
# Reuse CPUs assigned to monad-rpc, as it should be doing very little,
# if anything at all on a validator node.
cpuset: "12-15"
# The ONLY host resource the sidecar can see: a dedicated IPC
# directory that contains only the mempool socket. The directory is
# mounted (not the socket file) so the sidecar transparently follows
# the new socket inode after a validator restart.
volumes:
- /var/run/monad-ipc:/var/run/monad-ipc
# Expose metrics on the host loopback only.
ports:
- "127.0.0.1:8765:8765"
command:
- --log-level=info
- --network=${SIDECAR_NETWORK}
- --monitoring-port=8765
- --txpool-socket=/var/run/monad-ipc/mempool.sock
Verify GPG signature.
SIDECAR_VERSION=0.0.16 # Set this to the version you want to install
TAG=v$SIDECAR_VERSION
BASE="https://github.com/FastLane-Labs/fastlane-sidecar/releases/download/${TAG}"
# Download manifest + signature
curl -fLO "${BASE}/fastlane-sidecar-${TAG}.txt"
curl -fLO "${BASE}/fastlane-sidecar-${TAG}.txt.asc"
# Verify the signature
gpg --verify "fastlane-sidecar-${TAG}.txt.asc" "fastlane-sidecar-${TAG}.txt"
# Pin compose to the GPG-attested digest (NOT by tag - tags can move, digests can't)
DIGEST=$(grep '^Digest:' "fastlane-sidecar-${TAG}.txt" | awk '{print $2}')
# Set image and digest in .env for compose to pick up (idempotent, re-run it on every upgrade)
sed -i \
-e "s|^SIDECAR_VERSION=.*|SIDECAR_VERSION=${SIDECAR_VERSION}|" \
-e "s|^SIDECAR_DIGEST=.*|SIDECAR_DIGEST=${DIGEST}|" \
/home/fastlane/.env
grep -q "^SIDECAR_VERSION=" /home/fastlane/.env || echo "SIDECAR_VERSION=${SIDECAR_VERSION}" >> /home/fastlane/.env
grep -q "^SIDECAR_DIGEST=" /home/fastlane/.env || echo "SIDECAR_DIGEST=${DIGEST}" >> /home/fastlane/.env
Verify image.
IMAGE="ghcr.io/fastlane-labs/fastlane-sidecar@${DIGEST}"
# Image verification
cosign verify "$IMAGE" \
--certificate-identity-regexp "^https://github\.com/FastLane-Labs/fastlane-sidecar/\.github/workflows/publish-docker\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
Verify provenance.
# Provenance verification
gh attestation verify "oci://$IMAGE" --owner FastLane-Labs
# If you do not wish to login to gh, run this instead
cosign verify-attestation "$IMAGE" \
--type slsaprovenance1 \
--certificate-identity-regexp "^https://github\.com/FastLane-Labs/fastlane-sidecar/\.github/workflows/publish-docker\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
Pull the image and run.
docker compose -f /home/fastlane/compose.yml pull
docker compose -f /home/fastlane/compose.yml up -d
Confirm the sidecar is running properly.
curl http://localhost:8765/health | jq
last_received_at- Must be a recent timestamp, indicating the sidecar is functioning properlytx_received- Number of transactions the sidecar received from monad-bft. Must be non-zero to be considered healthy
monad-bft gets transactions from the network when the validator is about to become the next leader (as per Monad design), therefore tx_received can show 0 for a few minutes before it starts increasing.
Inspect logs.
docker logs -f fastlane-sidecar
Operations
Logs.
docker logs -f fastlane-sidecar
Start/Stop.
docker compose -f /home/fastlane/compose.yml up -d
docker compose -f /home/fastlane/compose.yml down
Status.
docker ps --filter name=fastlane-sidecar
Upgrading
Open a session with the fastlane user.
sudo machinectl shell fastlane@
Re-run the GPG verify block from above with the new SIDECAR_VERSION (writes new SIDECAR_VERSION + SIDECAR_DIGEST into /home/fastlane/.env).
Pull and restart the container.
docker compose -f /home/fastlane/compose.yml pull
docker compose -f /home/fastlane/compose.yml up -d
Confirm.
docker inspect fastlane-sidecar --format '{{.Image}}'
curl http://localhost:8765/health | jq