Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
export(set_github_action)
export(set_gitlab_ci)
export(shiny2docker)
export(shiny2docker_uvr)
importFrom(attachment,create_renv_for_prod)
importFrom(cli,cli_alert_danger)
importFrom(cli,cli_alert_info)
importFrom(cli,cli_alert_success)
importFrom(dockerfiler,Dockerfile)
importFrom(dockerfiler,dock_from_renv)
importFrom(here,here)
importFrom(yesno,yesno2)
67 changes: 67 additions & 0 deletions R/shiny2docker_uvr.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#' shiny2docker_uvr
#'
#' Generate a Dockerfile for a Shiny application using
#' [uvr](https://github.com/nbafrank/uvr) instead of `renv`. The R version is
#' installed inside the container by `uvr` (no `rocker/r-ver` base image
#' required) and packages are restored from `uvr.lock` via `uvr sync --frozen`.
#'
#' Lifecycle: experimental. Requires the project to already be uvr-managed
#' (`uvr.toml`, `uvr.lock`, and `.r-version` present at the project root).
#'
#' @param path Character. Path to the folder containing the Shiny application.
#' @param output Character. Path to the generated Dockerfile.
#' @param uvr_toml Path to the `uvr.toml` manifest. Defaults to `<path>/uvr.toml`.
#' @param uvr_lock Path to the `uvr.lock` lockfile. Defaults to `<path>/uvr.lock`.
#' @param r_version_file Path to the R version pin. Defaults to `<path>/.r-version`.
#' @param base_image Character. Docker base image. Defaults to `"debian:stable-slim"`.
#' @param uvr_version Character. `"latest"` or a semver tag like `"v0.3.1"`.
#' Pinning a tag is recommended for reproducible builds.
#' @param port Integer. Shiny port to expose. Default `3838`.
#' @param host Character. Shiny host. Default `"0.0.0.0"`.
#' @param extra_sysreqs Character vector of extra debian packages to install
#' before `uvr sync` (escape hatch for system dependencies that uvr does not
#' detect on its own).
#' @param write Logical. Whether to write the Dockerfile to `output`. Default `TRUE`.
#'
#' @return Invisibly, an R6 `dockerfiler::Dockerfile` object that can be further
#' customised before writing.
#'
#' @export
#'
#' @importFrom dockerfiler Dockerfile
shiny2docker_uvr <- function(path = ".",
output = file.path(path, "Dockerfile"),
uvr_toml = file.path(path, "uvr.toml"),
uvr_lock = file.path(path, "uvr.lock"),
r_version_file = file.path(path, ".r-version"),
base_image = "debian:stable-slim",
uvr_version = "latest",
port = 3838,
host = "0.0.0.0",
extra_sysreqs = NULL,
write = TRUE) {

uvr_assert_state(
uvr_toml = uvr_toml,
uvr_lock = uvr_lock,
r_version_file = r_version_file
)

if (!file.exists(file.path(dirname(output), ".dockerignore"))) {
create_dockerignore_uvr(path = file.path(dirname(output), ".dockerignore"))
}
Comment thread
VincentGuyader marked this conversation as resolved.
Outdated

dock <- uvr_build_dockerfile(
base_image = base_image,
uvr_version = uvr_version,
port = port,
host = host,
extra_sysreqs = extra_sysreqs
)

if (isTRUE(write)) {
dock$write(output)
}

invisible(dock)
}
198 changes: 198 additions & 0 deletions R/utils_uvr.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#' Write a uvr-aware .dockerignore
#'
#' Adds the entries that are specific to a uvr-managed project (`.uvr/`,
#' `.Rprofile` written by `uvr init`) on top of the defaults used by the
#' `renv` backend.
#'
#' @param path Path to the `.dockerignore` to write.
#' @noRd
create_dockerignore_uvr <- function(path = ".dockerignore") {
entries <- c(
".Rhistory",
".git",
".gitignore",
".Rproj.user",
"manifest.json",
"rsconnect/",
".uvr/",
"renv/",
"renv.lock"
)
Comment thread
VincentGuyader marked this conversation as resolved.
writeLines(entries, con = path)
}

