shinyctl

v0.1.0 · Linux and macOS · amd64 and arm64 · MIT

Shiny apps that stay up.

shinyctl runs each R Shiny app as several copies behind a built-in load balancer, with a warm spare ready to take over, and redeploys without cutting anyone off. One binary. No Docker, no root.

$ curl -fsSL https://raw.githubusercontent.com/spriyansh/shinyctl/main/install.sh | sh

Or with Go 1.26.10: go install github.com/spriyansh/shinyctl@v0.1.0. Other ways are on the install page.

~ $ curl -fsSL https://raw.githubusercontent.com/spriyansh/shinyctl/main/install.sh | sh
Downloading shinyctl v0.1.0 for darwin/arm64
Installed shinyctl v0.1.0 to /Users/you/.local/bin/shinyctl
~ $ shinyctl version
v0.1.0

One binary, checked against the release's checksums, copied into ~/.local/bin. Nothing needs root.

Recorded from shinyctl v0.1.0 running the example app.

What you just watched

The same session, said plainly. The numbers come from the release test on a Raspberry Pi 5, with requests running the whole time.

One port, several copies

Each app runs as replicas active copies plus warm-replicas spares, all behind the one port in your manifest. A cookie keeps each visitor on the same copy, so Shiny sessions work.

default: 1 active copy, 1 warm spare · cookie lb_<port>

A copy dies, the page keeps answering

The load balancer sends the next request to the warm spare at once. shinyctl restarts the dead copy, with a backoff, and it becomes the new spare.

spare took over in 0.04 s · 0 of 414 requests failed

Redeploys that drain

restart, or an apply that changes how the app starts, brings up new copies first. Old copies keep their visitors until those leave or drain-timeout passes. A new version that fails to start never takes over.

drain-timeout 15m by default · --now skips the wait

Upgrades and reboots

A new shinyctl starts its load balancer next to the old one and takes over without refusing a connection. On Linux, shinyctl startup adds a systemd user unit, so apps come back after a reboot.

two upgrades under load · 0 of 473 and 0 of 470 failed

How it works

One binary runs as three things: the shinyctl command you type, shinyctl-api in the background, and the load balancer in front of your apps. If shinyctl-api stops, the load balancer and the copies keep serving, and a new one adopts them. More on how it works.

How shinyctl is put together Visitors reach the load balancer on the app's port, which sends each request to one of the app's R copies. You talk to shinyctl-api over a socket; it records state in state.json, pushes the copies table to the load balancer, and starts and stops the copies. Visitors browsers, on :8765 requests Load balancer one public port per app sticky cookie lb_8765 · WebSockets R copies · 127.0.0.1:20000–29999 copy 3 active takes new visitors copy 4 warm ready spare, takes over at once copy 2 draining keeps its visitors, then stops shinyctl-api runs the cycle every 5 s and on every request or exit copies table, swapped in memory starts, stops, watches exits You apply · status · restart api.sock records state.json processes, start times, crash counts How shinyctl is put together Visitors reach the load balancer, which sends each request to one of the app's R copies. You talk to shinyctl-api, which records state in state.json, pushes the copies table to the load balancer, and starts and stops the copies. Visitors browsers, on :8765 requests Load balancer one public port per app sticky cookie lb_8765 · WebSockets to one copy R copies · 127.0.0.1:20000–29999 copy 3 active takes new visitors copy 4 warm ready spare copy 2 draining keeps its visitors, then stops You apply · status · restart api.sock shinyctl-api runs the cycle every 5 s and on every request or exit records state.json processes, start times, crash counts starts and stops copies table, swapped in memory

Docs

Start with the install page. The reference for every manifest field, command and status column is in the README.