shinyctl

Running in production

shinyctl serves plain HTTP on one machine. To run apps for real users, put HTTPS in front, start shinyctl at boot, and leave room for the copies in memory.

Checklist

  • Set host: 127.0.0.1 in each manifest, and put HTTPS in front.
  • On Linux, start at boot, and enable lingering so logging out doesn't stop your apps.
  • Check there is memory for every copy, plus the extra copy a redeploy runs for a moment.
  • Copy the example's reload script into your app's UI, so open pages recover after a crash or a redeploy (why).

HTTPS and the internet

Set host: 127.0.0.1 in the app's manifest, so only this machine can reach its port, and put one of these in front.

nginx

With a certificate from Let's Encrypt, for example through certbot. Shiny needs the WebSocket upgrade and a long read timeout:

map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server {
    listen 443 ssl;
    server_name shiny.example.org;
    ssl_certificate     /etc/letsencrypt/live/shiny.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/shiny.example.org/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_read_timeout 1d;
        proxy_buffering off;
    }
}

To serve the app under a path, use location /myapp/ { proxy_pass http://127.0.0.1:8080/; ... } with both slashes, so the app sees /, and send the address without the final slash to the one with it:

location = /myapp { return 301 /myapp/; }

Cloudflare Tunnel

Needs no open port. For a quick try:

cloudflared tunnel --url http://127.0.0.1:8080

For a permanent address, create a named tunnel (see Cloudflare's documentation). WebSockets work as they are.

Tailscale

Serves the app inside your tailnet over HTTPS, or to the internet with Funnel (see Serve and Funnel):

tailscale serve --bg 8080       # HTTPS inside your tailnet
tailscale funnel --bg 8080      # public

What shinyctl does with them

A web server on the same machine in front of the port is trusted for X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, so the app sees the visitor's address and that the request was HTTPS. Those headers from anywhere else are replaced. The cookie that keeps a visitor on a copy becomes Secure; SameSite=None when the server in front says the request was https.

Start at boot (Linux)

Nothing restarts shinyctl-api after a reboot by itself. shinyctl startup prints a systemd user unit for it, and on standard error the commands to install it:

mkdir -p ~/.config/systemd/user && mkdir -p -m 700 ~/.shinyctl
shinyctl startup > ~/.config/systemd/user/shinyctl.service
systemctl --user daemon-reload
pkill -u "$USER" -f '(^|/)shinyctl serve$'
systemctl --user enable --now shinyctl
loginctl enable-linger "$USER"
  • The pkill line stops a shinyctl-api you started by hand, which would hold the socket. Its apps and the load balancer keep running, and the service adopts them.
  • The unit uses the PATH of the shell you ran startup in, plus Rscript's folder. Run startup again after moving shinyctl or R.
  • Lingering keeps your user's services running after you log out. Without it, logging out can stop everything; shinyctl-api warns about it in api.log.
  • On macOS, run shinyctl serve -d after a reboot.

Memory

Each copy is a full R process, and with the defaults every app runs two: one active copy and one warm spare. A small app takes about 110 to 140 MB per copy on a Raspberry Pi 5, and the load balancer about 40 MB in all. status shows the total.

During a redeploy both versions run side by side: for the few seconds R takes to start, the peak is the old copies plus replicas new ones, which is 3 R processes with the defaults. Old copies that still have visitors run until drain-timeout. On a small machine, set warm-replicas: 0, a shorter drain-timeout, or use --now.

Logs

shinyctl logs -f shinyctl.yaml             # the last 100 lines of every copy
shinyctl logs -f shinyctl.yaml --copy 2    # one copy
shinyctl logs -f shinyctl.yaml --follow    # keep printing until Ctrl-C
shinyctl logs --api                        # shinyctl-api and the load balancer

The files are under ~/.shinyctl/logs/, and rotate at 1 GiB, keeping one previous file.

Stopping and uninstalling

shinyctl-api and the load balancer keep running after you remove the binary, and the load balancer keeps every app's port open. To stop everything, delete the apps, stop the service if you installed one, then stop the two processes:

shinyctl delete -f shinyctl.yaml                       # for each app
systemctl --user disable --now shinyctl                # Linux, if you installed the service
pkill -u "$USER" -f '(^|/)shinyctl (serve|proxy --config .*)$'

Then remove shinyctl itself:

rm -f ~/.config/systemd/user/shinyctl.service && systemctl --user daemon-reload   # Linux, with the service
rm -f ~/.local/bin/shinyctl
rm -rf ~/.shinyctl

Limits to know

  • One machine. Copies of an app run on the machine shinyctl runs on.
  • Copies don't share memory. Anything a session keeps in R lives in its copy.
  • The load balancer is in the path of every request. If it dies, shinyctl-api restarts it within about a second, and the apps are unreachable until then.
  • Apps keep running if shinyctl-api dies, but nothing restarts a crashed copy until it is back. Any apply, or shinyctl serve -d, brings it back; on Linux the service restarts it by itself.
  • stop signals only the main R process. Workers the app started, for example through callr or future, can outlive it.
  • A drain ends at drain-timeout. Sessions still open then are cut, and a tab left open counts as a session.
  • A new version that starts but fails on its page still takes over: a copy counts as ready once R accepts connections.

The full list is in the README.