Deployment guide

ShinyProxy

Overview

ShinyProxy is an open source deployment server for data science web applications, built by Open Analytics. It puts a sign-in page in front of your applications instead of leaving an analytics app running on a shared port for anyone to reach. It starts a dedicated container for each user who opens an app and proxies that container back through a single URL. When the user stops the app or the session goes idle, the container is destroyed and its memory returns to the machine.

You declare applications in one configuration file: an identifier, a display name, the container image to run and the port it listens on. The unit of deployment is a container image, so the same server publishes R Shiny apps, Python Dash and Streamlit apps, Jupyter notebooks and anything else that serves HTTP, side by side and each with its own dependencies.

The SecureAnyCloud image runs the unmodified ShinyProxy 3.2.4 release on OpenJDK 21 on a hardened Ubuntu 24.04 LTS base, patched at build time. It uses the Docker engine as its container backend. nginx sits in front on port 80 and handles both HTTP and the WebSocket that interactive apps depend on.

The image ships unprovisioned and unreachable. ShinyProxy's reference configuration documents demo users such as jack and jeff with the password password. None of them exists anywhere in this image. No ShinyProxy configuration file exists in the image at all, only a template, so there is no credential to find.

On the first boot of every VM, a one-shot service generates this VM's administrator password, renders the configuration from that template and starts ShinyProxy. Only then does it publish the site on port 80. Until that finishes, nothing is bound to port 80.

A small sample application written by SecureAnyCloud is baked into the image as a container. Your first sign-in launches a real container and renders a real dashboard instead of an empty app list. The sample is built from scratch at image build time from SecureAnyCloud's own source, and no third-party application image is redistributed with this product.

What you get:

  • ShinyProxy 3.2.4, the unmodified upstream release jar, pinned by SHA256 and run on OpenJDK 21
  • The Docker engine from the Ubuntu archive as the container backend, so security updates arrive through the normal channel
  • nginx on :80 reverse proxying ShinyProxy, including the WebSocket that interactive apps need
  • A per-VM administrator password generated on first boot and recorded in a root-only file
  • A sample analytics app written by SecureAnyCloud, baked in as a container image, so the appliance works out of the box
  • JVM heap and metaspace ceilings tuned so the server, the container engine and a launched app all fit Standard_B2s
  • Nothing bound to port 80 until first boot has provisioned and secured the instance
  • 24/7 SecureAnyCloud support

Key facts

  • Default SSH user: azureuser
  • Web interface: http://<vm-public-ip>/
  • Application port: 127.0.0.1:8080 (loopback only, reached through nginx)
  • Actuator port: 127.0.0.1:9090 (loopback only)
  • Credentials file: /root/shinyproxy-credentials.txt (mode 600, root only)
  • Configuration: /etc/shinyproxy/application.yml (rendered on first boot)
  • Sample app image: sac-sample-analytics:1.0
  • Recommended size: Standard_B2s

Prerequisites

Before you deploy, make sure you have an active Azure subscription, an SSH key pair, and a VNet and subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is the recommended starting point and is what the tuning in this image targets. Size up when you publish heavier applications or expect many concurrent sessions, because every signed-in user gets a container of their own.

In the network security group, allow inbound 22/tcp from your management network and 80/tcp so your users can reach ShinyProxy. Also allow 443/tcp once you terminate TLS on the VM.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource and search the Marketplace for ShinyProxy by SecureAnyCloud. Select Create.

On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80). Then choose Review + create and Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:shinyproxy-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name shinyproxy \
  --image sac1647283583153:shinyproxy-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open port 80 so your users can reach ShinyProxy. Run:

az vm open-port --resource-group <your-rg> --name shinyproxy --port 80

Step 3 - Confirm the services are running

Connect over SSH as azureuser. First boot takes well under a minute. Once it has finished, nginx binds port 80 and ShinyProxy is ready.

systemctl is-active docker shinyproxy nginx

You want three active lines. Confirm the runtime versions and that the container engine is up:

java -version 2>&1 | head -1; docker version --format 'Docker {{.Server.Version}}'; nginx -v 2>&1

Check which ports are listening. ShinyProxy and its actuator are on loopback, and only nginx faces the network:

ss -ltn | grep -E ':80|:8080|:9090'

If port 80 is not bound, first boot has not completed. This is deliberate and fails closed: ShinyProxy is never published before it has been provisioned and secured. Check with systemctl status shinyproxy-firstboot.service and journalctl -u shinyproxy-firstboot.service.

Step 4 - Retrieve this VM's administrator credentials

