← All Insights Software · Engineering R Shiny · AWS · ShinyProxy Live at app.plantnura.com

Shiny in production on AWS: one private workspace per scientist, with login, plans and payments built in.

How we took DataViz Pro, a 31-module R Shiny application for plant science, from a laptop to a secure, paid, multi-user service in Mumbai. The full architecture, the real numbers and every step, including the mistakes worth avoiding.

31Shiny modules, 150+ plot types, served from one Docker image
11–14 sfrom click to a fresh, private R session
395R & Bioconductor packages pinned with renv (R 4.5.3)
0 bytesof user data kept after a session ends
1 · Why this exists

Shiny apps are easy to build and hard to run for strangers.

Running shiny::runApp() on your laptop takes one line. Putting the same app in front of paying researchers around the world, safely, takes about forty decisions.

DataViz Pro is PlantNura's publication-ready visualisation workbench for crop science and breeding data: field trials, genomics, GWAS, heatmaps, population structure. It is a large app (31 modules, 395 pinned R and Bioconductor packages), its users upload unpublished research data, and some of them bring multi-gigabyte genotype matrices.

That set the bar. We needed every user to get their own isolated R process, a real sign-in, plans that unlock features and memory, payments in rupees and dollars, and a privacy promise we could prove: close the tab and your data is gone. All of it had to be affordable for a young company and run by a small team.

Isolation by design

ShinyProxy starts a fresh Docker container per user, sized by plan, and removes it when the session ends.

Identity is the plan

Amazon Cognito groups are the plans. A payment webhook moves people between groups; the app reads one variable.

Lean and serverless where it counts

One Graviton server for R; Lambda, DynamoDB and EventBridge for everything business-side. About US$145 a month in total.

2 · Beyond the classic guides

Standing on two good shoulders.

Two write-ups have taught a generation of R users how to reach the cloud. Charles Bordet's Ultimate Guide to Deploying a Shiny App on AWS walks you from an EC2 instance to Shiny Server, Nginx, a domain, Let's Encrypt and a simple login. The AWS Architecture Blog's Scaling RStudio/Shiny using Serverless Architecture and AWS Fargate (2021) shows an enterprise reference design for data-science teams on ECS Fargate with EFS, WAF and Transfer Family.

Both are excellent at what they set out to do. Neither set out to run a commercial, multi-tenant Shiny product with sign-up, paid plans, per-user memory budgets and a provable data-deletion promise. That is the gap this post fills.

CapabilityClassic EC2 + Shiny Server guideAWS reference (Fargate, 2021)PlantNura blueprint (2026)
AudienceOne app, a few known usersInternal data-science teamPublic, paying researchers worldwide
Per-user isolation✕ shared R process (open-source Shiny Server)◐ Shiny containers scale; RStudio one container per service✓ one private container per user session, removed at the end
Sign-inNginx basic auth / Auth0 / shinymanagerLocal Linux users (Cognito with RStudio Pro)✓ Cognito OpenID, email sign-up, branded pages
Plans & paywall✕✕✓ 4 plans → 4 ShinyProxy specs, feature locks in R
Payments✕✕✓ Razorpay hosted checkout (INR + USD), signed webhooks, subscriptions, passes, refunds
Memory per userWhatever the box has leftPer task definition✓ 1 GB → 16 GB by plan, measured by load tests
Data privacyFiles persist on the serverPersistent EFS by design✓ session-only; containers without internet or AWS credentials
HTTPS / edgeLet's Encrypt on NginxRoute 53, WAF, ACM, ALB✓ ACM + ALB, HTTP→HTTPS, path-based routing
OperationsManualCloudWatch, CloudTrail, backups✓ 7 alarms, weekly AMIs, budget alerts, one-command rollback
Cost for a small teamLowestHigher (NAT, EFS, Fargate per task)≈ US$145 / month all-in, credit-funded
3 · The architecture

Twelve moving parts, one clear path.

