Skip to content

Deployment

RPC Plane is a single binary (and a single ~20 MB container image) with no database and no external dependencies. That makes it easy to run almost anywhere — as a standalone service, or as a sidecar next to your app.

It is an internal component: your app talks to it, and it talks to your RPC providers. It should never be exposed to the public internet. Keep its ports on loopback, a private network, or a ClusterIP.

Every manifest below references provider keys as ${HELIUS_API_KEY}-style variables. Supply them as environment variables and RPC Plane expands them when it loads the config. Run rpc-plane check afterwards to confirm none expanded to an empty string.

Copy-paste-ready versions of everything here live in examples/deploy/.

Port or socket?

The one decision that shapes every deployment is how RPC Plane listens:

TCP port (0.0.0.0:9400) Unix socket (/run/rpc-plane/proxy.sock)
Reachable from any host/pod over the network same host or pod only
Overhead loopback TCP stack none — kernel socket path
Network exposure a real port (firewall it) no port at all
Use when proxy and clients are on different hosts proxy and client are co-located
Examples here Compose, K8s Deployment, Nomad K8s sidecar, systemd on the same box

Co-located deployments (a sidecar, or the proxy and app on one VM) should prefer a Unix socket — it removes the loopback hop entirely and exposes no port. The trade-off: some Solana client libraries only dial TCP URLs. The Unix socket guide covers the connection details, the permission model, and the measured latency win.

The metrics endpoint (metrics_listen) always binds to TCP, regardless of how the proxy listens — so Prometheus can scrape it either way.

Docker

docker run -d --name rpc-plane \
  -v $(pwd)/rpc-plane.toml:/etc/rpc-plane.toml:ro \
  -e HELIUS_API_KEY=... \
  -e QUICKNODE_API_KEY=... \
  -p 9400:9400 -p 9401:9401 \
  ghcr.io/rpcplane/rpc-plane:latest

The image expects the config at /etc/rpc-plane.toml and runs rpc-plane -c /etc/rpc-plane.toml run by default. Inside a container the config must bind 0.0.0.0, not 127.0.0.1, for the published port to work:

[server]
listen         = "0.0.0.0:9400"
metrics_listen = "0.0.0.0:9401"

Docker Compose

A minimal stack — one service, keys from a .env file:

services:
  rpc-plane:
    image: ghcr.io/rpcplane/rpc-plane:latest
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./rpc-plane.toml:/etc/rpc-plane.toml:ro
    ports:
      - "9400:9400"   # proxy
      - "9401:9401"   # metrics
    healthcheck:
      # The runtime image has no curl; use the built-in status command.
      test: ["CMD", "rpc-plane", "-c", "/etc/rpc-plane.toml", "status"]
      interval: 15s
      timeout: 5s
      retries: 3
cp .env.example .env   # add your keys
docker compose up -d
curl http://localhost:9400/health | jq

The repo ships an observability overlay that adds Prometheus and Grafana with the RPC Plane dashboard auto-provisioned:

docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d
# Grafana → http://localhost:3000   Prometheus → http://localhost:9090

Full files: examples/deploy/docker-compose/.

Kubernetes

Shared Deployment (TCP)

Run the proxy as its own Deployment + ClusterIP Service when several apps share one proxy. They reach it at http://rpc-plane:9400. Keys go in a Secret, config in a ConfigMap; both probes hit /health:

        - name: rpc-plane
          image: ghcr.io/rpcplane/rpc-plane:latest
          envFrom:
            - secretRef: { name: rpc-plane-keys }
          ports:
            - { name: rpc, containerPort: 9400 }
            - { name: metrics, containerPort: 9401 }
          readinessProbe:
            httpGet: { path: /health, port: rpc }
          livenessProbe:
            httpGet: { path: /health, port: rpc }
          volumeMounts:
            - name: config
              mountPath: /etc/rpc-plane.toml
              subPath: rpc-plane.toml

Full manifest (Secret + ConfigMap + Deployment + Service, with a hardened securityContext): kubernetes/deployment.yaml.

Sidecar (Unix socket)

Co-locate the proxy in your app's pod and share a Unix socket through an emptyDir volume — no proxy port, no loopback overhead:

      securityContext:
        runAsUser: 1000      # same UID in both containers so the app
        runAsGroup: 1000     # can connect to the socket the proxy creates
        fsGroup: 1000
      containers:
        - name: app
          image: your-app:latest
          volumeMounts:
            - { name: rpc-plane-socket, mountPath: /run/rpc-plane }
        - name: rpc-plane
          image: ghcr.io/rpcplane/rpc-plane:latest
          # listen = "/run/rpc-plane/proxy.sock" in the mounted config
          ports:
            - { name: metrics, containerPort: 9401 }
          # httpGet can't probe a socket — probe the always-TCP metrics port.
          livenessProbe:
            httpGet: { path: /metrics, port: metrics }
          volumeMounts:
            - { name: config, mountPath: /etc/rpc-plane.toml, subPath: rpc-plane.toml }
            - { name: rpc-plane-socket, mountPath: /run/rpc-plane }
      volumes:
        - { name: rpc-plane-socket, emptyDir: {} }

If your Solana client can't speak HTTP over a Unix socket, set listen = "127.0.0.1:9400" instead and point the app at http://localhost:9400 — containers in a pod share localhost. Full manifest (with that variant in the comments): kubernetes/sidecar.yaml.

Metrics

If you run the Prometheus Operator, scrape the metrics port with a ServiceMonitor.

Nomad

The docker driver with a template stanza for the config and a Consul service with an HTTP health check:

    task "rpc-plane" {
      driver = "docker"
      config {
        image = "ghcr.io/rpcplane/rpc-plane:latest"
        ports = ["proxy", "metrics"]
        args  = ["-c", "/local/rpc-plane.toml", "run"]
      }
      env { HELIUS_API_KEY = "..." }   # or pull from Vault
      template {
        destination = "local/rpc-plane.toml"
        # In an HCL heredoc, escape ${ as $$ so Nomad leaves the env ref for
        # RPC Plane to expand: url = "...?api-key=$${HELIUS_API_KEY}"
        data = <<-EOF
          [server]
          listen = "0.0.0.0:9400"
          ...
        EOF
      }
    }

Full job (network ports, Consul service + health check, Vault example): nomad/rpc-plane.nomad.hcl.

systemd (bare metal / VM)

Run the binary directly under a hardened unit:

[Service]
Type=simple
User=rpc-plane
ExecStart=/usr/local/bin/rpc-plane -c /etc/rpc-plane/rpc-plane.toml run
EnvironmentFile=/etc/rpc-plane/rpc-plane.env
Restart=on-failure

# Unix socket mode: set listen = "/run/rpc-plane/proxy.sock" and uncomment —
# systemd creates /run/rpc-plane owned by the service user.
# RuntimeDirectory=rpc-plane

NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

Full unit (complete hardening set) and EnvironmentFile template: systemd/.

Health checks

Endpoint Port Always TCP? Use for
/health proxy (9400 or socket) no — follows listen readiness/liveness on TCP deployments; rich JSON of per-provider health
/metrics metrics_listen (9401) yes liveness when the proxy is on a Unix socket; Prometheus scraping

On a Unix socket, httpGet probes can't reach /health, so probe /metrics (always TCP) for liveness. The full health JSON is still served on the socket and readable from a co-located container:

curl --unix-socket /run/rpc-plane/proxy.sock http://localhost/health | jq