Every VM generates its own administrator password on first boot and writes it, with the sign-in address, to a root-only credentials file. Read it:

sudo cat /root/shinyproxy-credentials.txt

The image itself contains no configuration file and therefore no credential. First boot renders /etc/shinyproxy/application.yml from the shipped template, and both files are readable only by root and the service account:

sudo stat -c '%a %U:%G %n' /etc/shinyproxy/application.yml.tmpl /etc/shinyproxy/application.yml /root/shinyproxy-credentials.txt

Check for yourself that upstream's documented demo accounts do not exist here. This command counts how many times jack or jeff appear in the running configuration. The answer is 0:

sudo grep -c -iE '(^|[^A-Za-z])(jack|jeff)([^A-Za-z]|$)' /etc/shinyproxy/application.yml || true

Step 5 - Prove the whole thing works from the command line

The image ships a probe that exercises the entire product path end to end. The probe checks that:

  • An anonymous request for an app is refused.
  • Upstream's demo credentials are rejected.
  • This VM's password authenticates.
  • Opening the app starts a container and returns the app's own HTML through the proxy.
  • Stopping the app removes the container again.

Run the probe:

sudo /usr/local/sbin/shinyproxy-roundtrip.sh

List the baked sample app image that the probe launches, and confirm that no container is left behind afterward:

sudo docker images sac-sample-analytics; docker ps --filter ancestor=sac-sample-analytics:1.0

Step 6 - Sign in to ShinyProxy

Browse to http://<vm-public-ip>/. ShinyProxy opens on its sign-in page, and nothing is reachable without authenticating. Enter the shinyproxy.admin.user and shinyproxy.admin.pass values from Step 4.

After you sign in, the application catalog opens. Every app declared in the configuration appears here, and this image ships one to get you started.

Step 7 - Launch the sample application

Select SecureAnyCloud Sample Analytics. ShinyProxy starts the container for your session, waits for it to answer and then proxies it into the page. A container that did not exist a moment ago serves the dashboard you see. The signed-in user it reports is the identity ShinyProxy passed into that container.

The metric tabs across the top are ordinary links handled by the container, so clicking one sends a real request through the proxy into your session. Switching metric redraws the chart and the statistics.

Choose Stop app when you are finished. The container is destroyed immediately. If you close the tab instead, ShinyProxy notices the missing heartbeat and removes the container about a minute later.

Step 8 - Watch and manage running sessions

Select Admin in the toolbar for the operator view. Active Proxies lists every running session on the server with its owner, the application it is running, how long it has been up and the loopback endpoint ShinyProxy is proxying to, plus controls to inspect or stop any session.

Anyone in the admins group sees this view. The per-VM administrator created on first boot is in that group.

Step 9 - Publish your own application

An application is a container image plus a few lines of configuration. First make your image available to the Docker engine on this VM. Build it here, or run docker pull from a registry you trust:

sudo docker images --format '{{.Repository}}:{{.Tag}}'

Then add a spec under proxy.specs in /etc/shinyproxy/application.yml. Copy the pattern of the existing entry:

sudo sed -n '/^ specs:/,$p' /etc/shinyproxy/application.yml

A new entry looks like this. port is the port your application listens on inside its container. 3838 is the R Shiny convention, and Dash and Streamlit typically use 8050 and 8501:

- id: my-dashboard display-name: My Dashboard description: Quarterly reporting container-image: my-registry/my-dashboard:1.4.0 port: 3838 container-memory-limit: 512m access-groups: [admins]

Apply the change with sudo systemctl restart shinyproxy, then reload the catalog in your browser. Keep container-memory-limit set on every app, because it stops one user's session from taking the whole VM down.

Applications must serve pages using relative links so they work behind the proxy path. R Shiny does this by default. For Dash, set requests_pathname_prefix. For Streamlit, set --server.baseUrlPath.

Step 10 - Add users and groups

Users live under proxy.users in the same file, and access-groups on an app restricts it to named groups. To add a user, copy the existing user block and give the new user their own password and groups:

sudo sed -n '/^ users:/,/^ container-backend:/p' /etc/shinyproxy/application.yml | sed -E 's/(password: ).*/\1"<redacted>"/'

Restart ShinyProxy after editing. For more than a handful of accounts, point ShinyProxy at your existing identity provider instead. It supports LDAP, OpenID Connect, Keycloak, SAML and social login through the proxy.authentication setting, and the upstream configuration reference documents each one.

To change the generated administrator password, edit its password value and restart the service.