#' Assert that the project is uvr-ready
#'
#' Checks that `uvr.toml`, `uvr.lock` and a R version pin file all exist, and
#' that `uvr.lock` is at least as recent as `uvr.toml`. Stops with an actionable
#' message otherwise.
#'
#' @param uvr_toml Path to `uvr.toml`.
#' @param uvr_lock Path to `uvr.lock`.
#' @param r_version_file Path to `.r-version`.
#'
#' @return Invisibly `TRUE` on success.
#' @noRd
uvr_assert_state <- function(uvr_toml, uvr_lock, r_version_file) {

if (!file.exists(uvr_toml)) {
stop(
"uvr.toml not found at '", uvr_toml, "'.\n",
"Run `uvr init` (or `uvr import` from an existing renv.lock), then `uvr lock`.",
call. = FALSE
)
}
if (!file.exists(uvr_lock)) {
stop(
"uvr.lock not found at '", uvr_lock, "'.\n",
"Run `uvr lock` to generate it.",
call. = FALSE
)
}
if (file.info(uvr_lock)$mtime < file.info(uvr_toml)$mtime) {
stop(
"uvr.lock is older than uvr.toml — manifest changed since last lock.\n",
"Run `uvr lock` to refresh it.",
call. = FALSE
)
}
if (!file.exists(r_version_file)) {
stop(
".r-version not found at '", r_version_file, "'.\n",
"Run `uvr r pin <version>` to pin the R version for the project.",
call. = FALSE
)
}

invisible(TRUE)
}
Comment thread
VincentGuyader marked this conversation as resolved.
Comment thread
VincentGuyader marked this conversation as resolved.

#' Build the uvr release download URL
#'
#' @param uvr_version Either `"latest"` or a tag like `"v0.3.1"` / `"0.3.1"`.
#' @return A list with `url` and `arg_value`. `arg_value` is `NA` when
#' `uvr_version = "latest"` (no `ARG UVR_VERSION` injected).
#' @noRd
uvr_release_url <- function(uvr_version = "latest") {
asset <- "uvr-x86_64-unknown-linux-gnu.tar.gz"
base <- "https://github.com/nbafrank/uvr/releases"

if (identical(uvr_version, "latest")) {
return(list(
url = sprintf("%s/latest/download/%s", base, asset),
arg_value = NA_character_
))
}

tag <- if (startsWith(uvr_version, "v")) uvr_version else paste0("v", uvr_version)
if (!grepl("^v[0-9]+\\.[0-9]+\\.[0-9]+", tag)) {
stop(
"uvr_version must be 'latest' or a semver tag like 'v0.3.1' / '0.3.1'. ",
"Got: '", uvr_version, "'",
call. = FALSE
)
}
Comment thread
VincentGuyader marked this conversation as resolved.
list(
url = sprintf("%s/download/%s/%s", base, tag, asset),
arg_value = tag
)
}