The website stays on ordinary web hosting and only links to the app. Everything that needs R, identity or money lives in one AWS account in Mumbai. Click a number (or a step below) to trace that part of the flow.

PlantNura DataViz Pro architecture on AWS Browser and website on Hostinger route through Hostinger DNS to an Application Load Balancer with an ACM certificate. The load balancer sends /start and /account to a checkout Lambda after Cognito sign-in, /webhooks/razorpay to a webhook Lambda, and everything else to ShinyProxy on a Graviton EC2 server, which starts one private DataViz Pro container per user on an internal Docker network. Supporting services: ECR, Parameter Store, CloudWatch with SNS, Systems Manager, weekly AMI backups, DynamoDB, SES, EventBridge Scheduler and an expiry Lambda. AWS CLOUD · Asia Pacific (Mumbai) ap-south-1 resolves app.plantnura.com sign-in card · UPI · netbanking OpenID · groups pull image(per release) internet Researcher's browseranywhere in the worldHTTPS + WebSocket plantnura.comHostinger · plans & prices/start?plan=…&billing=… Hostinger DNSCNAME app → load balancer Razorpayhosted checkout onlyINR + USD · subscriptionssigned webhooks ApplicationLoad Balancer ACM cert · 443 · 80→301 /webhooks/* → λ/start /account → λdefault → ShinyProxy Checkout λplan page · order /subscription · AccountPython · arm64 Webhook λHMAC + amount checkidempotent per payment5 Razorpay events Amazon Cognitoemail sign-up · branded logingroups = plansresearcher · bigdata · xl EC2 · r7g.xlarge (Graviton) 4 vCPU · 32 GB · AL2023 ShinyProxy 3.2 (Docker) one spec per plan · memory caps · session caps DVP_PLAN injected per spec internal network · no internet · no AWS keys User AExplorer1 GB User BResearcher2 GB User CBig Data16 GB tab closed→ removed Amazon ECRprivate image registryv‹version›-‹git sha› Parameter StoreSecureString secretsread by IAM role only CloudWatch + SNS7 alarms → emailmemory · disk · 5xx · health Systems Manageradmin shell, no SSH portsessions recorded Weekly AMI backupsData Lifecycle Managerkeep 4 · no reboot DynamoDBentitlements · end dateson-demand Amazon SESreceiptsrolling out EventBridgeScheduleronce a night Expiry λends lapsed plans3-day reminders 1 2 3 4 5 6 7 8 9 10 11 12
User & identity flow Request routing Compute, images & payments Security boundary Serverless & management Data & backups
Figure 1 · Production architecture of app.plantnura.com (October 2026). λ = AWS Lambda. Identifiers and account numbers are deliberately omitted.
4 · A user's 14 seconds

What actually happens after "Start free".

The whole design is judged by one experience: a scientist clicks, and a few seconds later has a clean, private R session with exactly the features they paid for. Here is that path, end to end.

ClickPlan button on plantnura.com
/start?plan=
Sign inCognito hosted page, branded
OpenID
Pay (optional)Razorpay hosted checkout
webhook → group
LaunchShinyProxy starts a container
11–14 s
WorkUpload, analyse, export figures
DVP_PLAN
LeaveContainer and files removed
0 bytes kept
Why launches take 11–14 seconds, and why that is fine.A container loads R and the app's packages from scratch, so every user gets a clean session with no state from anyone else. We show a branded launching page with progress steps, tips and a citation box while it starts, so the wait feels like part of the product.
5 · The build, step by step

Ten phases, each one verified before the next.

We worked in strict phases with one rule: finish and prove a step before starting the next. Every phase below lists what we did, the configuration that matters, and the check that told us it was done. Test mode came first for everything involving money; live mode only after a written go-live checklist.