Step 11 - Storage, memory and sizing

The JVM runs with explicit ceilings instead of the defaults, because on a 4 GiB machine it shares memory with the Docker engine and with every app container it starts. Check the settings:

systemctl show -p MainPID --value shinyproxy | xargs -I{} sh -c 'tr "\0" " " < /proc/{}/cmdline'; echo; free -m | head -2
  • -Xmx640m (heap ceiling): leaves room for Docker and the app containers on Standard_B2s.
  • -XX:MaxMetaspaceSize=256m (class metadata ceiling): Spring Boot loads a lot of classes, and unbounded metaspace is the usual surprise.
  • -XX:+UseSerialGC (collector): lowest footprint on 2 vCPU. The parallel collectors cost memory this machine does not have.
  • container-memory-limit (per app): caps each user session so one app cannot exhaust the VM.

There is deliberately no swap on this image. Azure certification rejects swap baked into the OS disk, and an appliance that only fits by swapping is not sized correctly. If you publish memory-heavy applications, move to a larger VM size instead of adding swap.

These paths matter:

  • /opt/shinyproxy/shinyproxy-3.2.4.jar: the upstream release jar
  • /etc/shinyproxy/application.yml: your configuration (users, apps, backend)
  • /etc/shinyproxy/application.yml.tmpl: the shipped template that first boot renders from
  • /var/lib/docker: the container engine store, including the sample app image
  • /etc/nginx/sites-available/shinyproxy: the public reverse proxy vhost
  • /usr/share/doc/sac/: license texts and attribution

Back up /etc/shinyproxy/application.yml and any images you built on the VM. ShinyProxy itself keeps no user data, because sessions are ephemeral by design.

Step 12 - Logs and troubleshooting

ShinyProxy logs to the journal, including every session it starts and stops. Use this view when someone reports that an app would not open, because each line carries the user, the proxy ID and the app ID:

sudo journalctl -u shinyproxy --no-pager -n 400 | grep -E 'Starting proxy|Proxy activated|Proxy released|User logged in' | tail -8; echo '(session events above; empty until an app has been launched)'

ShinyProxy also records every rejected sign-in as Authentication failure [user: ...] [error: Bad credentials]. Counting these entries is a quick way to spot someone using stale credentials, or a bot trying the upstream demo accounts that do not exist on this image:

sudo journalctl -u shinyproxy --no-pager -n 400 | grep -c 'Authentication failure' || true

For the full untruncated log, including the Spring Boot startup banner and container backend detail, use sudo journalctl -u shinyproxy --no-pager -n 200.

If an application never appears, the usual causes are that the image is not present on this VM, or that the port in the spec is not the port the application listens on inside its container. Both show up in the journal as a container that started and then failed its readiness wait. Check that the image is there with docker images, and confirm the port by running the container by hand.

If the browser shows the app frame but it stays blank, the application is probably generating absolute links. Configure its path prefix as described in Step 9.

Step 13 - Put TLS in front

Sessions are authenticated with a cookie, so serve ShinyProxy over HTTPS for real use. The nginx vhost at /etc/nginx/sites-available/shinyproxy already forwards the real scheme and the WebSocket upgrade, so adding a certificate is the only change you need.

Point a DNS name at the VM, allow 443/tcp in the NSG, and issue a certificate with certbot for nginx. The vhost already sets the X-Forwarded-Proto header, so ShinyProxy builds its redirects correctly once TLS is terminated.

Licensing and attribution

ShinyProxy is copyright Open Analytics NV and is licensed under the Apache License, Version 2.0. This image ships the unmodified upstream release jar. SecureAnyCloud wrote the bundled sample application, and its container image is built from scratch with no third-party application code. It is statically linked against the Go standard library, which Google LLC distributes under the BSD 3-Clause license. Every other component comes from the Ubuntu 24.04 LTS archive under its own license.

cat /usr/share/doc/sac/SHINYPROXY-ATTRIBUTION.txt

ShinyProxy and Open Analytics are trademarks of Open Analytics NV. Shiny is a trademark of Posit Software, PBC. Docker is a trademark of Docker, Inc. Ubuntu is a trademark of Canonical Ltd. SecureAnyCloud is not affiliated with, endorsed by or sponsored by any of them. These names are used only to identify the software included in, or compatible with, this image.

Support

SecureAnyCloud publishes and supports this image, with 24/7 support. For help with a deployment, contact SecureAnyCloud support through the Azure Marketplace listing.

Deploy on Azure

Launch ShinyProxy on Ubuntu 24.04 LTS with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure