Operations

This page covers running p2proxy as a long-lived service.

systemd

A minimal unit file:

# /etc/systemd/system/p2proxy.service
[Unit]
Description=Bitping p2proxy
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Environment=BITPING_API_KEY=<your-key>
ExecStart=/usr/local/bin/p2proxy --no-ui --config /etc/p2proxy/Config.yaml
Restart=on-failure
RestartSec=5
DynamicUser=yes
StateDirectory=p2proxy

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now p2proxy
journalctl -u p2proxy -f

DynamicUser=yes runs the daemon as a transient unprivileged user. StateDirectory=p2proxy gives it /var/lib/p2proxy for the sticky-peer file.

WireGuard mode needs no extra privileges — the tunnel is terminated in userspace, so there’s no NET_ADMIN, no TUN device and no kernel module to load. The default 51820 is above 1024, so the unit above runs it unchanged.

If you need to bind a listener below port 1024, either pick a high port (recommended) or grant the binary the capability:

sudo setcap 'cap_net_bind_service=+ep' "$(which p2proxy)"

Docker

The official image is ghcr.io/bitpingapp/p2proxy:latest. NO_UI=true is set by default so the daemon doesn’t try to draw a TUI without a TTY.

docker run -d --name p2proxy \
  -p 1080:1080 \
  -p 9091:9091 \
  -e BITPING_API_KEY=<your-key> \
  -v "$PWD/Config.yaml:/app/Config.yaml:ro" \
  ghcr.io/bitpingapp/p2proxy:latest

You need to expose:

  • Every per-server port (-p 1080:1080, etc.) so clients on the host or network can reach it. A WireGuard server is UDP — publish it as -p 51820:51820/udp, and set host: "0.0.0.0" on that server so Docker’s port forwarder can reach the listener.
  • The metrics port (-p 9091:9091) if you want to scrape Prometheus metrics from outside the container.
  • If you pin a fixed libp2p port via listen_addrs, publish that port too — not strictly required, but it improves direct-vs-relay connectivity.

docker-compose

A reference docker-compose.yml ships in the repo. Typical shape:

services:
  p2proxy:
    image: ghcr.io/bitpingapp/p2proxy:latest
    restart: unless-stopped
    environment:
      BITPING_API_KEY: $BITPING_API_KEY
    ports:
      - "1080:1080"
      - "9091:9091" # metrics
    volumes:
      - ./Config.yaml:/app/Config.yaml:ro

p2proxy picks its own libp2p listen ports by default, so there’s nothing fixed to publish. If you set listen_addrs to a fixed port, publish that port too (it improves direct-vs-relay connectivity).

Prometheus metrics

p2proxy exposes Prometheus metrics on http://localhost:9091/metrics by default (set metrics_port to change it). Useful series:

Series Type Notes
p2proxy_peers_connected gauge Connected libp2p peers.
p2proxy_sessions_active gauge In-flight SOCKS5 sessions.
p2proxy_socks_connections_total, p2proxy_sessions_initialized_total counter Accepted / fully-established sessions.
p2proxy_upload_bytes_total, p2proxy_download_bytes_total counter Bytes relayed each direction.
p2proxy_session_errors_total{stage} counter Session failures by stage (handshake / request / peer-connection / data-transfer).
p2proxy_stream_opened_total, p2proxy_stream_acquire_failed_total counter Peer-stream opens / failures.
p2proxy_ping_rtt_seconds histogram Peer round-trip latency.
p2proxy_socks_rejected_no_peer_total counter Sessions rejected — no exit peer available.

WireGuard servers add:

Series Type Notes
p2proxy_wg_tcp_flows_active, p2proxy_wg_udp_flows_active gauge Flows currently bridged out of the tunnel.
p2proxy_wg_flows_accepted_total, p2proxy_wg_tcp_flows_total, p2proxy_wg_udp_flows_total counter Flows accepted from the tunnel.
p2proxy_wg_udp_peer_incapable_total, p2proxy_wg_udp_fallback_dns_total counter Exit peers that only speak the TCP forwarder, and DNS queries retried over TCP as a result.
p2proxy_wg_udp_dropped_total, p2proxy_wg_udp_dropped_full_total, p2proxy_wg_udp_at_cap_total counter UDP discarded — unsupported by the exit, a full queue, or the session cap.
p2proxy_wg_decap_errors_total counter Datagrams that failed to decrypt. A steady rate means a key or preshared-key mismatch, or unsolicited internet traffic hitting the port.
p2proxy_wg_spoofed_source_total counter Packets whose source address wasn’t in the sending peer’s allowed_ips.
p2proxy_wg_egress_unrouted_total counter Return packets with no matching peer allowed_ips — usually a too-narrow CIDR.
p2proxy_wg_rejected_no_peer_total, p2proxy_wg_udp_no_peer_total counter Flows dropped because no exit peer was available.

A minimal scrape config:

scrape_configs:
  - job_name: p2proxy
    static_configs:
      - targets: ["localhost:9091"]

For dashboards, p2proxy_session_errors_total{stage} is the series you’ll watch most — a rising rate is the leading indicator of peer trouble.

Upgrading

  • Homebrew: brew upgrade --cask BitpingApp/tap/p2proxy
  • Debian/Ubuntu/RHEL/Alpine: download the new package from the releases page and reinstall.
  • Docker: pull the new tag and recreate the container — docker compose pull && docker compose up -d.
  • From source: git pull && cargo build --release -p p2proxy.

Restart the daemon to pick up the new binary. Config.yaml is read at startup, so config changes also require a restart.

Peer identity and state on disk

p2proxy’s libp2p identity is derived deterministically from your API key, so there’s no keypair file to back up or lose — the same key always yields the same peer ID.

The one file it does write is sticky_peers.json (the remembered exit peer per listen port), relative to the working directory. Run separate instances from separate working directories, and under systemd give the unit a WorkingDirectory=/var/lib/p2proxy to go with StateDirectory=p2proxy. Deleting it just means the next start re-discovers an exit.

Next