install.packages("golem")
library(golem)
::create_golem(path = "gap") golem
The golem
framework
The golem
package provides many options for R programmers experienced with creating R packages, but who are looking to streamline their application development.1
“opinionated framework for building production-grade Shiny applications”
Getting started
To create a new golem
app from the console, enter the following:
If creating a golem
app from RStudio’s New Project Wizard, the following defaults are available:
Package files
The initial folder structure for a new golem
application is below:
gap
├── DESCRIPTION
├── NAMESPACE
├── R
│ ├── app_config.R
│ ├── app_server.R
│ ├── app_ui.R
│ └── run_app.R
├── dev
│ ├── 01_start.R
│ ├── 02_dev.R
│ ├── 03_deploy.R
│ └── run_dev.R
├── inst
│ ├── app
│ │ └── www
│ │ └── favicon.ico
│ └── golem-config.yml
├── man
│ └── run_app.Rd
└── gap.Rproj
The dev/
folder contains golem
‘s ’guided tour’ scripts. These contain functions to help guide application development.
dev/01_start.R
opens automatically
gap/dev/
├── 01_start.R
├── 02_dev.R
├── 03_deploy.R
└── run_dev.R
1 directory, 4 files
dev/run_dev.R
is for running the ‘development version’ of the application.
If you are familiar with R package development, you can think of the dev/
scripts as a ‘Shiny app-package development checklist.’
DESCRIPTION: In the
dev/01_start.R
script, users build aDESCRIPTION
file withgolem::fill_desc()
fill_desc()
uses thedesc
package and the sections are entered in akey = "value"
format
::fill_desc( golempkg_name = "gap", pkg_title = "An example goelm app", pkg_description = "A working example of the golem package.", author_first_name = "Martin", author_last_name = "Frigaard", author_email = "mjfrigaard@pm.me", repo_url = NULL # The URL of the GitHub Repo (optional) )
- In
dev/02_dev.R
, theattachment::att_amend_desc()
will “AmendDESCRIPTION
with dependencies read from package code parsing”.
::att_amend_desc() attachment
- If
attachment
is not installed, useinstall.package('attachment')
dev/01_start.R
contains the usethis
functions for for creating common package development files:
LICENSE
::use_mit_license() usethis
README
::use_readme_rmd() usethis
Code of Conduct
::use_code_of_conduct() usethis
Lifecycle badge
::use_lifecycle_badge("Experimental") usethis
NEWS.md
::use_news_md(open = FALSE) usethis
Git
::use_git() usethis
golem
files
The golem
functions in dev/01_start.R
are for setting the golem
options and using recommended tests.
Options
::set_golem_options() golem
Tests (with
testthat
)::use_recommended_tests() golem
Favicon
::use_favicon() golem
Helper functions:
golem::use_utils_ui()
andgolem::use_utils_server()
creategolem
’s UI (R/golem_utils_ui.R
) and server (R/golem_utils_server.R
) utility functions in theR/
folder
golem
gives away lots of free code!
Both R/golem_utils_ui.R
and R/golem_utils_server.R
contain a lot of helper functions that come in handy if you’re tired of writing out particular function names (like reactiveValuesToList()
or column(width = 6)
/column(width = 12)
)
Check them out here:
App Code
The dev/02_dev.R
file covers the ‘development’ phase of a new a golem
app-package. Most of the golem
functions in dev/02_dev.R
will create files in the R/
and inst/
folders.
The two pre-configured UI and server functions are in R/app_ui.R
and R/app_server.R
:
R/
├── app_config.R
├── app_server.R
├── app_ui.R
└── run_app.R
1 directory, 4 files
app_ui()
andapp_server()
aregolem
-flavored UI and server files, which means they include the@noRd
tag and include additionalgolem
utilities.#' The application User-Interface #' #' @param request Internal parameter for `{shiny}`. #' DO NOT REMOVE. #' @noRd <- function(request) { app_ui tagList( # Leave this function for adding external resources golem_add_external_resources(), # Your application UI logic fluidPage( h1("gap") ) ) }
#' The application server-side #' #' @param input,output,session Internal parameters for {shiny}. #' DO NOT REMOVE. #' @import shiny #' @noRd <- function(input, output, session) { app_server # Your application server logic }
If you do some digging, you’ll find most of these
golem
utilities are wrappers forshiny
andusethis
functions. For example,golem_add_external_resources()
is a wrapper forshiny::addResourcePath()
andhtmltools::htmlDependency()
:#' Add external Resources to the Application #' #' This function is internally used to add external #' resources inside the Shiny application. #' <- function() { golem_add_external_resources add_resource_path( "www", app_sys("app/www") ) $head( tagsfavicon(), bundle_resources( path = app_sys("app/www"), app_title = "gap" )# Add here other external resources # for example, you can add shinyalert::useShinyalert() ) }
- And
app_sys()
is a wrapper forsystem.file()
:
#' Access files in the current app #' #' NOTE: If you manually change your package name in the DESCRIPTION, #' don't forget to change it here too, and in the config file. #' For a safer name change mechanism, use the `golem::set_golem_name()` function. #' #' @param ... character vectors, specifying subdirectory and file(s) #' within your package. The default, none, returns the root of the app. #' #' @noRd <- function(...) { app_sys system.file(..., package = "gap") }
- And
run_app.R
is an exported function that is available for me to run my app after I’ve installed the package:library(gap) ::run_app() gap
Creating code files
golem
has wrappers for creating modules and helper functions in theR/
folder:## Add modules ---- ## Create a module infrastructure in R/ ::add_module(name = "name_of_module1", with_test = TRUE) golem::add_module(name = "name_of_module2", with_test = TRUE) golem ## Add helper functions ---- ## Creates fct_* and utils_* ::add_fct("helpers", with_test = TRUE) golem::add_utils("helpers", with_test = TRUE) golem
with_test = TRUE
ensures these functions will also create test files intests/
Configuration
The
R/app_config.R
file contains two functions:app_sys()
(covered above) andget_golem_config()
, which reads theinst/golem-config.yml
configuration filedefault: golem_name: gap golem_version: 0.0.0.9000 app_prod: no production: app_prod: yes dev: golem_wd: !expr here::here()
golem-config.yml
gives access to the app version, name, and (development) working directory, so it can be used to add “production-only elements” and is “shareable across golem projects”
get_golem_config()
is also included in theR/app_config.R
file# Read App Config <- function( get_golem_config value,config = Sys.getenv( "GOLEM_CONFIG_ACTIVE", Sys.getenv( "R_CONFIG_ACTIVE", "default" ) ),use_parent = TRUE, # Modify this if your config file is somewhere else file = app_sys("golem-config.yml") ) {::get( configvalue = value, config = config, file = file, use_parent = use_parent ) }
Test files
golem::use_recommended_tests()
creates the tests/
folder and a series of unit tests in the dev/01_start.R
script. This function is essentially a wrapper around usethis::use_testthat()
, but with some additional ‘recommendations’.2
golem::use_recommended_tests()
adds thespelling
package to ourDESCRIPTION
and updates theWORDLIST
The
tests
folder uses thetestthat
frameworktests/testthat/ ├── test-golem-recommended.R ├── test-golem_utils_server.R └── test-golem_utils_ui.R 2 directories, 4 files
External files
The inst/
file initially has the following contents/structure:
inst/
├── WORDLIST
├── app/
│ └── www/
│ └── favicon.ico
└── golem-config.yml
The golem-config.yml
file is covered above, but the inst/app/
folder works just like the inst/extdata
folder (it is loaded when the package is installed and makes these files available to the application).
dev/02_dev.R
includes golem wrappers for including CSS, JavaScript, and SASS files to the inst/app/www/
folder:
Deploy
The final script in the guided tour contains functions for deploying a new application to Posit Connect or Docker (it opens automatically after completing the dev/02_dev.R
)
RStudio (Posit) Connect
Docker
golem::add_dockerfile_with_renv()
andgolem::add_dockerfile_with_renv_shinyproxy()
I’ll deploy my app using shinyapps.io, so after running
golem::add_shinyappsio_file()
I will see the following output and a newapp.R
file:::add_shinyappsio_file() golem ── Creating _disable_autoload.R ────────────────────────────────────────────────────── ✔ Created'/Users/mjfrigaard/projects/gap' ✔ Setting active project to '^app\\.R$' to '.Rbuildignore' ✔ Adding '^rsconnect$' to '.Rbuildignore' ✔ Adding 'pkgload' to Imports field in DESCRIPTION ✔ Adding `pkgload::fun()` • Refer to functions with /Users/mjfrigaard/projects/gap/app.R ✔ File created at : To deploy, run::deployApp() • rsconnect 'll need to upload the whole package to ShinyApps.io • Note that you
app.R
contents# Launch the ShinyApp (Do not remove this comment) # To deploy, run: rsconnect::deployApp() # Or use the blue button on top of this file ::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE) pkgloadoptions( "golem.app.prod" = TRUE) ::run_app() # add parameters here (if any) gap
Building golem
apps
Building an application with golem
is very similar to developing an R package. However, golem
streamlines some of the R package development processes into wrapper functions. The sections below cover creating modules, utility functions, and tests in a new golem
app:
Modules
New modules can be created with golem::add_module()
. I’ve provided an example below to demonstrate the variable arguments and options:
add_module(name = 'name',
fct = 'fun',
utils = 'fun',
with_test = TRUE,
export = TRUE)
The code above creates the following files:
name = ‘name’: creates
R/mod_name.R
, a boilerplate Shiny module (ui and server functions)show/hide R/mod_name.R
#' name UI Function #' #' @description A shiny Module. #' #' @param id,input,output,session Internal parameters for {shiny}. #' #' @rdname mod_name #' @export #' #' @importFrom shiny NS tagList <- function(id){ mod_name_ui <- NS(id) ns tagList( ) } #' name Server Functions #' #' @rdname mod_name #' @export <- function(id){ mod_name_server moduleServer( id, function(input, output, session){ <- session$ns ns }) } ## To be copied in the UI # mod_name_ui("name_1") ## To be copied in the server # mod_name_server("name_1")
fct = ‘fun’ and utils = ‘fun’: creates two empty R files in the
R/
folder (R/mod_name_fct_fun.R
andR/mod_name_utils_fun.R
) with the same prefix as the module.with_test = TRUE: creates
tests/testthat/test-mod_name.R
, a test file for the module with the following boilerplate tests:show/hide tests/testthat/test-mod_name.R
testServer(mod_name_server, # Add here your module params args = list(), { <- session$ns ns expect_true( inherits(ns, "function") )expect_true( grepl(id, ns("")) )expect_true( grepl("test", ns("test")) )# Here are some examples of tests you can # run on your module # - Testing the setting of inputs # session$setInputs(x = 1) # expect_true(input$x == 1) # - If ever your input updates a reactiveValues # - Note that this reactiveValues must be passed # - to the testServer function via args = list() # expect_true(r$x == 1) # - Testing output # expect_true(inherits(output$tbl$html, "html")) }) test_that("module ui works", { <- mod_name_ui(id = "test") ui ::expect_shinytaglist(ui) golem# Check that formals have not been removed <- formals(mod_name_ui) fmls for (i in c("id")) { expect_true(i %in% names(fmls)) } })
export = TRUE: exports the module functions (
@export
) with the name provided toname
(@rdname
).
The gap
application includes two modules:
_apps/gap/R
├── mod_scatter_display.R └── mod_var_input.R
mod_var_input
collects the reactive inputs from the UI and passes them to mod_scatter_display
(view the code in these modules here on GitHub).
Utility functions
The scatter_plot()
utility function was created with the utils
argument of add_module()
, so it’s stored the mod_scatter_display_utils.R
file:
<- function(df, x_var, y_var, col_var, alpha_var, size_var) {
scatter_plot ::ggplot(data = df,
ggplot2::aes(x = .data[[x_var]],
ggplot2y = .data[[y_var]],
color = .data[[col_var]])) +
::geom_point(alpha = alpha_var, size = size_var)
ggplot2
}
Utility functions can also be created directly with golem::add_utils()
or golem::add_fct()
.3
Including mod
in the name of module scripts and functions makes it easier to separate them from other functions in my package namespace, if I’m using tab-completion, or if I’m searching for a particular file using Ctrl
+ .
:
UI
golem
UI contents are placed in R/app_ui.R
:
#' The application User-Interface
#'
#' @param request Internal parameter for `{shiny}`.
#' DO NOT REMOVE.
#'
#' @keywords internal
<- function(request) {
app_ui tagList(
# Leave this function for adding external resources
golem_add_external_resources(),
# Your application UI logic
fluidPage(
sidebarLayout(
sidebarPanel(
mod_var_input_ui("vars")
),mainPanel(
fluidRow(
br(),
p(em("Brought to you by: "),
# add golem hex (in www/)
img(src = "www/golem-hex.png", width = "5%")
)
),mod_scatter_display_ui("plot")
)
)
)
) }
Server
The module server functions are added to R/app_server.R
:
#' The application server-side
#'
#' @param input,output,session Internal parameters for {shiny}.
#' DO NOT REMOVE.
#'
#'
#' @keywords internal
<- function(input, output, session) {
app_server # Your application server logic
<- mod_var_input_server("vars")
selected_vars
mod_scatter_display_server("plot", var_inputs = selected_vars)
}
Data
movies.RData
is added to inst/extdata
and loaded into the package with data-raw/movies.R
:
## code to prepare `movies` dataset goes here
<- system.file('extdata/movies.RData', package = 'gap')
pth load(pth)
::use_data(movies, overwrite = TRUE) usethis
After calling usethis::use_data_raw('movies')
, we use system.file()
to locate the inst/extdata/movies.RData
file with the code in data-raw/movies.R
and save it in the data/
folder.
Launch
golem
has helper functions for adding the root app.R
file to launch the application:
golem::add_rstudioconnect_file()
golem::add_shinyserver_file()
golem::add_shinyappsio_file()
The contents of the app.R
file generated from golem::add_rstudioconnect_file()
are below:
# Launch the ShinyApp (Do not remove this comment)
# To deploy, run: rsconnect::deployApp()
# Or use the blue button on top of this file
::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE)
pkgloadoptions( "golem.app.prod" = TRUE)
::run_app() # add parameters here (if any) gap
After running devtools::load_all()
, devtools::document()
, devtools::install()
, the application can be launched from the app.R
file (or by entering gap::run_app()
in the Console).
Unit tests
golem
apps come with boilerplate unit tests via use_recommended_tests()
, use_utils_ui(with_test = TRUE)
, use_utils_server(with_test = TRUE)
.
tests/testthat/
├── test-golem-recommended.R
├── test-golem_utils_server.R
└── test-golem_utils_ui.R
2 directories, 3 files
During development, we can include unit tests for new modules and/or functions using the with_test = TRUE
argument.
System tests
System tests can be performed with shinytest2
(similar to non-package or non-golem
apps). Two example shinytest2
tests can be found in test-shinytest2.R
and test-app-feature-01.R
:
tests
└── testthat
├── fixtures
│ ├── make-tidy_ggp2_movies.R
│ └── tidy_ggp2_movies.rds
├── helper.R
├── setup-shinytest2.R
├── test-app-feature-01.R
└── test-shinytest2.R
test-shinytest2.R
contains the boilerplate test from runningshinytest2::record_test()
:show/hide test-shinytest2.R
library(shinytest2) test_that("{shinytest2} recording: feature-01", { <- AppDriver$new(name = "feature-01", height = 800, width = 1173) app $set_inputs(`vars-y` = "imdb_num_votes") app$set_inputs(`vars-x` = "critics_score") app$set_inputs(`vars-z` = "genre") app$set_inputs(`vars-alpha` = 0.7) app$set_inputs(`vars-size` = 3) app$set_inputs(`vars-plot_title` = "New plot title") app$expect_values() app })
test-app-feature-01.R
containstestthat
s BDD functions describing an app feature and scenario:show/hide test-app-feature-01.R
library(shinytest2) describe("Feature 1: Scatter plot data visualization dropdowns As a film data analyst I want to explore variables in the movie review data So that I can analyze relationships between movie reivew sources", { describe("Scenario A: Change dropdown values for plotting Given the movie review application is loaded When I choose the variable [critics_score] for the x-axis And I choose the variable [imdb_num_votes] for the y-axis And I choose the variable [genre] for the color", { it("Then the scatter plot should show [critics_score] on the x-axis And the scatter plot should show [imdb_num_votes] on the y-axis And the points on the scatter plot should be colored by [genre]", { <- AppDriver$new(name = "feature-01-senario-a", app height = 800, width = 1173) $set_inputs(`vars-y` = "imdb_num_votes") app$set_inputs(`vars-x` = "critics_score") app$set_inputs(`vars-z` = "genre") app$expect_values() app }) }) describe("Scenario B: Change dropdown values for plotting Given the movie review application is loaded When I choose the size of the points to be [3] And I choose the opacity of the points to be [0.7] And I enter '[New plot title]' for the plot title", { it("Then the size of the points on the scatter plot should be [3] And the opacity of the points on the scatter plot should be [0.7] And the title of the plot should be '[New plot title]'", { <- AppDriver$new(name = "feature-01-senario-b", app height = 800, width = 1173) $set_inputs(`vars-alpha` = 0.7) app$set_inputs(`vars-size` = 3) app$set_inputs(`vars-plot_title` = "New plot title") app$expect_values() app }) }) })
Test coverage
The covrpage
package provides a test coverage report in tests/README.md
file. This file includes a report of the R file tested, the unit test context, number of tests, and the test status.4
Adding files and images
To include other files (like images), add the image file to inst/app/www/
, then add the www/
to the path (see example UI code below)
# add icon
::tags$img(src = "www/shiny.png") shiny
If I wanted to include images in their own folder (like images/
), I can use golem::addResourcePath()
to add the name of the sub-folder to inst/app/
# add icon
::add_resource_path(
golemprefix = 'images',
directoryPath = system.file('app/images',
package = 'gap'))
Now I can add the image file to the inst/app/www/images/
folder and include the following code in the UI:
# add icon
::tags$img(src = "www/images/golem-hex.png") shiny
In R/app_ui.R
, the app_ui()
function contains the UI layout functions (fluidPage()
, sidebarLayout()
, etc.), and a call to golem_add_external_resources()
:
#' The application User-Interface
#'
#' @param request Internal parameter for `{shiny}`.
#' DO NOT REMOVE.
#' @import shiny
#' @noRd
<- function(request) {
app_ui tagList(
# Leave this function for adding external resources
golem_add_external_resources(),
# Your application UI logic
fluidPage(
sidebarLayout(
sidebarPanel(
mod_var_ui("vars"),
h6(
img(src = "www/images/shiny.png", width = "15%"),
em(
"The data for this application comes from the ",
a("Building web applications with Shiny",
href = "https://rstudio-education.github.io/shiny-course/"
),"tutorial"
)
)
),mainPanel(
fluidRow(
br(),
p(em("Brought to you by: "),
# add golem hex (in www/images/)
img(src = "www/images/golem-hex.png", width = "5%")
)
),mod_plot_ui("plot")
)
)
)
) }
After running devtools::load_all()
, devtools::document()
, devtools::install()
, the image is properly rendered with the application:
gap::run_app()
gap
inst/
folder
golem
takes advantage of the inst/
folder and R package structure to allow users to provide additional ‘assets’ to the application.
For example, if we use system.file()
on the local directory ('.'
), we see all the folders available to the application at runtime
::dir_tree(path = system.file('', package = 'gap'))
fs/Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/gap/.
├── DESCRIPTION
├── INDEX
├── LICENSE
├── Meta
│ ├── Rd.rds
│ ├── data.rds
│ ├── features.rds
│ ├── hsearch.rds
│ ├── links.rds
│ ├── nsInfo.rds
│ └── package.rds
├── NAMESPACE
├── R
│ ├── gap
│ ├── gap.rdb
│ └── gap.rdx
├── WORDLIST
├── app
│ └── www
│ ├── favicon.ico
│ └── images-hex.png
│ ├── golem
│ └── shiny.png
├── data
│ ├── Rdata.rdb
│ ├── Rdata.rds
│ └── Rdata.rdx
├── extdata
│ └── movies.RData-config.yml
├── golem
├── help
│ ├── AnIndex
│ ├── aliases.rds
│ ├── gap.rdb
│ ├── gap.rdx
│ └── paths.rds
└── html00Index.html
├── └── R.css
Adding resources
golem
has multiple functions for creating and using external files in your Shiny app-package. The add_dockerfile*
set of functions are particularly helpful for including Docker files. I’ll cover two here: golem::add_dockerfile()
and golem::add_dockerfile_with_renv()
.
In the gap
directory, you’ll see the Dockerfile
from add_dockerfile()
. The files from add_dockerfile_with_renv()
are in the inst/docker-renv
folder..
add_dockerfile
add_dockerfile()
results in the following Dockerfile
:
FROM rocker/verse:4.3.2
RUN apt-get update && apt-get install -y libicu-dev libxml2-dev make pandoc zlib1g-dev && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/
RUN echo "options(repos = c(CRAN = 'https://cran.rstudio.com/'), download.file.method = 'libcurl', Ncpus = 4)" | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site
RUN R -e 'install.packages("remotes")'
RUN Rscript -e 'remotes::install_version("rlang",upgrade="never", version = "1.1.2")'
RUN Rscript -e 'remotes::install_version("knitr",upgrade="never", version = "1.45")'
RUN Rscript -e 'remotes::install_version("stringr",upgrade="never", version = "1.5.1")'
RUN Rscript -e 'remotes::install_version("shiny",upgrade="never", version = "1.8.0")'
RUN Rscript -e 'remotes::install_version("config",upgrade="never", version = "0.3.2")'
RUN Rscript -e 'remotes::install_version("spelling",upgrade="never", version = "2.2.1")'
RUN Rscript -e 'remotes::install_version("rmarkdown",upgrade="never", version = "2.25")'
RUN Rscript -e 'remotes::install_version("golem",upgrade="never", version = "0.4.1")'
RUN Rscript -e 'remotes::install_version("ggplot2",upgrade="never", version = "3.4.4")'
RUN Rscript -e 'remotes::install_github("rstudio/htmltools@a8a3559edbfd9dda78418251e69273fa9dfeb9bc")'
RUN Rscript -e 'remotes::install_github("r-lib/testthat@fe50a222c62cc8733b397690caf3b2a95856f902")'
RUN mkdir /build_zone
ADD . /build_zone
WORKDIR /build_zone
RUN R -e 'remotes::install_local(upgrade="never")'
RUN rm -rf /build_zone
EXPOSE 80
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"
This Dockerfile
sets up a Docker image with specific R/Linux packages, adds and installs the gap app-package from the current directory, and configures the container to run the Shiny app on port 80.
FROM rocker/verse:4.3.2
which is a pre-built R environment.RUN apt-get update && apt-get install -y
updates and installs several Linux packages:libcurl4-openssl-dev
: These are the development files and libraries forlibcurl
, which is used for for secure data transfer operations in R packages (theopenssl
indicates OpenSSL support).libicu-dev
: The development files and libraries for the International Components for Unicode library.libssl-dev
: The development files and libraries for SSL libraries, necessary for secure communications over networks.libxml2-dev
: Development files for thelibxml2
library, which is used for parsing XML and HTML documents.make
: themake
build utility that automatically builds executable programs and libraries from source code.pandoc
: the universal document converter used for converting markdown files to various other formatszlib1g-dev
: development files for thezlib
compression libraryrm -rf /var/lib/apt/lists/*
cleans up the package list to reduce the image size.
RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/
creates two directories (/usr/local/lib/R/etc/
and/usr/lib/R/etc/
) for R configuration filesRUN echo "options(...) | tee ...
configures global R settings (CRAN repository, download method, and number of CPU cores)RUN R -e 'install.packages("remotes")'
installs theremotes
packageRUN Rscript -e 'remotes::install_version(...)'
is used to installs specific versions of R packages used in the gap app-package without upgrading dependencies.RUN Rscript -e 'remotes::install_github(...)'
is used to install R packages directly from GitHub repositories (at specific commits).RUN mkdir /build_zone
will create a directory in the image for building the application.ADD . /build_zone
adds the contents of the current directory (from where the Docker build command is run) into the/build_zone
directory in the image.WORKDIR /build_zone
sets the/build_zone
directory as the working directory for all subsequent commands.RUN R -e 'remotes::install_local(upgrade="never")'
installs the R package located in/build_zone
without upgrading dependencies (this is ourgap
app-package being containerized).RUN rm -rf /build_zone
reduce the final image size by removing the/build_zone
directory.EXPOSE 80` exposes port 80 of the container (this is typically used for web applications like Shiny apps).
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"
specifies the command to be run when the Docker container starts. These calls set the port and host Shiny app options, loadsgap
, and runs the standalone app function (gap::run_app()
).
add_dockerfile_with_renv
add_dockerfile_with_renv()
creates a tmp/deploy
folder and adds the following files:
deploy/
├── Dockerfile
├── Dockerfile_base
├── README
├── gap_0.0.0.9000.tar.gz
└── renv.lock.prod
Dockerfile_base
is used to create a base image with necessary dependencies and configurations
FROM rocker/verse:4.3.2
RUN apt-get update -y && apt-get install -y make zlib1g-dev git libicu-dev && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/
RUN echo "options(renv.config.pak.enabled = FALSE, repos = c(CRAN = 'https://cran.rstudio.com/'), download.file.method = 'libcurl', Ncpus = 4)" | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_version("renv", version = "1.0.3")'
COPY renv.lock.prod renv.lock
RUN R -e 'renv::restore()'
Each instruction does the following:
RUN apt-get update -y && apt-get install
updates and installs several Linux packages (make
,zlib1g-dev
,git
,libicu-dev
)RUN mkdir
andRUN echo
creates directories and configures R settings (CRAN repository, download method, number of CPUs to use)RUN R -e
installs theremotes
package and version 1.0.3 of therenv
package (renv
is used for project-local package management)COPY
copies a filerenv.lock.prod
into the image asrenv.lock
RUN R -e
usesrenv::restore()
to install the R packages specified in this lock file
Dockerfile
builds on this base image to set up the gap
app-package and its dependencies.
FROM gap_base
COPY renv.lock.prod renv.lock
RUN R -e 'renv::restore()'
COPY gap_*.tar.gz /app.tar.gz
RUN R -e 'remotes::install_local("/app.tar.gz",upgrade="never")'
RUN rm /app.tar.gz
EXPOSE 80
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"
- These instructions perform the following:
FROM gap_base
starts fromgap_base
, which is the image built usingDockerfile_base
.COPY
&RUN R -e
repeats the step of copyingrenv.lock.prod
and restoring packages usingrenv::restore()
.COPY gap_*.tar.gz /app.tar.gz
copies files matchinggap_*.tar.gz
(presumably our compressed app-package) into the image as/app.tar.gz
.RUN R -e
installs this local package usingremotes::install_local()
.RUN rm
reduce the size of the final image by removing the copiedtar.gz
file.EXPOSE 80
exposes port 80 (necessary for external access to the app when it’s running inside the container).CMD R -e
defines the command to run when the container starts, which in this case, runs thegap
application (gap::run_app()
) on port80
and (bound to0.0.0.0
).
The docker build
and docker run
commands in the README
create the base image from Dockerfile_base
, start the container from the Dockerfile
image, and launche the gap
application on port 80
.
docker build -f Dockerfile_base --progress=plain -t gap_base .
docker build -f Dockerfile --progress=plain -t gap:latest .
docker run -p 80:80 gap:latest
# then go to 127.0.0.1:80
gap_0.0.0.9000.tar.gz
is the app-package to deploy in the Docker container.
Dependencies
Below is a quick overview of the dependencies in gap
.
NAMESPACE
The namespace file for gap
is importing shiny
(and the .data
operator from rlang
).
# Generated by roxygen2: do not edit by hand
export(run_app)
import(shiny) importFrom(rlang,.data)
As you can see, we’re only exporting the run_app()
function from gap
.
Imports
The Imports
field in the DESCRIPTION
file lists the following:
Imports:
config (>= 0.3.1),
ggplot2,
golem (>= 0.3.5),
rlang,
shiny (>= 1.7.4),
stringr, tools
As we can see, golem
apps add golem
as a dependency:
::local_deps_explain(
pakdeps = 'golem',
root = "_apps/gap")
ℹ Loading metadata database
✔ Loading metadata database ... done
-> golem gap
Recap
If you typically build Shiny apps in a single app.R
file (or in ui.R
and server.R
files), the golem
framework might seem overwhelming. I’ll give a quick overview of some areas I found confusing when I started using goelm
:
dev/
containsgolem
‘s ’guided tour’ scripts (01_start.R
,02_dev.R
,03_deploy.R
) andrun_dev.R
R/
: the primary app files for the UI and server are stored in theR/
folder (R/app_ui.R
,R/app_server.R
,R/run_app.R
), as well as the configuration function (R/app_config.R
)golem
apps are run using thegap::run_app()
function (included in theR/
folder)While developing,
golem
also comes with arun_dev
function that reads theR/run_dev.R
file and evaluates the code.The
inst/
folder holds thegolem-config.yml
and location of any external app files.Deploying the application can be done with a single function:
rsconnect::deployApp()
Generally speaking, golem
’s start-up scripts save time and serve as a gentle introduction to some of the functions used in R package development.
The add_
functions are an area where golem
really separates itself from standard R package development. Having dedicated Shiny development functions (and the app/inst/www
folder) reduces the cognitive overhead of mapping the standard R package development functions (i.e., those from usethis
and devtools
) into Shiny app-package development.
golem
is a popular framework for a reason–it’s designed to allow developers to build a shiny application and R package simultaneously. Added bonuses include taking advantage of RStudio’s build tools, great documentation, and user-guides..
Below is an overview of the features/functions in the golem
framework:
Feature | Arguments/Options | Description/Comments |
---|---|---|
dev/ scripts |
|
These files are automatically included in new golem apps and walk through set up, development, and deployment |
fill_desc() : fills DESCRIPTION file |
Arguments are passed as strings without having to worry about formatting (i.e., utils::person() ). |
Includes many necessary fields often overlooked when using usethis::create_package() |
attachment::att_amend_desc() |
Updates the package dependencies in the Imports field of DESCRIPTION |
Although not part of the golem package, attachment is built by the fine folks at ThinkR and makes managing dependencies smoother. |
set_golem_options() |
This sets a variety of options in the golem-config.yml file (most notably the name, version, and path to your app-package). |
|
use_recommended_tests() |
Create testthat infrastructure and adds a collection of boilerplate tests in the tests/testthat/ folder. |
|
use_utils_ui() & use_utils_server() |
with_test is set to TRUE |
Creates a collection of commonly used UI and server functions (and accompanying tests). |
add_module("name", fct, utils, export, with_test) |
|
This is one of the best features in An added bonus is a consistent file naming convention. |
add_fct() and add_utils() |
with_test : creates the accompanying tests/testthat/test-[name].R file. |
These are essentially wrappers for usethis::use_r() and usethis::use_test() |
Adding non-R code files:
|
Each
|
Each of these functions create the necessary files in the inst/app folder. |
Footnotes
The tests
golem
creates in thetests/testthat/
folder can serve as a nice guide for users new totestthat
↩︎New functions created with
golem::add_*()
functions are placed in theR/
folder with a@noRd
tag by default (this behavior can be changed with theexport
argument).↩︎Test coverage is only included for the three initial
golem
test files (test-golem-recommended.R
,test-golem_utils_server.R
,test-golem_utils_ui.R
).↩︎