Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
# Host data directory (JWT + execution + consensus). Default matches official docs.
# DATA_DIR=/blockchain

# HTTP JSON-RPC (wallets). Bound to 0.0.0.0 (LAN). Default 8545
# HTTP JSON-RPC (wallets). Default bind 0.0.0.0 (LAN), port 8545.
# Set HTTP_ADDR=127.0.0.1 for this machine only (survives git pull; do not edit compose).
# HTTP_ADDR=0.0.0.0
# HTTP_PORT=8545

# WebSocket RPC. Bound to 0.0.0.0 (LAN). Default 8546
# WebSocket RPC. Default bind 0.0.0.0 (LAN), port 8546.
# WS_ADDR=0.0.0.0
# WS_PORT=8546

# Beacon REST/JSON HTTP API. Default host 127.0.0.1 (this machine only), port 3500.
Expand All @@ -26,13 +29,15 @@
# Lower this on 16 GB hosts if the machine swaps; raise it on 32 GB+ SSDs.
# GETH_CACHE=1024

# P2P listen ports (host networking). Change only if you already forwarded
# different ports or run a second node. Keep TCP/UDP pairs in sync on the router.
# GETH_P2P_PORT=30303
# BEACON_P2P_TCP_PORT=13000
# BEACON_P2P_UDP_PORT=12000

# Optional: pin or float client images.
# Defaults in docker-compose.yml are digest-pinned version tags.
# GETH_IMAGE=registry.gitlab.com/pulsechaincom/go-pulse:v3.3.0
# BEACON_IMAGE=registry.gitlab.com/pulsechaincom/prysm-pulse/beacon-chain:v2.3.0
# To float on upstream latest instead: ./update.sh --latest
# (that command writes GETH_IMAGE / BEACON_IMAGE into this file so it sticks)

