Using systemd to Run Shiny Apps
Tags: shiny, rstats
Published: 2021-06-07 16:36:08

*By: [Peter Solymos](https://hosting.analythium.io/author/peter/)*

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](https://hosting.analythium.io/run-shiny-apps-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](https://CRAN.R-project.org/package=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](https://en.wikipedia.org/wiki/Systemd) the system and [service manager](http://weng-blog.com/2016/11/04/linux-boot-systemd.html) 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](https://hosting.analythium.io/dockerized-shiny-apps-with-dependencies/) 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](https://en.wikipedia.org/wiki/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](https://stackoverflow.com/questions/3510673/find-and-kill-a-process-in-one-line-using-bash-and-regex) 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](https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6) 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](https://fixyacloud.wordpress.com/2020/01/26/how-to-run-a-server-on-port-80-as-a-normal-user-on-linux/) 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](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-20-04) (`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](https://hosting.analythium.io/host-shiny-apps-with-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.

Further reading
---------------

-   [Using
    `nohup`](https://www.r-bloggers.com/2012/01/long-running-r-commands-unix-screen-nohup-and-r/)
-   [`systemd`
    tutorial](https://www.cloudsavvyit.com/3092/how-to-add-your-own-services-to-systemd-for-easier-management/)
-   [`systemctl`
    commands](https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units)