3 OctMaster plan 5 OctApp readiness+ Dockerfile 6 OctAccount · ECR · serverHTTPS · Cognito 7 OctPayments(test mode) 8 OctLimits · privacyload tests 9 Oct · LIVEFirst real purchase 12 OctOperations: alarms,backups, cost review 14 Octv1.1 release 15 Octv1.1.1 · 876 checks
Figure 2 · From a written plan to live payments in seven calendar days, then two production releases in the following week, each with a one-command rollback.
PHASE0

Foundations: a safe, cost-capped account

AWS OrganizationsIAM Identity CenterBudgets≈ $0
  1. Separate accounts from day one. An AWS Organization with a management account (billing, budgets, credits) and a dedicated workload account for the app. Nothing runs in the management account.
  2. Lock the root user with MFA and never use it for daily work. Daily sign-in goes through a named identity with MFA.
  3. Set the budget alarm before creating anything that costs money. Alerts at 50%, 80% and 100% of actual and forecast spend.
  4. Pin the region. A service control policy keeps work in Mumbai (ap-south-1), which also matches our privacy policy: data is processed in India.
  5. Tag everything (Project, Env, Owner, ManagedBy) so the bill splits per product when the next app arrives.
  6. Apply for AWS Activate early. Credits now cover the running costs for months.
The budget confirmation email has arrived, root shows MFA, and the console opens in Mumbai by default.
PHASE1

Make the app cloud-ready, then containerise it

R · renv · DockerAmazon ECRlinux/arm64

Most "it works on my laptop" failures are fixed before Docker. We ran a readiness audit on the Shiny code and fixed ten things, one commit at a time:

  1. No outside servers in the browser. CDN fonts and icon links removed, so the app works on a locked-down network and leaks nothing.
  2. Cap CPU per user. parallel::detectCores() - 1 would grab the whole server; it now reads a per-plan DVP_CORES value.
  3. No stray files. A null graphics device stops Rplots.pdf appearing in the app folder.
  4. Pin every package with renv: 395 packages, R 4.5.3, Bioconductor 3.22. Every build is identical.
  5. A tight .dockerignore cut the build context from about 540 MB to 58 MB.
  6. One environment variable decides the plan. Unset means full access (developer laptop); a typo falls back to the free plan, never to full access.
  7. Check the CPU architecture. All packages built on ARM, so we chose Graviton: more memory per dollar.
# The app never decides access by itself: the platform tells it.
plan <- tolower(Sys.getenv("DVP_PLAN", unset = "full"))
if (!plan %in% c("explorer", "researcher", "bigdata", "xl", "full")) {
  plan <- "explorer"   # unknown value = free plan, never full access
}

# CPU per user comes from the platform too (default 2 cores)
n_cores <- max(1L, min(parallel::detectCores() - 1L,
                       as.integer(Sys.getenv("DVP_CORES", "2")), na.rm = TRUE))
The image runs on the Mac for every plan value (including a typo, which must behave like the free plan) and appears in ECR with a version tag.
PHASE2

Run it: server, ShinyProxy, load balancer, HTTPS

EC2 GravitonALBACMSystems Manager
  1. Two security groups, nothing else open. The load balancer accepts 80/443 from the internet; the server accepts port 8080 only from the load balancer's security group. No SSH port at all: admin access is through Session Manager.
  2. The server: Amazon Linux 2023 on Graviton, encrypted disk, IMDSv2 required, termination protection, and an IAM role (no access keys on the machine). We started on a small instance and resized after load tests.
  3. ShinyProxy runs as a Docker container on the server and talks to the Docker daemon to start one app container per user.
  4. Certificate: ACM issues the certificate for app.plantnura.com, validated by two CNAME records added in Hostinger DNS. Keep those records; ACM uses them to renew.
  5. Load balancer: HTTPS on 443, HTTP redirected with a 301, idle timeout raised to one hour so long Shiny WebSocket sessions are not cut.
  6. DNS: a single CNAME app → the load balancer's address, in the same Hostinger panel as the website.
# Shiny keeps a WebSocket open; the default 60 s idle timeout drops quiet sessions
aws elbv2 modify-load-balancer-attributes --load-balancer-arn $ALB_ARN \
  --attributes Key=idle_timeout.timeout_seconds,Value=3600