#' Build the Dockerfile object for the uvr backend
#'
#' @param base_image Base OS image (e.g. `"debian:stable-slim"`).
#' @param uvr_version `"latest"` or a tag (`"v0.3.1"`).
#' @param port Shiny port.
#' @param host Shiny host.
#' @param extra_sysreqs Optional character vector of extra debian packages to
#' `apt-get install` before `uvr sync`.
#'
#' @return An R6 `dockerfiler::Dockerfile` object.
#' @noRd
uvr_build_dockerfile <- function(base_image = "debian:stable-slim",
uvr_version = "latest",
port = 3838,
host = "0.0.0.0",
extra_sysreqs = NULL) {

release <- uvr_release_url(uvr_version)

dock <- dockerfiler::Dockerfile$new(FROM = base_image)

dock$ENV("DEBIAN_FRONTEND", "noninteractive")
dock$ENV("LANG", "en_US.UTF-8")
dock$ENV("LC_ALL", "en_US.UTF-8")

dock$RUN(paste(
"apt-get update && apt-get install -y --no-install-recommends",
# base tooling
"ca-certificates curl locales tzdata",
# binutils provides `ar`, used by `uvr r install` to extract the Posit .deb
"binutils",
# runtime + build deps required by Posit's R .deb
"g++ gcc gfortran make zip unzip ucf",
"libbz2-dev libc6 libcairo2 libcurl4-openssl-dev libdeflate-dev",
"libglib2.0-0 libgomp1 libicu-dev liblzma-dev libopenblas-dev",
"libpango-1.0-0 libpangocairo-1.0-0 libpaper-utils libpcre2-dev",
"libpng16-16 libreadline8 libtcl8.6 libtiff6 libtirpc-dev libtk8.6",
"libx11-6 libxt6 zlib1g-dev",
# extra deps commonly needed when uvr falls back to source compilation
# (P3M binaries are not always available on debian targets as of uvr 0.2.15)
"libuv1-dev libxml2-dev libssl-dev libsodium-dev",
"&& sed -i '/en_US.UTF-8/s/^# //' /etc/locale.gen && locale-gen",
"&& rm -rf /var/lib/apt/lists/*"
))

if (!is.na(release$arg_value)) {
dock$ARG(paste0("UVR_VERSION=", release$arg_value))
}

Comment on lines +483 to +486

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARG UVR_VERSION=... is injected but the download RUN uses a fully resolved URL and never references $UVR_VERSION, so the build arg is effectively unused and can't be overridden via --build-arg. Either remove the ARG line, or use it in the download URL to make it meaningful.

Suggested change
if (!is.na(release$arg_value)) {
dock$ARG(paste0("UVR_VERSION=", release$arg_value))
}

Copilot uses AI. Check for mistakes.
dock$RUN(paste0(
"curl -fsSL ", release$url,
" | tar xz -C /usr/local/bin uvr && uvr --version"
))

dock$WORKDIR("/srv/shiny-server")
dock$COPY(from = ".r-version", to = "./")
dock$COPY(from = "uvr.toml", to = "./")
dock$COPY(from = "uvr.lock", to = "./")
Comment thread
VincentGuyader marked this conversation as resolved.

# Workaround for nbafrank/uvr <= 0.2.15: `uvr r install` writes
# `etc/Renviron.site` directly under <r-versions>/<ver>/ but fails because
# that `etc/` dir doesn't exist (Posit's .deb places etc under lib/R/).
# Pre-creating the dir lets the install complete. Track upstream and drop
# this once fixed.
dock$RUN(paste(
"mkdir -p \"/root/.uvr/r-versions/$(cat .r-version)/etc\"",
"&& uvr r install \"$(cat .r-version)\""
))

if (length(extra_sysreqs) > 0) {
dock$RUN(paste(
"apt-get update && apt-get install -y --no-install-recommends",
paste(extra_sysreqs, collapse = " "),
"&& rm -rf /var/lib/apt/lists/*"
))
Comment thread
VincentGuyader marked this conversation as resolved.
}

# Note: ideally `uvr sync --frozen` for CI strictness, but as of uvr 0.2.15
# the frozen check rejects locks generated on a different host than the
# container's target OS (e.g. host-side P3M URL vs CRAN source URL). Track
# upstream and switch back to --frozen once that's resolved.
dock$RUN("apt-get update && uvr sync && rm -rf /var/lib/apt/lists/*")

dock$COPY(from = ".", to = "/srv/shiny-server/")

# `uvr run` takes a script, not arbitrary R args, so embed the launch as a
# tiny R file written into the image at build time.
dock$RUN(sprintf(
"printf '%%s\\n' \"shiny::runApp(appDir = '/srv/shiny-server', host = '%s', port = %s)\" > /srv/shiny-server/_uvr_start.R",
host, port
Comment thread
VincentGuyader marked this conversation as resolved.
Outdated
))

dock$EXPOSE(port)

dock$CMD("uvr run /srv/shiny-server/_uvr_start.R")

dock
}
60 changes: 60 additions & 0 deletions man/shiny2docker_uvr.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/testthat/fixtures/uvr-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.uvr/library/
1 change: 1 addition & 0 deletions tests/testthat/fixtures/uvr-app/.r-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.4.2
9 changes: 9 additions & 0 deletions tests/testthat/fixtures/uvr-app/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
library(shiny)
ui <- fluidPage(
titlePanel("uvr POC"),
textOutput("msg")
)
server <- function(input, output, session) {
output$msg <- renderText("Hello from a uvr-built shiny container.")
}
shinyApp(ui, server)
Loading
Loading