twinkle
is a package designed to help manage a set of shiny applications. The package itself does not actually contain any shiny-specific code but mostly helps:
- fetch code from github (possibly from a private repository)
- build application-specific libraries so that each app runs in relative isolation
- synchronise the app sources and library into directory trees that can contain both staging and “production” instances
Usage
Our primary use of this system is as a docker container as part of a deployment acting as a load balanced shiny server. To do this, we have three services:
<apache> -- <haproxy> -- <shiny...>
where apache
presents the interface to the world and looks after https, haproxy
acts as a load balancer and we have several (one to many) copies of shiny
, each capable of serving every application.
We like this setup, compared with something like shinyproxy where balancing occurs between applications but multiple users of the same appliation compete (this at least used to be the case). In our experience, multiple users tend to correlate on the same application, e.g., when running a workshop or after launching an app related to a new publication.
We are reasonably agnostic about how deployments are configured, and the actual configuration is fairly basic; see shiny-dide
for a full example using docker compose
and minimal configuration for the three services above, which is deployed at shiny.dide.ic.ac.uk
We then imagine some persistant storage (such as a docker volume) holding all the data for applications; this is where shiny
will serve from but also some persistant storage that we need in order to set the applications up. The mrc-ide/twinkle
container contains a cli application twinkle
that can perform simple administrative commands based on a configuration that describes the applications.
Configuration
The configuration is in yaml, and the only valid top-level key is apps
. This is a list of objects, each representing an application with the name being the anme of the application. A simple configuration would look like:
apps:
myapp:
username: alice
repo: alices-app
branch: main
which contains a single application that will be served at /myapp
, which we will find at github.com/alice/alices-app
The valid fields within each application are:
-
username
(required): the github user or organisational name that holds the sources -
repo
(required): the name of the repository within theusername
namespace on github -
branch
: optionally the branch to deploy. By default we will use the default branch from github (i.e., the one that you see when you navigate to the github landing page and the branch that is checked out by default on clone) -
subdir
: the subdirectory within the repo where the application itself can be found. It is reasonably common that the application might be found somewhere other than the root; packages often useinst/app
and sometimes a shiny app is part of a larger repository. The application can be stored anywhere within the repo, but it may not reference files above the subdirectory while it runs -
private
: a boolean indicating if the repository is private. If so, you will need to add a deploy key to the repository in order to clone it over ssh -
script
: an optional script to run after the app has been sync-ed to the destination (either staging or production).
The intention for use of script
is for apps that are installed into a directory that is read-only at app runtime, which is the case in shiny-dide
so that we can safely run many workers on the same tree. This causes issues where it is convenient to have the code in the application perform some calculation after cloning but before running. Examples that we have seen where this was used (or could have been used) are:
- downloading some data that is not able to be stored in the git repository
- prerending rmarkdown for use with
learnr
Note that (at least at present) this script is run separately after the sync to staging and to production, so ideally the script should be deterministic or you will see slightly different results. It is possible that we could change things to allow sync to production to occur from staging (rather from the sources) but that requires that we always update source/lib, then staging, then production.
Packages
Applications will depend on a number of packages in order to run. This should be indicated within the application root of the repository (so in the repository root if subdir
is not used, or within subdir
otherwise).
Most applications can use the same pkgdepends.txt
format that is supported by conan
, as used in hipercow
. See the docs here for details. Typically this is simply a file pkgdepends.txt
in the application root, with each line being the name of a package or a github specification like mrc-ide/malariasimulation@v1.6.0
, for example:
# Also use the mrc-ide universe
repo::https://mrc-ide.r-universe.dev
# Specific version of malaria simulation, via a tag
mrc-ide/malariasimulation@v1.6.0
# Package from CRAN
coda
The actual package installation is performed with pkgdepends
.
Alternatively, you can use a script conan.R
and install packages within that however you want. This is useful for cases where pkgdepends is failing to resolve things and you need to manually override bits of dependency resolution that is failing to get right.
Finally, we support legacy provision.yml
files used with the original shiny server, but as we don’t want new projects to use this method we do not describe it. There is no pressing need to migrate old projects, however.
The precedence order is conan.R
(highest precedence), then pkgdepends.txt
, then provision.yml
(lowest precedence).
Interaction
Here, we assume the exact setup used in shiny-dide
, and we assume that the system is running (i.e., docker compose ps
shows services running).
You should be able to run
$ ./twinkle --help
Twinkle.
Usage:
twinkle create-deploy-key [--force] <name>
twinkle update-src [--branch=NAME] <name>
twinkle install-packages <name>
twinkle sync [--production] <name>
twinkle deploy [--production] <name>
twinkle list [<pattern>]
twinkle delete <name>
Options:
--branch=NAME Github branch to use
which will show available commands, mapping on to the various steps in deployment. From here, you can:
-
create-deploy-key
: create a deploy key for a private application; this generates and saves the private key within the persistant storage, then prints the public key and instructions for adding this to github. Once you have completed this step you can clone private repositories -
update-src
: refresh the server copy of the application source code -
install-packages
: install packages for the application -
sync
: push the (updated) copies of the application source and packages onto the staging directory or the live copy of the application
The other commands are helpers:
-
deploy
is a wrapper aroundupdate-src
,install-packages
andsync
, designed to use when setting up a new application where you want to iterate quickly -
list
lists known applications -
delete
removes all files associated with an application
Other comments
We assume that three environment variables are set
-
TWINKLE_ROOT
: points at the directory where all twinkle data will be stored. The applications to be served will be found at${TWINKLE_ROOT}/apps
and the staging applications at${TWINKLE_ROOT}/staging
-
TWINKLE_LOGS
: points at the location that the logs will be written -
TWINKLE_CONFIG
: points at the location of the configuration
In the shiny-dide
setup, we set these in the compose file, bind-mounting the configuration in from the host and using a docker volume for the root, shared among all workers. Practically, the server configuration needs to be kept in sync with this configuration, with site_dir
set to ${TWINKLE_ROOT}/apps