# ShinyProxy answers /login with a redirect once sign-in is on: accept 2xx-3xx
aws elbv2 modify-target-group --target-group-arn $TG_ARN \
  --health-check-path /login --matcher HttpCode=200-399
https://app.plantnura.com shows the padlock; two browsers produce two separate containers; closing a tab removes its container.
PHASE3

Login: Cognito groups are the plans

Amazon CognitoParameter StoreOpenID Connect
  1. A Cognito user pool (Essentials tier, free up to 10,000 monthly active users) with email-only sign-up, self-registration and deletion protection. Login pages carry PlantNura branding and links to the Terms and Privacy Policy.
  2. Groups: researcher, bigdata, xl, admins. No group means the free Explorer plan, so a new user needs no extra step.
  3. An app client with callback https://app.plantnura.com/login/oauth2/code/shinyproxy and scopes openid email.
  4. The client secret lives in Parameter Store as a SecureString. ShinyProxy's start script reads it with the server's IAM role and passes it in as an environment variable. It is never in git, the image or a config file.
  5. One ShinyProxy spec per plan, all using the same image, each restricted with access-groups. Users see only the workspace their plan allows.
proxy:
  authentication: openid
  openid:
    auth-url:  https://<your-domain>.auth.ap-south-1.amazoncognito.com/oauth2/authorize
    token-url: https://<your-domain>.auth.ap-south-1.amazoncognito.com/oauth2/token
    jwks-url:  https://cognito-idp.ap-south-1.amazonaws.com/<pool-id>/.well-known/jwks.json
    client-id: ${COGNITO_CLIENT_ID}
    client-secret: ${COGNITO_CLIENT_SECRET}   # injected from Parameter Store at start
    roles-claim: cognito:groups                 # Cognito groups become ShinyProxy groups
  container-wait-time: 120000                 # ms: allow a heavy R app to boot
  container-wait-timeout: 30000              # ms per readiness probe (default 5 s is too short)
  template-path: /opt/shinyproxy/templates/plantnura
  specs:
    - id: researcher
      display-name: DataViz Pro · Researcher
      container-image: <ecr-repo>/datavizpro:v1.1.2-<sha>
      access-groups: [researcher, admins]
      container-network: dvp-apps                # internal network, no internet
      container-memory-limit: 2g
      max-lifetime: 240                         # minutes
      container-env:
        DVP_PLAN: researcher
        DVP_UPGRADE_URL: https://app.plantnura.com/start
    # ... explorer (1g), bigdata (16g, max-total-instances: 1), xl
A new email account lands on Explorer; an account in admins sees every workspace. Changing a group takes effect at the next sign-in.
PHASE4

Payments: hosted checkout, signed webhooks, no servers

Lambda (Python, arm64)DynamoDBEventBridge SchedulerRazorpay
  1. Let the load balancer do the routing and the sign-in. ALB listener rules send POST /webhooks/razorpay to the webhook Lambda (no auth, signature-checked instead), and /start and /account through an authenticate-cognito action to the checkout Lambda. Everything else goes to ShinyProxy.
  2. Checkout Lambda reads the plan from the link, creates a Razorpay order for one-off passes or a subscription for monthly plans, and opens Razorpay's hosted checkout. PlantNura never handles card or UPI details.
  3. Webhook Lambda verifies the HMAC-SHA256 signature on the raw body, checks the amount against the price book, ignores repeats (keyed on the payment ID), adds the user to the Cognito group and records the paid-up date in DynamoDB.
  4. Five events drive everything: order.paid, subscription.charged, subscription.cancelled, subscription.halted, refund.processed. A cancelled monthly plan keeps access to the end of the paid month; a full refund removes the plan.
  5. A daily expiry job (EventBridge Scheduler at 00:15 IST → Lambda) returns lapsed plans and passes to Explorer. A reminder email three days before a pass ends is rolling out with Amazon SES.
  6. Each Lambda has its own least-privilege IAM role: the console's default role is always swapped out.
