Using systemd to Run Shiny Apps
Lots of resources describe how you can host Shiny apps with Docker, Shiny Server, or via other means. But we also know Shiny apps can be launched locally. What makes your local setup different from these other options is that your local machine does not usually have a static internet protocol (IPv4) address. Without a static IPv4, it is really hard to share the app with other people because the address keeps changing unpredictably, and you might sometimes power off your machine.
Shiny uses the httpuv R package under the hood which is an HTTP and websocket server library. Could you just run Shiny directly on a remote server? This post explores this topic using systemd the system and service manager for most modern Linux distributions. All I am trying to do in this post is to make a point that the shiny R package is really self-sufficient and in the simplest case, it does not need any other layer for sharing an app.
Server setup
Spin up a virtual machine on the cloud provider of your choice. I use Ubuntu Linux 20.04 here if you are following along, your root user name might also be different (root
on DigitalOcean, ubuntu
on AWS, etc.). I also assume you have your ssh
keypair configured for passwordless login.
Create a file called setup.sh
in your current work directory on the local machine you are working on and copy-paste this into the file:
#!/bin/bash
# add CRAN to apt sources
apt-key adv --keyserver keyserver.ubuntu.com \
--recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9
printf '\ndeb https://cloud.r-project.org/bin/linux/ubuntu focal-cran40/\n' \
| tee -a /etc/apt/sources.list
add-apt-repository -y ppa:c2d4u.team/c2d4u4.0+
# install system requirements & R
export DEBIAN_FRONTEND=noninteractive
apt-get -y update
apt-get -y upgrade
apt-get -yq install \
software-properties-common \
libopenblas-dev \
libsodium-dev \
r-base \
r-base-dev \
r-cran-shiny \
r-cran-rmarkdown \
r-cran-plotly \
r-cran-ggplot2 \
r-cran-jsonlite \
r-cran-forecast
Once you know the IPv4 address, store it in the HOST
environment variable and run the setup script via ssh
:
export HOST="165.227.39.92"
ssh root@$HOST "bash -s" < setup.sh
After the installation, you should have R installed with some Shiny-related and commonly used packages ready to be used. Now log in with ssh root@$HOST
to continue.
Add an application
You will create a shiny
user group and a shiny
user that will own the home directory /home/shiny
. This is where you will use git
to pull the COVID-19 app that was introduced in this post:
# add group
addgroup --system shiny
# add new user and create home dir
adduser --system --ingroup shiny shiny
# change dir
cd /home/shiny
# change user to shiny
sudo -u shiny -s
# pull COVID app
git clone https://github.com/analythium/covidapp-shiny.git
You can now launch the app to listen on port 80 (standard HTTP port) as:
R -e "shiny::runApp('/home/shiny/covidapp-shiny/02-shiny-app/app', port = 80, host = '0.0.0.0')"
And immediately see an error: createTcpServer: permission denied
. The error message is telling you that the shiny
user won't be able to use low port numbers like 80 because ports below 1024 are privileged and only root
can open listening sockets on them. One option you have is to run the Shiny app on port 3838:
R -e "shiny::runApp('/home/shiny/covidapp-shiny/02-shiny-app/app', port = 3838, host = '0.0.0.0')"
This command now works fine and is able to execute the expression with runApp()
on port 3838 and host address 0.0.0.0
that is a meta-address referring to all IPv4 addresses on the local machine (somewhat different from the loopback address 127.0.0.1
which is the localhost).
Visit http://$HOST:3838
in your browser. You should see the COVID-19 app. But the R process is now running in the foreground. Ctr+C to quit, this kills the process and makes the app grey out in the browser. Can you run the process in the background?
Run Shiny in the background
The nohup
command is used to launch long-running scripts in the background, its name stands for "no hang up". nohup
makes the command following it to ignore the hang-up (HUP) signal so it can run after the current terminal is closed.
The general usage is nohup command > out.log 2>&1 &
. This will run the command in the background, redirect standard output and standard error into the log file (the 2>&1
part). nohup
does not send the program to the background, it is done by the &
which allows the user to continue using the current shell. Here is the full command to run the Shiny app:
nohup R -e "shiny::runApp('/home/shiny/covidapp-shiny/02-shiny-app/app', port = 3838, host = '0.0.0.0')" > /home/shiny/covidapp.log 2>&1 &
After running this command, you get back the prompt and a printout of the process identifier (PID). Use kill $PID
or kill -9 $PID
if you need to force kill. The following command finds the R process and kills it: kill $(ps aux | grep '[R] -e' | awk '{print $2}')
(this SO post explains how it is working).
Visit http://$HOST:3838
in your browser: the sight should be the familiar Shiny app. Open up the /home/shiny/covidapp.log
file to see the logs.
You might stop here, but the process can exit due to an error, or not restart after a reboot. The solution: systemd. exit
the current shell for the shiny
user and return to the root
shell (the prompt will change from $
to #
).
Configure and start a service
Create the shinyapp.service
file in the /etc/systemd/system
folder, shinyapp
is the name of the service after the file name:
touch /etc/systemd/system/shinyapp.service
Add this as content:
[Unit]
Description=COVID-19 Shiny App
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=shiny
ExecStart=R -e "shiny::runApp('/home/shiny/covidapp-shiny/02-shiny-app/app', port = 3838, host = '0.0.0.0')"
[Install]
WantedBy=multi-user.target
What do we have here? The After
directive means that the service must be started after the network is ready, Restart
instructs the service to always restart on exit irrespective of exit status, ExecStart
contains the command to run, User
is the owner of the app. See this post for an explanation of the other directives.
Reload the systemctl
daemon to update with your changes using systemctl daemon-reload
. Enable (to restart after reboot) then start the service:
systemctl enable shinyapp
systemctl start shinyapp
Visit the http://$HOST:3838
address to see the app running. Check the status with systemctl status shinyapp
to see something like this:
systemctl status shinyapp
● shinyapp.service - COVID-19 Shiny App
Loaded: loaded (/etc/systemd/system/shinyapp.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2021-06-02 06:09:13 UTC; 17s ago
Main PID: 710 (R)
Tasks: 2 (limit: 2344)
Memory: 167.1M
CGroup: /system.slice/shinyapp.service
└─710 /usr/lib/R/bin/exec/R -e shiny::runApp('/root/covidapp-shiny/02-shiny-app/app',~+~port~+~=~+~80,~+~>
Jun 02 06:09:22 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: method from
Jun 02 06:09:22 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: as.zoo.data.frame zoo
Jun 02 06:09:24 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: Attaching package: ‘plotly’
Jun 02 06:09:24 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: The following object is masked from ‘package:ggplot2’:
Jun 02 06:09:24 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: last_plot
Jun 02 06:09:24 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: The following object is masked from ‘package:stats’:
Jun 02 06:09:24 ubuntu-s-1vcpu-2gb-intel-tor1-01 nohup[710]: filter
Use systemctl restart shinyapp
to restart the service after you make changes to the Shiny app and pull a new version via git
. Because the service is enabled, it will restart after rebooting the server.
One last thing that you can do is to redirect port 80 to 3838 using iptables
:
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3838
Now visit http://$HOST
to see the Shiny app. Disabling port 3838 can be done with the uncomplicated firewall (ufw
) tool. Don't forget to destroy your server if you don't need it any more.
Summary
The setup presented here is generally applicable to all kinds of web services. You can add multiple apps by creating multiple services for them. Managing R package versions is possible by adding multiple users and install R packages into their user libraries, just like you would do with Shiny Server. Serving systemd based apps on URL paths requires a reverse proxy like Nginx or Caddy, similarly to how we proxied ports using Docker.
The approach I presented here leaves a lot more to be desired. I am not suggesting this is a super useful setup. But I believe that it demonstrates the power of the standard Linux toolbox and that Shiny is a fully capable web server on its own and can directly be deployed and served to users.