Installation
Install portunus-standalone with Docker or the one-click binary installer, then verify traffic flows.
portunus-standalone is a self-contained TCP/UDP forwarder configured
entirely from a TOML file. It uses the same data-plane code as
portunus-client but requires no running portunus-server — suitable for
single-host deployments, edge devices, and any host where you don't want
to run a control-plane server.
Install
Two supported install paths. Pick Docker for container hosts and One-click installer for plain Linux servers (systemd or OpenRC).
Docker
The GHCR image ghcr.io/zingerlittlebee/portunus-standalone:<version> is
published on every release (multi-arch: linux/amd64, linux/arm64).
# 1. write your forwarding rules (the container mounts this file read-only):
cat > portunus.toml <<'EOF'
[[rule]]
name = "ssh"
protocol = "tcp"
listen_port = 2222
target = "10.0.0.5:22"
EOF
chmod 0644 portunus.toml # the container runs as UID 65532 and must read it
# 2. Run the container:
docker run -d --network host \
--name portunus-standalone \
--restart unless-stopped \
-v "$PWD/portunus.toml:/etc/portunus/standalone.toml:ro" \
ghcr.io/zingerlittlebee/portunus-standalone:latestNotes:
- Pin a specific version in production (e.g.
:X.Y.Zrather than:latest). - The container runs as UID 65532 (
nonroot). The host file must be readable by that UID —chmod 0644 portunus.tomlis the simplest fix. If you geterror: io error: Permission denied (os error 13), this is why. --network hostlets port-range and arbitrary-port rules work without per-port mapping. On Docker Desktop or when host networking is unavailable, switch to bridge mode and enumerate ports — seecontrib/docker-compose.yml.- The path inside the container (
/etc/portunus/standalone.toml) is fixed by the image's defaultCMD. The host path can be anywhere. docker composeand Kubernetes manifests live incrates/portunus-standalone/contrib/.
One-click installer (binary + service)
For Linux servers without Docker. The installer never writes a config — the binary exits if one is missing — so create the config first, then install and it starts automatically:
# 1. write your forwarding rules
sudo sh -c 'mkdir -p /etc/portunus && cat > /etc/portunus/standalone.toml' <<'EOF'
[[rule]]
name = "ssh"
protocol = "tcp"
listen_port = 2222
target = "10.0.0.5:22"
EOF
# 2. install + start
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/Portunus/main/scripts/install.sh | sudo sh -s -- standaloneThe installer:
- Detects OS/arch, downloads the latest release, verifies its SHA-256.
- Installs the binary to
/usr/local/bin/portunus-standalone(mode0755). - Creates the
portunussystem user/group if missing. - Fixes ownership/permissions on your
/etc/portunus/standalone.toml(mode0640, ownerroot:portunus) so the service user can read it. It does not create the file — that is yours to author. Point at a different file with--config /path/to/your.toml. - Installs a hardened service unit — on systemd at
/etc/systemd/system/portunus-standalone.service(LimitNOFILE=65535,AmbientCapabilities=CAP_NET_BIND_SERVICE,ProtectSystem=strict,NoNewPrivileges=true); on Alpine an OpenRCsupervise-daemonscript in/etc/init.d. - Enables and starts the service — because the config exists. If you
install before creating the config, it lays down the unit but does not
start it and prints how to create the config and start;
--no-servicealways skips the start.
Validate or manage it afterwards:
portunus-standalone --check --config /etc/portunus/standalone.toml
sudo systemctl restart portunus-standalone # after editing the config
sudo systemctl status portunus-standalone --no-pagerRunning sh install.sh with no arguments opens an interactive menu —
pick option 3 for the standalone role.
Source build
For a source build:
git clone https://github.com/ZingerLittleBee/Portunus.git
cd Portunus
cargo build --release -p portunus-standalone
sudo install -m 0755 target/release/portunus-standalone /usr/local/bin/Verifying the installation
Confirm the rule is active and traffic actually flows.
systemd install:
systemctl status portunus-standalone --no-pager
journalctl -u portunus-standalone -n 20 --no-pager
ss -ltn | grep 2222 # check the listen port is boundDocker install:
docker logs portunus-standalone | head -20
docker exec portunus-standalone ss -ltn 2>/dev/null || ss -ltn | grep 2222You should see one event per rule on startup:
{"event":"standalone.rlimit_nofile","soft":65535,"hard":65535,…}
{"event":"rule.activated","rule_id":"…","listen_port":2222,…,"target":"10.0.0.5:22-22"}End-to-end TCP echo (replace 7777/7778 with your rule's ports; run the upstream first so the connect succeeds):
sudo apt-get install -y socat
nohup socat TCP-LISTEN:7778,reuseaddr,fork EXEC:/bin/cat </dev/null >/tmp/up.log 2>&1 &
( echo "verify-payload"; sleep 1 ) | socat -t 3 - TCP:127.0.0.1:7777
# → verify-payloadThe forwarder logs rule.conn_closed with non-zero bytes_in/bytes_out
for each closed connection.