import base64, hashlib, hmac

def verified(event, secret: bytes) -> bool:
    # Sign the RAW body exactly as received; parsing JSON first breaks the signature
    body = event.get("body") or ""
    raw = base64.b64decode(body) if event.get("isBase64Encoded") else body.encode()
    expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
    got = event["headers"].get("x-razorpay-signature", "")
    return hmac.compare_digest(expected, got)   # constant-time compare

# secret = Parameter Store SecureString, fetched with decryption by the Lambda's role
Test keys and live keys never mix.A Razorpay "ID provided is invalid" error almost always means a plan created in Live mode is being called with Test keys (or the reverse). Both Lambdas carry a single MODE switch that selects the matching keys and plan IDs.
In test mode: sign in → pay → webhook → plan unlocked → expiry job (simulated eight days ahead) → back to Explorer. Replaying a webhook does nothing; a wrong signature is rejected and raises an alarm.
PHASE5

Plan limits at runtime

ShinyProxy specscgroups
  1. Memory per container by plan (see section 6), enforced by Docker, so one heavy session cannot starve others.
  2. Session caps: free sessions share their own pool, Big Data runs one session at a time, and the server has an overall cap, so free users can never crowd out paying ones.
  3. Idle and lifetime limits: 15 minutes idle; longest session 1 hour on Explorer and 4 hours on paid plans.
  4. In-app upgrade buttons point to /start, so "Unlock" in the app and "Buy" on the website share one checkout.
  5. Usage events (session start and stop, plan, duration; never data) go to a local log so we can report Big Data hours before enforcing them.
Each plan opens with the right modules, upload limit and memory, and the session caps hold when we open more sessions than they allow.
PHASE6

Data and privacy: make the policy true

EC2 IMDSv2CloudWatch LogsSession Manager logging
  1. Uploads exist only inside the user's container. No shared disk, no bucket, no database of user files.
  2. App containers have no internet. They run on a Docker network created with --internal; only ShinyProxy is on both networks.
  3. Containers cannot reach AWS credentials. IMDSv2 with a hop limit of 1 means the server can read its role, but a container (one extra hop) cannot.
  4. Logs hold events, not data. Lambda logs keep 30 days; admin shell sessions are recorded for 90 days; usage records are trimmed to 12 months automatically.
  5. Then we rewrote the privacy policy to say exactly this, and nothing we could not prove.
# 1. An internal network: containers on it have no route to the internet
docker network create --internal dvp-apps
docker network connect dvp-apps shinyproxy        # ShinyProxy bridges both networks

# 2. Containers cannot borrow the server's IAM role
aws ec2 modify-instance-metadata-options --instance-id $INSTANCE_ID \
  --http-tokens required --http-put-response-hop-limit 1

# 3. Prove it from inside a user container (both must fail)
curl -m 5 https://example.com
curl -m 5 http://169.254.169.254/latest/api/token -X PUT
From inside a running user container, both the internet and the instance metadata service are unreachable, and the published privacy policy matches the system line by line.
PHASE7

Load tests before publishing any limit

synthetic datacgroup peak memory

We generated synthetic files from 25 MB to 4 GB, ran read, box plot, scatter and correlation in each workspace size, and recorded peak memory from the container's cgroup. The results changed two decisions: the server size and the Big Data file limit. Details in section 7.

Every published file limit is backed by a measured run with headroom.
PHASE8

Go-live: a checklist, not a feeling

legal pageslive keysrollback drill
  1. Terms, Privacy, Refund & Cancellation pages updated to match the plans exactly; prices identical on the website and in Razorpay (INR with GST, USD).
  2. Live keys and webhook secret entered by the owner, directly in the console; never in chat, email or code.
  3. A real purchase with a real card. It surfaced a live webhook-secret mismatch within minutes, fixed by rotating the secret in both places. A second real purchase proved the full chain.
  4. Alarms wired to email; a written rollback plan, including a one-rule "pause sales" switch on the load balancer that returns a fixed maintenance response for checkout.
