Self-hosting Shiny - Notes from EdinbR
Last month I was at the lovely EdinbR meetup to present my guide to self-hosting the amazing Shiny server. Some kind folks have been asking for the notes so that they can do it themselves, which seems fair, so here it is!
The Why
Let’s start with my slides before we get to the how-to. There are fantastic resources for hosting Shiny out there, so why would you roll your own?
Option 1: ShinyApps
For those wanting something easy and free / low-cost, then there’s ShinyApps which has a free tier, and then scaling costs if you want more. It’s a great way to try Shiny out, and I encourage those taking their first steps to try it! However, it has two drawbacks that may matter.
Firstly, there’s time. ShinyApps has a limited number of of “active hours” per month, and at the free tier it’s only 15, or ~1hr/day. That’s perfect for testing, but not if you are building an app for a team to use. Even at $40/month you only get 500 hours, so if your app is in constant use, that’s 16hrs/day - fine if your team is in one timezone, but mine isn’t :)
Second is privacy - by definition ShinyApps is public. You can protect it with a password, but that’s not the same as being on the company intranet. If you have sensitive data, that’s not going to fly.
Option 2 - RSConnect
If ShinyApps doesn’t suit, perhaps RSConnect will? It’s an amazing resource for teams, and it’s on-premise, so no privacy concerns or time constraints! Slight downside though, it’s $15,000/year! Unless you qualify for discounts, of course, but I’m fairly sure we don’t :)
Option 3 - Self-host
However if, like me, you are reasonably happy with the command line, and have $5/month for a basic VM, you can have 100% uptime Shiny for just a little effort - it’s the usual cost/effort/time triangle. Disclaimer: a $5 / month VM isn’t going to have the resource to handle many users, but it will always be available. Scaling might be a future post :P
The HowTo
OK, let’s get to it. We’re going to cover:
- Basic install of Shiny
- Useful configuration options
- Apache frontend
- SSL certs
- Deploying apps
I’m also going to assume a few things:
- Basic understanding of system administration
- Ubuntu VM (CentOS/Debian/Fedora etc should all work in similar ways)
- I’m using a lowest-tier DigitalOcean VM
- Shell access (no shared CPanel stuff)
- SSH access
- x86 chip (Shiny isn’t packaged for ARM, although you can run it from source)
Basic install of Shiny
Starting from our shell on our clean VM, we just need to follow the Shiny server admin guide. Get a beverage, this takes about 20 min!
# Get root
sudo -i
# Update package lists
apt-get update
# Install R itself, ~242 packages
apt-get install r-base
# Install the Shiny runtime - ~17 packages
R -e "install.packages('shiny', repos='https://cran.rstudio.com/')"
# Install RMarkdown so the demo app works - ~12 packages
R -e "install.packages('rmarkdown', repos='https://cran.rstudio.com/')"
# Install Shiny Server
cd /tmp
apt-get install gdebi-core
# Go to https://rstudio.com/products/shiny/download-server/ for the current DEB
wget https://download3.rstudio.org/ubuntu-14.04/x86_64/shiny-server-1.5.12.933-amd64.deb
gdebi /tmp/shiny-server-1.5.12.933-amd64.deb
That’s it! If we check the process list, we can see the server is running, and it’s port is listed in the network stats:
# pgrep shiny -alf
19228 /opt/shiny-server/ext/node/bin/shiny-server /opt/shiny-server/lib/main.js
# ss -lntp | grep 3838
LISTEN 0 128 :::3838 :::*
Finally, we can hit http://YOUR-IP:3838
and Shiny should load with a couple of
example apps. Success!
Useful configuration options
OK, so far all we’ve is Read The Fine Manual, not too impressive. Certainly not worth a blog post. Let’s go further - now we’ll add some of the more useful configuration options. Shiny Server has many options that might be useful - some are only available in the paid-for version however.
We’ll add two options available in the free version - apps running from user’s home directories (good for dev work), and a directory of “production” apps that are ready to go.
Here’s the diff on a “default” Shiny install:
--- opt/shiny-server/config/default.config 2019-09-09 17:52:06.000000000 +0000
+++ /etc/shiny-server/shiny-server.conf 2018-11-09 10:18:53.920680178 +0000
@@ -1,21 +1,39 @@
-# Instruct Shiny Server to run applications as the user "shiny"
-run_as shiny;
+# Tell Shiny Server that we want to run as the user whose
+# home directory we find the application in.
+run_as :HOME_USER: shiny;
# Define a server that listens on port 3838
server {
listen 3838;
# Define a location at the base URL
+ location /~ {
+
+ # Allow users to host their own apps in ~/ShinyApps
+ user_dirs;
+
+ # Optionally, you can restrict the privilege of hosting Shiny applications
+ # only to members of a particular Linux group.
+ # members_of shinyUsers;
+ }
+
+ location /apps {
+ site_dir /srv/shiny-server/apps/;
+ log_dir /var/log/shiny-server/apps;
+ directory_index on;
+ }
+
+ # Define a location at the base URL
location / {
# Host the directory of Shiny Apps stored in this directory
- site_dir /srv/shiny-server;
+ site_dir /srv/shiny-server/main;
# Log all Shiny output to files in this directory
- log_dir /var/log/shiny-server;
+ log_dir /var/log/shiny-server/main;
# When a user visits the base URL rather than a particular application,
# an index of the applications available in this directory will be shown.
- directory_index on;
+ directory_index off;
}
}
Make these changes and then run systemctl restart shiny-server.service
to
restart Shiny Server.
The run_as
and user_dirs
additions mean that any user can host Shiny apps in
~/ShinyApps
and these are accessible at http://YOUR_IP:3838/~/USER/APP
. As a
concrete example, let’s copy a working sample app from Shiny itself:
mkdir /home/shiny/ShinyApps
cp -r /srv/shiny-server/sample-apps/hello /home/shiny/ShinyApps
vi ~shiny/ShinyApps/hello/ui.R # Change a string so you know it's this app
Now navigate to http://YOUR_IP:3838/~/shiny/hello
and you should see your app!
This is great for “hidden” apps you’re working on (as there’s no directory
listing) and for teams to work on things (as anyone with an account can host
apps).
The rest of the changes make for a “production” area with a directory listing.
Let’s set it up, again using the hello
app:
mkdir /srv/shiny-apps/
cp -r /srv/shiny-server/sample-apps/hello /srv/shiny-apps/
vi /srv/shiny-apps/hello/ui.R # Make a different change this time
Now if you go to http://YOUR_IP:3838/apps
you’ll get a directory listing with
hello
in it - clicking that will take you to the copy of hello
we just
created. This is super for the more “official” apps you create, that are for a
wider audience, and should be writiable to be just anyone (perhaps, some signoff
process is appropriate first :P).
Also notice how neither of these deployments required restarting Shiny Server itself - it’s happy to serve newly-deployed apps as and when they are created, with no downtime to existing apps.
Apache frontend
So far so good, but we can go further. No one can ever remembers what port something runs on, and in any case it’s not wise to expose Shiny to the internet, so let’s put an Apache proxy on it.
(Note, you can use any webserver for this, I’m just most familiar with Apache. The process will be the same for other webservers)
Start by installing Apache (well, yeah, obviously):
apt-get install apache2
Then we need to create a config for our Shiny proxy. Apache numbers files, so
we’ll create /etc/apache2/sites-available/25-shiny.conf
<VirtualHost *:80>
ServerName whatever.you.like.for.now.org
## Vhost docroot
DocumentRoot "/var/www/vhosts/shiny/htdocs"
## Directories
<Directory "/var/www/vhosts/shiny/htdocs">
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
</Directory>
## Logging
ErrorLog "/var/log/apache2/shiny_server-http_error.log"
ServerSignature Off
CustomLog "/var/log/apache2/shiny_server-http_access.log" combined
## Proxy rules
ProxyRequests Off
ProxyPreserveHost Off
ProxyPass / http://localhost:3838/ nocanon
ProxyPassReverse / http://localhost:3838/
</VirtualHost>
The only really interesting part is the Proxy*
directives, which tell Apache
to proxy traffic on port 80
(first line) to port 3838
. Let’s enable it:
a2enmod proxy_http
a2dissite 000-default
a2ensite 25-shiny.conf
systemctl restart apache2
Excellent. If all is well, you should be able to drop the port on any of the
earlier test links now - e.g. http://YOUR_IP/apps
. Result!
SSL certs
Now that we have Apache, we can also add SSL to our Shiny Server. This is 2019, and there’s really no excuse for not using SSL on your sites - LetsEncrypt makes this trivial.
We’ll start with installing certbot
which is the LetsEncrypt commandline
tooling:
apt-get install certbot
Now, the part I can’t demonstrate - you’ll need a domain name for your host, which means you need make changes to your DNS (or access to someone who can). At
the very least, you need to make an “A” or “AAAA” record for your server’s IP to your chosen hostname. Once done, you can check it with dig
:
# dig @8.8.8.8 -t A YOUR_NAME +short
YOUR_IP
Great. Now we’ll get a cert for YOUR_NAME and set up Apache. For brevity, we’ll
use the standalone
Certbot plugin, which requires us to stop Apache - there
are better ways but this post is already huge :P
systemctl stop apache2
certbot certonly -d YOUR_NAME --standalone
a2enmod ssl
Certbot will run it’s own webserver for a moment and ask the LetsEncrypt servers
to verify the domain, before writing out a certificate. Now we create the
config, in /etc/apache2/sites-available/25-shiny-ssl.conf
:
<VirtualHost *:443>
ServerName YOUR_NAME
## Vhost docroot
DocumentRoot "/var/www/vhosts/shiny/htdocs"
## Directories
<Directory "/var/www/vhosts/shiny/htdocs">
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
</Directory>
## Logging
ErrorLog "/var/log/apache2/shiny_server-https_error_ssl.log"
ServerSignature Off
CustomLog "/var/log/apache2/shiny_server-https_access_ssl.log" combined
## Proxy rules
ProxyRequests Off
ProxyPreserveHost Off
ProxyPass / http://localhost:3838/ nocanon
ProxyPassReverse / http://localhost:3838/
## SSL directives
SSLEngine on
SSLCertificateFile "/etc/letsencrypt/live/YOUR_NAME/fullchain.pem"
SSLCertificateKeyFile "/etc/letsencrypt/live/YOUR_NAME/privkey.pem"
SSLCertificateChainFile "/etc/letsencrypt/live/YOUR_NAME/chain.pem"
</VirtualHost>
We also want to redirect traffic from port 80
to port 443
so that we only
use HTTPS. Let’s create a new file for that, in
/etc/apache2/sites-available/25-shiny-redirect.conf
:
<VirtualHost *:80>
ServerName YOUR_NAME
## Vhost docroot
DocumentRoot "/var/www/vhosts/shiny/htdocs"
## Directories
<Directory "/var/www/vhosts/shiny/htdocs">
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Require all granted
</Directory>
## Logging
ErrorLog "/var/log/apache2/shiny-server_error.log"
ServerSignature Off
CustomLog "/var/log/apache2/shiny-server_access.log" combined
## Redirect rules
Redirect / https://YOUR_NAME/
</VirtualHost>
Now we can enable these, and disable the old config that served port 80
:
a2ensite 25-shiny-ssl 25-shiny-redirect
a2dissite 25-shiny
apache2ctl configtest # We've made a lot of changes so check, should return OK
systemctl start apache2
If all has gone well, then going to https://YOUR_NAME/apps
should work (and
display a ‘green padlock’), and going to http://YOUR_NAME/apps
should redirect
to the HTTPS version. Perfect.
And now we’re done! Almost…
Deploying apps
In my talk I also covered deploying apps to your self-hosted Shiny Server via
rsync
and my DeployR package. But
this post is huge, so I’ll do a separate post on DeployR soon. Stay tuned!
Conclusion
Give yourself a high-five if that worked, it’s a lot of config. But now you’ve
got a self-hosted Shiny Server that is suitable for you and your team to use
24⁄7, as well as shell access so you can use cron
, the
{pins}
package, run databases… it’s all
yours! Well done!