ShinyProxy on Ubuntu 24.04 LTS on Azure
Overview
ShinyProxy is an open source deployment server for data science web applications, built by Open Analytics. Rather than leaving an analytics app running on a shared port for anyone to reach, it puts a sign in page in front of your applications, starts a dedicated container for each user who opens one, and reverse 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.
Applications are declared in one configuration file: an identifier, a display name, the container image to run and the port it listens on. Because the unit of deployment is a container image, 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 cloudimg image runs the unmodified ShinyProxy 3.2.4 release on OpenJDK 21 on a hardened, fully patched Ubuntu 24.04 LTS base, with the Docker engine as its container backend and nginx in front on port 80 handling 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. In fact 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, starts ShinyProxy, and only then publishes the site on port 80. Until that finishes nothing is bound to port 80. Backed by 24/7 cloudimg support.
A small cloudimg written sample application is baked into the image as a container, so your first sign in launches a real container and renders a real dashboard instead of showing an empty app list. It is built from scratch at image build time from cloudimg's own source: no third party application image is redistributed with this product.
What is included:
- 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
:80reverse proxying ShinyProxy, including the WebSocket interactive apps need - A per VM administrator password generated on first boot and recorded in a root only file
- A cloudimg written sample analytics app, 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
Key facts
| Item | Value |
|---|---|
| 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 | cloudimg-sample-analytics:1.0 |
| Recommended size | Standard_B2s |
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus 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. NSG inbound: allow 22/tcp from your management network and 80/tcp (plus 443/tcp once you terminate TLS on the VM) so your users can reach ShinyProxy.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for ShinyProxy by cloudimg, and 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 Review + create and Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name shinyproxy \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Then open port 80 so your users can reach ShinyProxy:
az vm open-port --resource-group <your-rg> --name shinyproxy --port 80
Step 3 - Confirm the services are running
SSH in 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
Three active lines is what you want. 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 actually listening. ShinyProxy and its actuator are on loopback; nginx alone faces the network:
ss -ltn | grep -E ':80|:8080|:9090'

If port 80 is not bound, first boot has not completed. That is deliberate and fail 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:
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
Prove for yourself that upstream's documented demo accounts do not exist here. This counts how many times jack or jeff appear anywhere in the running configuration, and 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: it checks that an anonymous request for an app is refused, that upstream's demo credentials are rejected, that this VM's password authenticates, that opening the app really starts a container and returns the app's own HTML through the proxy, and that stopping the app removes the container again.
sudo /usr/local/sbin/shinyproxy-roundtrip.sh
You can see the baked sample app image it launches, and confirm no container is left behind afterwards:
docker images cloudimg-sample-analytics; docker ps --filter ancestor=cloudimg-sample-analytics:1.0

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

After signing in you land on the application catalogue. Every app declared in the configuration appears here, and this image ships one to get you started.

Step 7 - Launch the sample application
Select cloudimg Sample Analytics. ShinyProxy pulls the container for your session, waits for it to answer, and then proxies it into the page. The dashboard you see is served by a container that did not exist a moment ago, and 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 is a real request routed 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 simply close the tab, ShinyProxy notices the missing heartbeat and reaps 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 docker pull it from a registry you trust:
docker images --format '{{.Repository}}:{{.Tag}}'
Then add a spec under proxy.specs in /etc/shinyproxy/application.yml. The existing entry is the pattern to copy:
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, 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 catalogue in your browser. Keep container-memory-limit set on every app: it is what stops one user's session 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, and 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 someone, copy the existing user block and give them 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 anything beyond 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.
Change the generated administrator password whenever you like by editing its password value and restarting the service.
Step 11 - Storage, memory and sizing
The JVM is given explicit ceilings rather than the defaults, because on a 4 GiB machine it shares memory with the Docker engine and with every app container it starts:
systemctl show -p MainPID --value shinyproxy | xargs -I{} sh -c 'tr "\0" " " < /proc/{}/cmdline'; echo; free -m | head -2

| Setting | Value | Why |
|---|---|---|
-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; unbounded metaspace is the usual surprise |
-XX:+UseSerialGC |
Collector | Lowest footprint on 2 vCPU; the parallel collectors cost memory this box 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 rather than adding swap.
| Path | Holds |
|---|---|
/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 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/cloudimg/ |
Licence texts and attribution |
Back up /etc/shinyproxy/application.yml and any images you built on the VM. ShinyProxy itself keeps no user data: sessions are ephemeral by design.
Step 12 - Logs and troubleshooting
ShinyProxy logs to the journal, including every session it starts and stops. This is the view you want 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 those 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 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 run this behind HTTPS in production. 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 needed. Point a DNS name at the VM, allow 443/tcp in the NSG, and issue a certificate with certbot for nginx; the X-Forwarded-Proto header the vhost already sets means 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. The bundled sample application is written by cloudimg and its container image is built from scratch, containing no third party application code; it is statically linked against the Go standard library, which Google LLC distributes under the BSD 3-Clause licence. Every other component comes from the Ubuntu 24.04 LTS archive under its own licence.
cat /usr/share/doc/cloudimg/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. cloudimg 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
This image is published and supported by cloudimg with 24/7 support. For help with a deployment, contact cloudimg support through the Azure Marketplace listing.