Real money in, plan unlocked automatically, sale recorded, and every alarm green the next morning.
PHASE9

Operations: boring on purpose

CloudWatch agentData Lifecycle ManagerCost Explorer

Weekly server images, memory and disk metrics with alarms, a monthly cost review and a fixed release routine. See section 10.

A health check takes five minutes: target healthy, all alarms OK, memory and disk in range, no errors.
6 · Plans as workspaces

One image, four workspaces.

Every plan runs the same Docker image. What differs is what ShinyProxy gives the container: a memory budget, a session cap, a lifetime, and the value of DVP_PLAN, which the R code uses to lock or unlock modules and upload sizes. Changing a plan never needs a new build.

Explorer

DVP_PLAN=explorer · free
1 GB
  • 5 MB per file
  • Upload, preview, box & bar plots
  • Shared free pool
  • Session up to 1 h

Researcher

DVP_PLAN=researcher
2 GB
  • 60 MB per file
  • All 31 modules, full exports
  • Shared paid capacity
  • Session up to 4 h

Big Data

DVP_PLAN=bigdata
16 GB
  • Files up to 2 GB
  • All modules + big datasets
  • 1 session at a time
  • Session up to 4 h

XL

DVP_PLAN=xl · on request
16 GB
  • Institutes and special cases
  • Tuned per agreement
  • 1 session at a time
  • Capacity set per agreement

Bars show container memory relative to the largest workspace (16 GB). Explorer users keep full access to their own data in the open modules; the other modules show a preview with an Unlock button.

7 · Load-test science

Measure first, publish limits second.

File limits on a pricing page are promises. Before we published any, we loaded synthetic data files (19 columns, about 146 bytes per row) into each workspace size, ran read, box plot, scatter and correlation, and read the container's peak memory from its cgroup. Two simple rules came out of it.

0481216 01234 File size (GB) Peak memory (GB) Big Data workspace 16 GB 4 GB workspace peak ≈ 3.5 × file + 0.25 GB 4.48.112.412.7 2 GB file in 4 GB workspace: out of memory
Figure 3 · Peak container memory by file size. Purple: measured in the 16 GB workspace. Orange: the planning rule fitted to all runs. Plotting millions of points is the slow part (about 12 minutes for the 4 GB file).
Rule 1
Peak ≈ 3.5 × file size + 250 MB

R holds the parsed table, working copies and plot objects at once. Plan for three and a half times the file, plus the app itself.

Rule 2
Safe file limit ≈ workspace ÷ 8

Generous headroom for heavier analyses. A 16 GB workspace → 2 GB files, which is exactly the Big Data limit we published.

Reading a 60 MB CSV
read.csv 17 s fread ~2 s

One line of R (data.table::fread, with read.csv as fallback) made uploads about 8× faster for every user.

The result changed the server.We launched the build on an 8 GB Graviton instance. A 16 GB Big Data session cannot fit there, so after the tests we moved to an r7g.xlarge (4 vCPU, 32 GB): room for one Big Data session plus several free and Researcher sessions, at about US$110 a month. Resizing an EC2 instance is a stop, change type, start; the rest of the stack did not notice.
8 · Security & privacy

Eight layers between the internet and a researcher's data.

Unpublished field and genomic data is valuable. Our privacy promise is short ("your data lives only in your session") so every layer below exists to keep it true and checkable.

EdgeHTTPS only

ACM certificate on the load balancer; HTTP redirected; only ports 80 and 443 face the internet.

NetworkServer hidden

The server accepts traffic only from the load balancer's security group. No SSH port; admin access via Session Manager, every session recorded.

IdentityCognito sign-in

Every app request and every checkout page requires a signed-in user. Plans are group memberships, not flags in a browser.

PaymentsHosted & signed