# Suggested validator fee recipient (when you later run a validator).
# Not used by this RPC stack. Leave blank if you only need a private RPC.
# FEE_RECIPIENT=0xYourPulseChainAddressHere
51 changes: 29 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This project packages the official PulseChain clients in Docker Compose with a s
| [Go-Pulse](https://gitlab.com/pulsechaincom/go-pulse) | Execution layer (JSON-RPC / WebSocket) | `registry.gitlab.com/pulsechaincom/go-pulse:v3.3.0` |
| [Prysm-Pulse](https://gitlab.com/pulsechaincom/prysm-pulse) | Consensus layer (beacon chain) | `registry.gitlab.com/pulsechaincom/prysm-pulse/beacon-chain:v2.3.0` |

**Defaults:** mainnet · checkpoint sync · data under `/blockchain` · wallet RPC on the LAN (`0.0.0.0`) · Engine API and beacon HTTP/gRPC localhost-only · digest-pinned client tags (`./update.sh --latest` writes `:latest` into `.env`)
**Defaults:** mainnet · checkpoint sync · data under `/blockchain` · wallet RPC on the LAN (`0.0.0.0`) · Engine API and beacon HTTP/gRPC localhost-only · digest-pinned client tags (`./update.sh --latest` writes `:latest` into `.env`) · bind addresses and P2P ports via `.env` (so `git pull` does not wipe them)

---

Expand Down Expand Up @@ -82,7 +82,7 @@ CORS / vhosts default to `*` so LAN web wallets can reach the node. A page you v

The installer warns if the host itself has a public IP and UFW is not active. That is the typical VPS misconfiguration (an unauthenticated public RPC). Home machines behind NAT are fine.

LAN binding is intentional so phones and other machines on the same network can use `http://YOUR_LAN_IP:8545`. To restrict access to the host only, see [Localhost-only mode](#localhost-only-mode).
LAN binding is intentional so phones and other machines on the same network can use `http://YOUR_LAN_IP:8545`. To restrict access to the host only, set `HTTP_ADDR` / `WS_ADDR` in `.env` — see [Localhost-only mode](#localhost-only-mode).

---

Expand Down Expand Up @@ -208,13 +208,14 @@ Chain data is stored under **`/blockchain`** and is retained when containers are

By default, **wallet** RPC binds to `0.0.0.0` (all interfaces). The Engine API (`--authrpc.addr=127.0.0.1`, port 8551) and beacon HTTP/gRPC (`BEACON_HTTP_HOST` / `BEACON_GRPC_HOST`, default `127.0.0.1`) are already host-only.

To accept wallet RPC connections **only on the host**:
To accept wallet RPC connections **only on the host**, set these in `.env` (do not edit `docker-compose.yml` — a `git pull` would overwrite compose edits):

1. Edit `docker-compose.yml`.
2. Under the **geth** service, change:
- `--http.addr=0.0.0.0` → `--http.addr=127.0.0.1`
- `--ws.addr=0.0.0.0` → `--ws.addr=127.0.0.1`
3. Apply the change (`./restart.sh` recreates containers from compose; it does not keep stale flags):
```bash
HTTP_ADDR=127.0.0.1
WS_ADDR=127.0.0.1
```

Then apply:

```bash
./restart.sh
Expand All @@ -239,14 +240,14 @@ Then run `./restart.sh`. Keep those ports firewalled to your LAN; never port-for

| Port | Protocol | Purpose | Default bind |
|------|----------|---------|--------------|
| 8545 | TCP | HTTP JSON-RPC (wallets) | `0.0.0.0` (LAN) |
| 8546 | TCP | WebSocket RPC | `0.0.0.0` (LAN) |
| 8545 | TCP | HTTP JSON-RPC (wallets) | `0.0.0.0` (LAN; `HTTP_ADDR` / `HTTP_PORT`) |
| 8546 | TCP | WebSocket RPC | `0.0.0.0` (LAN; `WS_ADDR` / `WS_PORT`) |
| 3500 | TCP | Beacon REST API | `127.0.0.1` (localhost; override with `BEACON_HTTP_HOST`) |
| 4000 | TCP | Beacon gRPC | `127.0.0.1` (localhost; override with `BEACON_GRPC_HOST`) |
| 8551 | TCP | Engine API (JWT; geth ↔ beacon) | `127.0.0.1` (localhost only) |
| 30303 | TCP/UDP | Execution P2P | Host |
| 13000 | TCP | Beacon P2P | Host |
| 12000 | UDP | Beacon P2P | Host |
| 30303 | TCP/UDP | Execution P2P | Host (`GETH_P2P_PORT`) |
| 13000 | TCP | Beacon P2P | Host (`BEACON_P2P_TCP_PORT`) |
| 12000 | UDP | Beacon P2P | Host (`BEACON_P2P_UDP_PORT`) |

**Do not** forward RPC/API ports 8545, 8546, 3500, or 4000 to the public internet.

Expand All @@ -258,13 +259,13 @@ Your node can already make **outbound** connections. That is not enough if you w

To properly participate in the network you should also accept **inbound** peers. Nodes that only make outbound connections put more load on the network and usually have worse peer counts and slower sync.

**Open these ports for inbound traffic:**
**Open these ports for inbound traffic** (defaults; keep the router in sync if you change `.env`):

| Port | Protocol | Purpose |
|------|----------|---------|
| 30303 | TCP + UDP | Go-Pulse (execution) |
| 13000 | TCP | Beacon P2P |
| 12000 | UDP | Beacon P2P |
| Port | Protocol | Purpose | `.env` |
|------|----------|---------|--------|
| 30303 | TCP + UDP | Go-Pulse (execution) | `GETH_P2P_PORT` |
| 13000 | TCP | Beacon P2P | `BEACON_P2P_TCP_PORT` |
| 12000 | UDP | Beacon P2P | `BEACON_P2P_UDP_PORT` |

### 1. Firewall on the node (UFW)

Expand All @@ -277,6 +278,8 @@ sudo ufw allow 13000/tcp
sudo ufw allow 12000/udp
```

`./install.sh` uses the P2P ports from `.env` when it adds these rules.

### 2. Port forwarding on your router (required for inbound peers)

This is the part most people skip — and it’s the most important.
Expand Down Expand Up @@ -338,17 +341,20 @@ On a VPS or cloud VM, `10.0.0.0/8` (and often `172.16.0.0/12`) is the **VPC**, n
| JWT secret | `$DATA_DIR/jwt.hex` (mode `600`) |
| Execution image | `go-pulse:v3.3.0` digest-pinned (override with `GETH_IMAGE` or `./update.sh --latest`) |
| Beacon image | `beacon-chain:v2.3.0` digest-pinned (override with `BEACON_IMAGE` or `./update.sh --latest`) |
| Wallet RPC bind | `0.0.0.0` (`HTTP_ADDR` / `WS_ADDR`; set `127.0.0.1` for localhost-only) |
| Beacon HTTP / gRPC | `127.0.0.1` (`BEACON_HTTP_HOST` / `BEACON_GRPC_HOST`) |
| P2P ports | `30303` / `13000` / `12000` (`GETH_P2P_PORT`, `BEACON_P2P_TCP_PORT`, `BEACON_P2P_UDP_PORT`) |
| Geth IPC | disabled (`--ipcdisable`; admin APIs are not on the host filesystem) |
| Geth cache | `1024` MB (`GETH_CACHE`) |
| File descriptors | `nofile` 65535 (container ulimit) |
| Checkpoint sync | `https://checkpoint.pulsechain.com` (trusted third party; same as official docs) |
| Restart policy | `unless-stopped` |
| Stop grace period | `5m` |
| Geth healthcheck | `geth attach --exec eth.chainId` against local HTTP RPC (beacon does not wait on it) |
| Container logs | json-file, 50 MB × 5 files |
| Networking | `host` (aligned with official examples; simplifies P2P) |

Optional variables (`DATA_DIR`, ports, image pins) are documented in `.env.example` and are read by both Compose and the helper scripts.
Optional variables (`DATA_DIR`, bind addresses, ports, image pins) are documented in `.env.example` and are read by both Compose and the helper scripts. Changing `DATA_DIR` after the first sync starts a new empty node at the new path; the old chain data stays where it was.

---

Expand All @@ -357,10 +363,11 @@ Optional variables (`DATA_DIR`, ports, image pins) are documented in `.env.examp
| Issue | Suggested action |
|-------|------------------|
| Docker permission denied | Log out and back in after install (docker group membership), or prefix commands with `sudo`. On Omarchy this is common until the session picks up the `docker` group. |
| `address already in use` / crash loop | Another node is using ports 8545, 8546, 3500, 4000, or 8551. Stop the other process or change ports in `docker-compose.yml` |
| Installer asks about Docker CE | On Ubuntu/Debian, replacing `docker.io` / distro Compose can stop existing containers. Confirm only if you want Docker CE, or install Docker yourself and re-run. Unattended: `PULSE_ALLOW_DOCKER_CE=1`. |
| `address already in use` / crash loop | Another node is using ports 8545, 8546, 3500, 4000, or 8551. Stop the other process or change ports in `.env` |
| Beacon cannot find execution client | Confirm both containers are running and that `/blockchain/jwt.hex` exists and is shared by both |
| JWT / `401 Unauthorized` to execution | Ensure only one execution client is on port 8551 and both services use the same `/blockchain/jwt.hex`. The file must be 64 hex characters with **no newline**. Keep the host clock in sync (NTP / `timedatectl`); JWT `iat` skew also returns 401. |
| Wallet cannot connect | Verify LAN IP from `./status.sh` (not `hostname -I` / docker0), same network, host firewall rules; test `curl` against `127.0.0.1:8545` on the node. `./restart.sh` after compose edits (it recreates containers). |
| Wallet cannot connect | Verify LAN IP from `./status.sh` (not `hostname -I` / docker0), same network, host firewall rules; test `curl` against `127.0.0.1:8545` on the node. If you set `HTTP_ADDR=127.0.0.1`, only wallets on this machine work. `./restart.sh` after `.env` or compose edits (it recreates containers). |
| `./restart.sh` did nothing (old versions) | Current `restart.sh` uses `docker compose up -d --force-recreate`. Upgrade the scripts if an older copy only ran `up -d`. |
| Disk space pressure | Full nodes grow over time — monitor free space and use a large SSD |
| Slow sync | Prefer NVMe storage, adequate RAM, and open P2P ports where practical. On 16 GB hosts, try `GETH_CACHE=512` in `.env` if the machine swaps. |
Expand Down
24 changes: 20 additions & 4 deletions common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ GETH_CONTAINER="pulse-geth"
BEACON_CONTAINER="pulse-beacon"

# Env keys interpolated by docker-compose.yml / passed through sudo.
COMPOSE_ENV_KEYS="DATA_DIR,HTTP_PORT,WS_PORT,BEACON_HTTP_PORT,BEACON_GRPC_PORT,BEACON_HTTP_HOST,BEACON_GRPC_HOST,GETH_IMAGE,BEACON_IMAGE,GETH_CACHE"
COMPOSE_ENV_KEYS="DATA_DIR,HTTP_PORT,WS_PORT,HTTP_ADDR,WS_ADDR,BEACON_HTTP_PORT,BEACON_GRPC_PORT,BEACON_HTTP_HOST,BEACON_GRPC_HOST,GETH_IMAGE,BEACON_IMAGE,GETH_CACHE,GETH_P2P_PORT,BEACON_P2P_TCP_PORT,BEACON_P2P_UDP_PORT"

_trim() {
local s="$1"
Expand Down Expand Up @@ -100,15 +100,28 @@ load_dotenv "${_COMMON_DIR}/.env"
[[ -n "${DATA_DIR:-}" ]] || DATA_DIR=/blockchain
[[ -n "${HTTP_PORT:-}" ]] || HTTP_PORT=8545
[[ -n "${WS_PORT:-}" ]] || WS_PORT=8546
[[ -n "${HTTP_ADDR:-}" ]] || HTTP_ADDR=0.0.0.0
[[ -n "${WS_ADDR:-}" ]] || WS_ADDR=0.0.0.0
[[ -n "${BEACON_HTTP_PORT:-}" ]] || BEACON_HTTP_PORT=3500
[[ -n "${BEACON_GRPC_PORT:-}" ]] || BEACON_GRPC_PORT=4000
[[ -n "${BEACON_HTTP_HOST:-}" ]] || BEACON_HTTP_HOST=127.0.0.1
[[ -n "${BEACON_GRPC_HOST:-}" ]] || BEACON_GRPC_HOST=127.0.0.1
[[ -n "${GETH_CACHE:-}" ]] || GETH_CACHE=1024
[[ -n "${GETH_P2P_PORT:-}" ]] || GETH_P2P_PORT=30303
[[ -n "${BEACON_P2P_TCP_PORT:-}" ]] || BEACON_P2P_TCP_PORT=13000
[[ -n "${BEACON_P2P_UDP_PORT:-}" ]] || BEACON_P2P_UDP_PORT=12000
[[ -n "${GETH_IMAGE:-}" ]] || GETH_IMAGE="${GETH_IMAGE_PINNED}"
[[ -n "${BEACON_IMAGE:-}" ]] || BEACON_IMAGE="${BEACON_IMAGE_PINNED}"
export DATA_DIR HTTP_PORT WS_PORT BEACON_HTTP_PORT BEACON_GRPC_PORT
export BEACON_HTTP_HOST BEACON_GRPC_HOST GETH_CACHE GETH_IMAGE BEACON_IMAGE
export DATA_DIR HTTP_PORT WS_PORT HTTP_ADDR WS_ADDR BEACON_HTTP_PORT BEACON_GRPC_PORT
export BEACON_HTTP_HOST BEACON_GRPC_HOST GETH_CACHE GETH_P2P_PORT
export BEACON_P2P_TCP_PORT BEACON_P2P_UDP_PORT GETH_IMAGE BEACON_IMAGE

wallet_rpc_is_localhost() {
case "${HTTP_ADDR}" in
127.0.0.1|localhost) return 0 ;;
*) return 1 ;;
esac
}

# Cache how we talk to Docker so we do not run `docker info` on every call.
_DOCKER_MODE=""
Expand Down Expand Up @@ -276,10 +289,13 @@ os_is_debian_family() {
}

# Omarchy (https://omarchy.org) is Arch-based. Stock images still report ID=arch,
# so also look for Omarchy tools and install paths.
# so also look for Omarchy tools and install paths — but only on Arch-family hosts.
# Leftover ~/.local/share/omarchy on Ubuntu must not steal the Debian Docker CE path.
os_is_omarchy() {
local os_id="${1:-}"
local id_like="${2:-}"
[[ "${os_id}" == "omarchy" ]] && return 0
os_is_arch_family "${os_id}" "${id_like}" || return 1
command -v omarchy-pkg-add >/dev/null 2>&1 && return 0
command -v omarchy >/dev/null 2>&1 && return 0
[[ -f /etc/profile.d/omarchy.sh ]] && return 0
Expand Down
27 changes: 21 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,34 @@ services:
- --authrpc.port=8551
- --authrpc.vhosts=localhost
- --http
- --http.addr=0.0.0.0
- --http.addr=${HTTP_ADDR:-0.0.0.0}
- --http.port=${HTTP_PORT:-8545}
- --http.api=eth,net,web3,txpool
- --http.corsdomain=*
- --http.vhosts=*
- --ws
- --ws.addr=0.0.0.0
- --ws.addr=${WS_ADDR:-0.0.0.0}
- --ws.port=${WS_PORT:-8546}
- --ws.api=eth,net,web3,txpool
- --ws.origins=*
- --maxpeers=150
- --discovery.port=30303
- --port=30303
- --discovery.port=${GETH_P2P_PORT:-30303}
- --port=${GETH_P2P_PORT:-30303}
# Image is alpine + geth only. attach-to-HTTP is the available readiness probe.
# Beacon still starts as soon as the container exists (depends_on without
# service_healthy) so a probe mismatch cannot dead-lock first boot.
healthcheck:
test:
- CMD
- geth
- attach
- --exec
- eth.chainId
- http://127.0.0.1:${HTTP_PORT:-8545}
interval: 20s
timeout: 10s
retries: 15
start_period: 45s

# ---------------------------------------------------------------------------
# Consensus client — Prysm-Pulse Beacon Chain
Expand Down Expand Up @@ -93,6 +108,6 @@ services:
# gRPC API (localhost; not needed for MetaMask)
- --rpc-host=${BEACON_GRPC_HOST:-127.0.0.1}
- --rpc-port=${BEACON_GRPC_PORT:-4000}
- --p2p-tcp-port=13000
- --p2p-udp-port=12000
- --p2p-tcp-port=${BEACON_P2P_TCP_PORT:-13000}
- --p2p-udp-port=${BEACON_P2P_UDP_PORT:-12000}
- --p2p-max-peers=150
Loading
Loading