| Title: | Containerize Your 'R' Project |
|---|---|
| Description: | Provides tools for containerizing 'R' projects. Reads an 'renv' lock file, generates a ready-to-use 'Dockerfile', builds the container image, and pushes it to a registry. Generated 'COPY' instructions preserve the local directory structure inside the container. Cross-platform builds are supported via 'Docker' buildx for targeting 'linux/amd64' from ARM hosts. Designed to help researchers build portable, reproducible workflows that can be reliably shared, archived, and rerun across systems. See R Core Team (2025) <https://www.R-project.org/>, Ushey et al. (2025) <https://CRAN.R-project.org/package=renv>, and Docker Inc. (2025) <https://www.docker.com/>. |
| Authors: | Erwin Lares [aut, cre] (ORCID: <https://orcid.org/0000-0002-3284-828X>) |
| Maintainer: | Erwin Lares <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.2.0.9000 |
| Built: | 2026-10-10 01:05:00 UTC |
| Source: | https://github.com/erwinlares/containr |
build_image() builds a container image from a Dockerfile using either
podman or docker. It auto-detects which tool is available on the system
unless tool_preference is set to a single value. Use dry_run = TRUE to
preview the exact command that would be run without executing it.
build_image( dockerfile = "Dockerfile", tag = NULL, platform = "linux/amd64", tool_preference = c("podman", "docker"), dry_run = FALSE, verbose = FALSE, comments = FALSE )build_image( dockerfile = "Dockerfile", tag = NULL, platform = "linux/amd64", tool_preference = c("podman", "docker"), dry_run = FALSE, verbose = FALSE, comments = FALSE )
dockerfile |
A character string. Path to the |
tag |
A character string or |
platform |
A character string or |
tool_preference |
A non-empty character vector of container tools to
try, in order. Defaults to |
dry_run |
Logical. If |
verbose |
Logical. If |
comments |
Logical. If |
When the target platform differs from the host architecture (e.g.
building linux/amd64 on an Apple Silicon Mac), build_image()
automatically uses docker buildx build instead of docker build, and
includes --load to ensure the image is available in the local store.
For podman, --platform is passed directly to podman build.
If tag was supplied, the same character string, returned
invisibly – so it can be captured or piped straight into
push_image(image_id = ) without retyping it (C-G1). If tag is
NULL (the default), there is no name to hand back yet; returns
invisible(NULL), and list_images() is how you find the built
image's ID.
Before calling build_image(), ensure the following are in place:
A Dockerfile exists at dockerfile. Use
generate_dockerfile() to create one if needed.
An renv.lock file is present in the build context – the generated
Dockerfile uses it to restore the R package environment inside the
container.
Either podman or docker is installed and the daemon (for docker)
or the Podman service is running. Verify with podman info or
docker info in a terminal.
Building for a different architecture than the host requires emulation.
On Apple Silicon Macs, building linux/amd64 images uses QEMU emulation
under Podman, which can be slow and unstable. Docker Desktop handles
cross-platform builds more reliably via buildx and Rosetta 2.
If builds fail with QEMU segfaults, consider:
Using Docker Desktop instead of Podman (tool_preference = "docker")
Building on a native x86_64 machine (e.g. via GitHub Actions)
Building directly on the target cluster if it supports container builds
For UW-Madison's DoIT GitLab Container Registry (the registry push_image()
currently supports), the full tag format is:
registry.doit.wisc.edu/<namespace>/<image-name>:<version>
For example: registry.doit.wisc.edu/erwin.lares/my-analysis:1.0.0
The version tag defaults to latest if omitted. Using explicit version
tags (e.g. 1.0.0) is recommended for reproducibility – latest will
be overwritten each time you push.
## Not run: # Build for linux/amd64 (default) with auto-detected tool build_image() # Build and tag for the DoIT GitLab Container Registry build_image(tag = "registry.doit.wisc.edu/namespace/my-analysis:1.0.0") # Build for the host architecture (no --platform flag) build_image(platform = NULL) # Build for ARM64 (e.g. local use on Apple Silicon) build_image(platform = "linux/arm64") # Preview the build command without running it build_image( tag = "registry.doit.wisc.edu/namespace/my-analysis:1.0.0", dry_run = TRUE ) # Guided build for first-time users build_image( tag = "registry.doit.wisc.edu/namespace/my-analysis:1.0.0", verbose = TRUE, comments = TRUE ) # Chain straight into push_image() -- build_image() returns its own # tag argument invisibly, and push_image()'s first parameter is # image_id, so a simple local tag flows through the pipe into the # full registry destination with nothing retyped or captured in an # intermediate variable (C-G1) build_image(tag = "my-analysis:dev") |> push_image(namespace = "erwin.lares", project = "my-analysis", tag = "1.0.0") ## End(Not run)## Not run: # Build for linux/amd64 (default) with auto-detected tool build_image() # Build and tag for the DoIT GitLab Container Registry build_image(tag = "registry.doit.wisc.edu/namespace/my-analysis:1.0.0") # Build for the host architecture (no --platform flag) build_image(platform = NULL) # Build for ARM64 (e.g. local use on Apple Silicon) build_image(platform = "linux/arm64") # Preview the build command without running it build_image( tag = "registry.doit.wisc.edu/namespace/my-analysis:1.0.0", dry_run = TRUE ) # Guided build for first-time users build_image( tag = "registry.doit.wisc.edu/namespace/my-analysis:1.0.0", verbose = TRUE, comments = TRUE ) # Chain straight into push_image() -- build_image() returns its own # tag argument invisibly, and push_image()'s first parameter is # image_id, so a simple local tag flows through the pipe into the # full registry destination with nothing retyped or captured in an # intermediate variable (C-G1) build_image(tag = "my-analysis:dev") |> push_image(namespace = "erwin.lares", project = "my-analysis", tag = "1.0.0") ## End(Not run)
generate_dockerfile() inspects an R project's dependencies via an renv
lockfile and writes a ready-to-use Dockerfile to the specified output
directory. It supports multiple Rocker base images, automatic system
library detection, Quarto installation, file copying, user creation, and
inline documentation comments.
generate_dockerfile( r_version = "current", r_mode = "base", auto_syslibs = TRUE, install_syslibs = NULL, output = ".", data_file = NULL, code_file = NULL, misc_file = NULL, add_user = NULL, home_dir = "/home", expose_port = "8787", install_quarto = FALSE, quarto_version = "latest", os_version = NULL, comments = FALSE, verbose = FALSE, config = NULL, binary_packages = TRUE, ppm_snapshot = "latest" )generate_dockerfile( r_version = "current", r_mode = "base", auto_syslibs = TRUE, install_syslibs = NULL, output = ".", data_file = NULL, code_file = NULL, misc_file = NULL, add_user = NULL, home_dir = "/home", expose_port = "8787", install_quarto = FALSE, quarto_version = "latest", os_version = NULL, comments = FALSE, verbose = FALSE, config = NULL, binary_packages = TRUE, ppm_snapshot = "latest" )
r_version |
A character string specifying the R version to use, e.g.
|
r_mode |
A character string selecting the Rocker base image. Inspired
by the Rocker Project. One of |
auto_syslibs |
Logical. If |
install_syslibs |
A character vector or |
output |
A character string. Directory path where the |
data_file |
A character vector or |
code_file |
A character vector or |
misc_file |
A character vector or |
add_user |
A character string. Name of a Linux user to create inside
the container with sudo access. Defaults to |
home_dir |
A character string. The working directory set inside the
container via |
expose_port |
A character string. Overrides the port exposed when
|
install_quarto |
Logical. If |
quarto_version |
Character string. Either |
os_version |
A character string or |
comments |
Logical. If |
verbose |
Logical. If |
config |
A character string. Path to a |
binary_packages |
Logical. If |
ppm_snapshot |
A character string. Which Package Manager snapshot
|
Called for its side effects. Writes a Dockerfile to output.
Returns invisible(NULL).
generate_dockerfile() requires an renv.lock file in the current working
directory. Create one with renv::snapshot() before calling this function.
If the lock file is out of sync with your project library, a warning is
issued – run renv::snapshot() to update it before building the image.
If the project uses Quarto with branding assets (i.e. toolero::create_qmd()
was called with use_style = TRUE), the assets/ folder must be copied
into the container alongside the .qmd file or Quarto will be unable to
resolve the CSS and HTML includes at render time. The simplest way to
ensure this is to pass misc_file = "assets/" – or
misc_file = c("assets/", other_files) if additional files are needed –
when calling generate_dockerfile(). No code change is required;
misc_file already accepts directories and copies them whole.
## Not run: # Requires renv.lock in the current working directory. # Run renv::snapshot() first if you don't have one. # Generate a minimal Dockerfile using a pinned R version. output defaults # to ".", so this writes to the current working directory -- the same # place build_image() looks by default. generate_dockerfile(r_version = "4.4.0") # Pin a specific R version with the tidyverse image generate_dockerfile(r_version = "4.3.0", r_mode = "tidyverse") # Add extra system libraries on top of auto-detected ones generate_dockerfile( r_version = "4.4.0", install_syslibs = c("libuv1-dev", "libwebp-dev"), output = "." ) # Include a data file -- directory structure is preserved in the container. # No code_file: for cluster jobs the script travels with each job. generate_dockerfile( r_version = "4.3.0", data_file = "data-raw/penguins.csv", comments = TRUE, output = "." ) # Several data files and a whole assets folder -- pass assets/ via # misc_file so that branding files (styles.css, header.html, footer.html) # are present inside the container when Quarto renders the .qmd generate_dockerfile( r_version = "4.3.0", data_file = c("data-raw/penguins.csv", "data-raw/islands.csv"), misc_file = "assets/", output = "." ) # Serve a Shiny app -- the image runs the app itself, so its code goes in # via code_file. Files land under /srv/shiny-server/ automatically generate_dockerfile( r_version = "4.3.0", r_mode = "shiny_server", code_file = "app.R", output = "." ) # RStudio Server plus Shiny Server in the same image generate_dockerfile( r_version = "4.3.0", r_mode = "rstudio_shiny", code_file = "app.R", output = "." ) # Install Quarto, pinned to a specific release rather than whatever is # currently latest -- the resolved version is recorded as # ENV QUARTO_VERSION in the generated Dockerfile either way generate_dockerfile( r_version = "4.3.0", install_quarto = TRUE, quarto_version = "1.5.57", output = "." ) # Fill in data_file and misc_file from a toolero project config instead # of retyping paths the project already declares (code_file is never # filled in from config) generate_dockerfile( r_version = "4.3.0", config = "_toolero.yml", output = "." ) ## End(Not run)## Not run: # Requires renv.lock in the current working directory. # Run renv::snapshot() first if you don't have one. # Generate a minimal Dockerfile using a pinned R version. output defaults # to ".", so this writes to the current working directory -- the same # place build_image() looks by default. generate_dockerfile(r_version = "4.4.0") # Pin a specific R version with the tidyverse image generate_dockerfile(r_version = "4.3.0", r_mode = "tidyverse") # Add extra system libraries on top of auto-detected ones generate_dockerfile( r_version = "4.4.0", install_syslibs = c("libuv1-dev", "libwebp-dev"), output = "." ) # Include a data file -- directory structure is preserved in the container. # No code_file: for cluster jobs the script travels with each job. generate_dockerfile( r_version = "4.3.0", data_file = "data-raw/penguins.csv", comments = TRUE, output = "." ) # Several data files and a whole assets folder -- pass assets/ via # misc_file so that branding files (styles.css, header.html, footer.html) # are present inside the container when Quarto renders the .qmd generate_dockerfile( r_version = "4.3.0", data_file = c("data-raw/penguins.csv", "data-raw/islands.csv"), misc_file = "assets/", output = "." ) # Serve a Shiny app -- the image runs the app itself, so its code goes in # via code_file. Files land under /srv/shiny-server/ automatically generate_dockerfile( r_version = "4.3.0", r_mode = "shiny_server", code_file = "app.R", output = "." ) # RStudio Server plus Shiny Server in the same image generate_dockerfile( r_version = "4.3.0", r_mode = "rstudio_shiny", code_file = "app.R", output = "." ) # Install Quarto, pinned to a specific release rather than whatever is # currently latest -- the resolved version is recorded as # ENV QUARTO_VERSION in the generated Dockerfile either way generate_dockerfile( r_version = "4.3.0", install_quarto = TRUE, quarto_version = "1.5.57", output = "." ) # Fill in data_file and misc_file from a toolero project config instead # of retyping paths the project already declares (code_file is never # filled in from config) generate_dockerfile( r_version = "4.3.0", config = "_toolero.yml", output = "." ) ## End(Not run)
list_images() returns a data frame of container images currently stored
in the local image store, as reported by podman image ls or
docker image ls. This is useful for finding the image ID to pass to
push_image() after building an image with build_image().
list_images(tool_preference = c("podman", "docker"), verbose = FALSE)list_images(tool_preference = c("podman", "docker"), verbose = FALSE)
tool_preference |
A non-empty character vector of container tools to
try, in order. Defaults to |
verbose |
Logical. If |
A data frame with five columns: repository, tag, image_id,
created, and size. Rows where both repository and tag are
<none> correspond to untagged images produced by build_image() when
no tag argument was supplied. Returns an empty data frame if no
images are found.
After calling build_image(), run list_images() to find the image ID
of the image you just built. Untagged images appear with <none> in the
repository and tag columns – the image_id column contains the hash
you need to pass to push_image():
imgs <- list_images() push_image( image_id = imgs$image_id[1], namespace = "erwin.lares", project = "my-analysis" )
## Not run: # List all local images and print them (verbose = TRUE) list_images(verbose = TRUE) # Capture the result for programmatic use, without printing (C17) imgs <- list_images() imgs$image_id[1] ## End(Not run)## Not run: # List all local images and print them (verbose = TRUE) list_images(verbose = TRUE) # Capture the result for programmatic use, without printing (C17) imgs <- list_images() imgs$image_id[1] ## End(Not run)
push_image() tags a locally built container image with a full registry
path and pushes it to a container registry. It handles both the
podman tag and podman push steps in a single call. Auto-detects
which container tool is available unless tool_preference is set to a
single value.
Use dry_run = TRUE to preview the exact commands without executing them.
The full path format is {registry}/{namespace}/{project}:{tag} – for
the default registry, e.g.
registry.doit.wisc.edu/<namespace>/<project-name>:<version>.
push_image( image_id = NULL, namespace = NULL, project = NULL, tag = "latest", registry = "registry.doit.wisc.edu", tool_preference = c("podman", "docker"), check_login = TRUE, dry_run = FALSE, verbose = FALSE, comments = FALSE )push_image( image_id = NULL, namespace = NULL, project = NULL, tag = "latest", registry = "registry.doit.wisc.edu", tool_preference = c("podman", "docker"), check_login = TRUE, dry_run = FALSE, verbose = FALSE, comments = FALSE )
image_id |
A character string. The local image ID or name to push,
as shown in |
namespace |
A character string. The account or organization the
image is scoped under, used to construct the full registry path. For
the default |
project |
A character string. The image's name – the third segment
of the full registry path, e.g. |
tag |
A character string. The version tag to assign to the image.
Defaults to |
registry |
A character string. The registry hostname. Defaults to
|
tool_preference |
A non-empty character vector of container tools to
try, in order. Defaults to |
check_login |
Logical. If |
dry_run |
Logical. If |
verbose |
Logical. If |
comments |
Logical. If |
The full registry URI the image was tagged and pushed as (or
would be, under dry_run = TRUE) – "{registry}/{namespace}/{project}:{tag}",
e.g. "registry.doit.wisc.edu/erwin.lares/my-analysis:1.0.0" – as a
character string, returned invisibly. Capture it to hand off to
submitr::htc_gen_submit(container_image = ) without reassembling
or retyping the URI by hand (C-G1).
Before calling push_image() against the default
"registry.doit.wisc.edu" registry, ensure the following are in place.
(For other registries – ghcr.io, quay.io, or any other hostname
supplied via registry – consult that registry's own documentation for
the equivalent setup; the steps below are DoIT GitLab-specific.)
The image has been built locally with build_image(). Run
podman image ls to find the image ID.
You have a GitLab account at git.doit.wisc.edu and a project with
the container registry enabled.
You have a Personal Access Token (PAT) with read_registry and
write_registry scopes. Create one at:
https://git.doit.wisc.edu/-/user_settings/personal_access_tokens
You are logged in to the registry. Authenticate once in a terminal:
podman login registry.doit.wisc.edu
Enter your NetID as the username and your PAT as the password.
The DoIT GitLab Container Registry requires authentication before
pushing. Use a Personal Access Token (PAT) rather than your NetID
password – PATs can be scoped to registry access only and revoked
independently. Authentication is cached by podman or docker after
the first login, so you only need to run podman login once per
machine per session. For other registries, use whatever credential type
that registry expects – e.g. a GitHub PAT with the write:packages
scope for ghcr.io.
Note that GitLab Self-Managed authentication tokens expire after five
minutes by default. If you see an unauthorized: authentication required
error mid-push on a large image, re-authenticate and push again.
## Not run: # Tag and push an image to the DoIT GitLab Container Registry -- # project is the image's own name, not a project that merely hosts # the registry (C18) push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis" ) # Push with an explicit version tag push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", tag = "1.0.0" ) # Preview the commands without running them push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", dry_run = TRUE ) # Guided push for first-time users push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", verbose = TRUE, comments = TRUE ) # Push to GitHub Container Registry instead of the default push_image( image_id = "974123909a36", namespace = "your-github-username", project = "my-analysis", registry = "ghcr.io" ) # Capture the full registry URI to hand off to submitr -- e.g. # submitr::htc_gen_submit(container_image = uri) -- instead of # reassembling it by hand from registry/namespace/project/tag (C-G1) uri <- push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", tag = "1.0.0" ) uri #> [1] "registry.doit.wisc.edu/erwin.lares/my-analysis:1.0.0" ## End(Not run)## Not run: # Tag and push an image to the DoIT GitLab Container Registry -- # project is the image's own name, not a project that merely hosts # the registry (C18) push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis" ) # Push with an explicit version tag push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", tag = "1.0.0" ) # Preview the commands without running them push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", dry_run = TRUE ) # Guided push for first-time users push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", verbose = TRUE, comments = TRUE ) # Push to GitHub Container Registry instead of the default push_image( image_id = "974123909a36", namespace = "your-github-username", project = "my-analysis", registry = "ghcr.io" ) # Capture the full registry URI to hand off to submitr -- e.g. # submitr::htc_gen_submit(container_image = uri) -- instead of # reassembling it by hand from registry/namespace/project/tag (C-G1) uri <- push_image( image_id = "974123909a36", namespace = "erwin.lares", project = "my-analysis", tag = "1.0.0" ) uri #> [1] "registry.doit.wisc.edu/erwin.lares/my-analysis:1.0.0" ## End(Not run)