Card and UPI details stay with Razorpay. Webhooks are HMAC-verified, amount-checked and idempotent; a bad signature raises an alarm.

IsolationOne container each

Separate process, memory cap and temporary storage per user. Non-root user inside the image.

EgressNo internet

App containers sit on an internal Docker network: nothing can be sent anywhere, including to any AI service.

CredentialsNo keys to steal

IAM roles only, least privilege per function. Secrets in Parameter Store (SecureString). IMDS hop limit 1 keeps containers away from the server's role.

RetentionSession-only data

Container removed at session end. Logs hold events, never values or file contents, and expire on a schedule.

9 · What it costs

About US$145 a month, almost all of it one server.

Because the business logic is serverless (Lambda, DynamoDB, EventBridge, Cognito's free tier), the bill is dominated by the machine that runs R. Costs scale with people online at the same time, not with how many accounts exist.

$145 per month, all-in ≈ $110 · EC2 r7g.xlarge 24/7, 32 GB Graviton, on-demand ≈ $35 · everything else load balancer, public IPv4, disk, CloudWatch, ECR, Lambda, DynamoDB and the rest
Figure 4 · Measured run-rate from the monthly cost review (October 2026, Mumbai region, before credits).
ItemMonthlyWhy so low
Amazon Cognito$0Essentials tier free up to 10,000 monthly active users
Lambda × 3≈ $0Runs only on a click, a payment or once a day
DynamoDB≈ $0On-demand, a few hundred tiny records
EventBridge Scheduler$0One run a day
NAT gateway, EFS, WAFnot neededNo outbound app traffic, no shared files by design
Credits carry the first months.AWS Activate plus sign-up credits (about US$1,120 in total) cover the run-rate into mid-2027, while we measure real usage and decide on savings plans or right-sizing.
10 · Releases & operations

Ship in minutes, roll back in one line.

Each release is an image tagged with the app version and the git commit, so what runs in production is always traceable to code. Version 1.1.1 went out with 876 automated regression checks passing; version 1.1.2 followed the same routine on 21 October.

# on the server, via Session Manager (no SSH)
NEW="v1.1.2-<sha>"; OLD="v1.1.1-<sha>"
docker pull $REPO:$NEW                                         # 1. fetch the image
cp /opt/shinyproxy/application.yml \
   /opt/shinyproxy/application.yml.before-$NEW                   # 2. keep the old config
sed -i "s/datavizpro:$OLD/datavizpro:$NEW/g" /opt/shinyproxy/application.yml   # 3. switch all plans
docker restart shinyproxy                                           # 4. new sessions use NEW

Seven alarms, one inbox

AlarmFires when
App downLoad balancer sees no healthy ShinyProxy target
Server status checkEC2 system or instance check fails
Server memory highMemory ≥ 85% for 5 minutes (CloudWatch agent)
Server disk highRoot disk ≥ 80%
Webhook bad signatureAny payment message fails verification
Webhook errorsWebhook Lambda throws
Checkout errorsCheckout Lambda throws

Plus: a weekly server image kept four deep, a monthly cost review against the budget, and a short list of launch metrics we watch (sign-ups, free-to-paid, Big Data hours, peak concurrent sessions, cost per active user).

11 · Field notes

Ten things that cost us an hour so they need not cost you one.

None of these are in the getting-started guides. Each one showed up as a confusing symptom in production or testing.

symptom: app never opens, ShinyProxy gives up

1 · The readiness probe was shorter than R's start-up

Our first page takes about 11 s; ShinyProxy's per-probe timeout defaults to 5 s. Set container-wait-timeout: 30000 and a generous container-wait-time.

symptom: target "Unhealthy", site still works

2 · Login turned the health check red

With OpenID on, /login answers with a redirect, not 200. An ALB with no healthy targets fails open, which hid it. Accept 200-399, and the "app down" alarm becomes meaningful.

symptom: 502 from a Lambda that "returned 400"

3 · ALB wants a full status line

Lambda targets behind an ALB must return statusDescription: "400 Bad Request". A bare code is treated as a malformed response.

symptom: quiet sessions drop after a minute

4 · Raise the load balancer idle timeout

Shiny keeps one WebSocket open. The ALB default of 60 s cuts users who are reading a plot. We use 3,600 s.

risk: a container reads the server's AWS role

5 · Set the IMDS hop limit to 1

With IMDSv2 and one hop, the server can use its role but Docker containers (one hop further) cannot. Test it from inside a container.

symptom: "The ID provided is invalid"

6 · Test and live plans are different worlds

Razorpay plan IDs belong to one mode. Keep both sets of IDs and keys behind a single MODE switch in code.

symptom: first live webhook rejected

7 · Rotate the webhook secret in both places at once

Generate a new random secret, paste it into Razorpay and Parameter Store, redeploy. Retries of the old event keep failing for about a day; that is expected.

symptom: new logo does not appear

8 · ShinyProxy caches assets at start

Custom templates are re-read live, but files under /assets are loaded into memory at start. Swap the file, then restart ShinyProxy.

symptom: "Access denied … service control policy"

9 · You are in the wrong region

With a region-lock policy, any console page outside Mumbai fails with an SCP error. Check the top-right corner first.

risk: an over-powered function

10 · Replace the console's default Lambda role

Creating a function in the console can attach a broad role. Give each function a dedicated role that can touch only its own parameters, table and group actions.

12 · Scaling roadmap

Built to grow without a rewrite.

Nothing in the stack stores user data on the server, so adding capacity is safe. Login, plans, payments and the image stay exactly the same at every stage; only where containers run changes.

Stage A · now

One Graviton server

ShinyProxy + Docker behind the ALB. Room for a 16 GB Big Data session alongside several free and Researcher sessions.

Stage B

Bigger or second server

Triggered when memory sits above ~70% at peak. A type change or a second target in the same group.

Stage C

Containers on ECS

ShinyProxy (3.1+) can start user containers on Amazon ECS, so AWS adds capacity with demand. Configuration change, same image.

Stage D

Second region

A copy near users in Europe or the Americas, if latency data asks for it. Decided from real numbers.

Work with PlantNura

Have a Shiny app that deserves real users?

We built this for our own product, DataViz Pro, and we reuse the same blueprint for our next apps. We can do it for yours: from a folder of R scripts to a secure, paid, monitored service your users can sign up to.

Readiness & containersCode audit, renv pinning, plan-aware app logic, a lean multi-architecture Docker image.
Hosting, login & paymentsShinyProxy on AWS, Cognito sign-in, plans, hosted checkout and webhooks, privacy by design.
Load tests & operationsMeasured file limits, right-sized servers, alarms, backups, release and rollback routines.
Talk to PlantNura →Try DataViz Pro free

References

  1. Panda, C., Hsieh, M. & Mukwevho, M. (2021). Scaling RStudio/Shiny using Serverless Architecture and AWS Fargate. AWS Architecture Blog.
  2. Bordet, C. The Ultimate Guide to Deploying a Shiny App on AWS.
  3. Open Analytics. ShinyProxy configuration reference (property names and units checked 18 Oct 2026).
  4. Amazon Web Services. Lambda functions as targets of an Application Load Balancer.
  5. Amazon Web Services. Authenticate users using an Application Load Balancer.
  6. Razorpay. Validate and test webhooks.
  7. Hussain, W. (2026). DataViz Pro: publication-ready data visualization for plant science (Version 1.1.2) [Web application]. PlantNura Consulting & Solutions LLP. plantnura.com/tools/datavizpro.html

Figures describe the production system as of 21 October 2026. Costs are AWS list prices for the Mumbai region before credits and taxes; your numbers will differ with region, instance type and usage. Account identifiers, resource IDs and secrets are intentionally omitted.

AWS and the names of AWS services are trademarks of Amazon.com, Inc. or its affiliates. This article is independent and not endorsed by AWS.