containr 0.2.0.9000
Breaking changes
r_mode = "tidystudio"has been removed. Renamed to"verse", matching the Rocker project’s own name for the underlying image (rocker/verse) –"studio"was misleading, since RStudio Server is already present via"tidyverse"two modes earlier, and nothing in the old name hinted at the TeX Live installation that’s the actual differentiator. No deprecation alias; regenerate any Dockerfile built withr_mode = "tidystudio"usingr_mode = "verse"instead.The
toolargument (a single tool name orNULLfor auto-detect) onbuild_image(),push_image(), andlist_images()is replaced bytool_preference, a non-empty character vector tried in order. Defaults toc("podman", "docker"), matching today’s default behavior. A length-1 vector (e.g.tool_preference = "docker") behaves like the oldtool = "docker"; a longer vector lets you set a custom auto-detect order, e.g.tool_preference = c("docker", "podman").tool = NULLhad no direct equivalent kept – passingNULLtotool_preferencenow errors, since the empty/auto-detect case is expressed by supplying more than one candidate instead.push_image()‘snetidargument is renamed tonamespace.netidonly ever made sense for the default"registry.doit.wisc.edu"registry – the same argument, and the same position in the assembled{registry}/{namespace}/{project}:{tag}path, is a GitHub username or organization forghcr.io, or a Quay namespace forquay.io.namespaceis the term those registries’ own documentation uses for this segment, not a namecontainrinvented. No alias kept; updatenetid = ...calls tonamespace = ....
New features
generate_dockerfile()gains aconfigargument: the path to a_toolero.ymlproject manifest, such as the onetoolero::init_project()writes. When supplied, it fills indata_file,code_file, andmisc_filefrom the manifest’s declaredfolders:– but only an argument the call left at its ownNULLdefault. An explicitdata_file/code_file/misc_filealways wins, so passingconfignever changes the behavior of a call that already states its own file arguments.code_fileis derived from the folder named by the manifest’sscript_dirconvention ("R"when the manifest does not say otherwise);misc_fileis derived from"assets"when present, the branding folderinit_project(branding = ...)creates;data_fileis derived from"data-raw"when present. Inverbosemode,generate_dockerfile()reports which of the three arguments came fromconfigrather than from the call. Aschema_versionthe file does not declare is treated as schema1; any other declared value warns rather than aborts, and the file is still read on a best-effort basis either way. Reading_toolero.ymlis not a dependency ontoolero: the schema is the contract, and a manifest written by hand is as valid an input as oneinit_project()created (#C01).When
configis supplied,generate_dockerfile()now also adds aRUN mkdir -pinstruction, right afterWORKDIR, creating every folder the manifest’sfolders:declares underhome_dir. Previously the generated image had nooutput/directory (or any other project folder) at all: a script callingtoolero::save_output()was safe, because that function creates parents recursively, but a bareggsave("output/figures/x.png")failed inside the container exactly as it would on an execute node with nooutput/yet created. A call that never passesconfigsees no new instruction (#C07).Two new
r_modevalues ongenerate_dockerfile():"shiny_server"(rocker/shiny) for serving Shiny apps, and"rstudio_shiny"(rocker/rstudiowith Shiny Server layered on top via Rocker’s owninstall_shiny_server.sh) for RStudio Server and Shiny Server in the same image. Both require R >= 4.0.0 and error informatively otherwise –/rocker_scripts/(which the Shiny Server install depends on) only exists in images built from therocker-versioned2project, which covers R >= 4.0.0; older tags on the same Docker Hub repositories predate that entirely.EXPOSEnow supports more than one port on a single line forrstudio_shiny, which exposes both8787and3838.data_file,code_file, andmisc_fileare copied to/srv/shiny-server/for these two modes, matching Shiny Server’s own default app directory – the four existing modes are unaffected, still copying tohome_diras before (see thecopy_root/home_dirfix below, #C24, for a related correction to exactly how that tracking works).expose_portremains an override for"rstudio"only; the two new modes expose fixed port(s) and ignore it, with a warning if supplied.push_image()now works against any OCI-compliant registry, not just the defaultregistry.doit.wisc.edu– tested againstghcr.ioandquay.io. Login-check guidance (both the pre-pushcommentsmessage and the not-logged-in error) is registry-aware: the default registry keeps its existing specific PAT-creation instructions, while any other registry gets generic guidance pointing at that registry’s own documentation instead of DoIT-specific instructions that would be wrong for it.containrnow ships a ready-to-use GitHub Actions template atinst/templates/build-and-push.yaml, for building and pushing your own project’s image from CI rather than locally. Callsbuild_image()/push_image()directly (the same functions you’d run yourself, not a hand-written shell equivalent) on a GitHub-hosted runner, which is nativelyx86_64– a direct fix, not a workaround, for QEMU emulation issues buildinglinux/amd64images locally on Apple Silicon. SeeREADME.md’s “Building in CI” section.data_file,code_file, andmisc_fileongenerate_dockerfile()now accept a character vector of paths, not just a single path, and a path may point to a directory, copied whole rather than one file at a time.code_file = c("R/prepare.R", "R/model.R")andmisc_file = "assets/"both work in a single call now; previously each argument accepted exactly one file path and rejected directories outright. Fully backward-compatible – a length-1 character path behaves exactly as before. TheCOPY-instruction generation already built its output viapurrr::map_chr()over these arguments, so this change is contained to.validate_file_arg().New
quarto_versionargument ongenerate_dockerfile(), defaulting to"latest"for full backward compatibility. Quarto installation (install_quarto = TRUE) previously always pulled fromquarto.org/download/latest/, the one unpinned layer in an otherwise deliberately pinned image (r_version,renv.lock)."latest"is now resolved to a concrete version at generation time via the Quarto releases API and installed from a versioned GitHub release URL instead; an explicit version (e.g."1.5.57") is validated against that same API and errors if no matching release exists. Either way, the resolved version is recorded in the generatedDockerfileasENV QUARTO_VERSION=..., recoverable from a running container without the originalDockerfileon hand. New internal helper.get_quarto_version()inR/get-quarto-version.R.New
os_versionargument ongenerate_dockerfile(), defaulting toNULL. WhenNULL, the Ubuntu version queried for system requirements (auto_syslibs) is now derived from the resolvedr_versionvia the Rocker Project’s own R-version-to-Ubuntu-release mapping (20.04 for R 4.0.0-4.1.3, 22.04 for R 4.2.2-4.3.3, 24.04 for R 4.4.2 and later, confirmed directly against rocker-project.org and the rocker-versioned2 wiki) rather than a single hardcoded default. Supplyingos_versionoverrides the derivation entirely, for a project that needs to query against a different Ubuntu release than the one itsr_versionwould normally resolve to. New internal helper.resolve_os_version()inR/r-mode-registry.R(#C14).
Bug fixes
The generated
Dockerfilenow restores therenvproject library immediately after copyingrenv.lock, before any of thedata_file,code_file, ormisc_filecontent is copied in. Previously the threeCOPYblocks for project content sat above the restore step, so editing a single line of an analysis script invalidated Docker/Podman’s build cache for thatCOPYlayer and, because the restore came after it, for the single most expensive layer in the image too – every package reinstalled from source on every rebuild, contrary to what the README says about later builds being faster. The restore only depends onrenv.lock, already in place earlier in the file, so nothing about moving it changes what gets installed.generate_dockerfile()now copiesrenv.lockto{home_dir}/renv.lockinstead of a hardcoded/home/renv.lock. The image’s install script runsrenv::restore()with no explicit project, which resolves the project from the working directory – i.e. whereverWORKDIR(home_dir) points. The hardcoded destination only worked by coincidence whenhome_dirwas left at its own default of"/home"; passinghome_dir = "/workspace"left the lockfile somewhererenv::restore()never looked.generate_dockerfile()’scopy_root(the destination fordata_file,code_file, andmisc_file) was hardcoded to the literal"/home"for"base","tidyverse","rstudio", and"verse", independent ofhome_dir– the same class of bug as therenv.lockfix above, in the same corner of the file. That agreed withhome_dir’s own default and silently disagreed the momenthome_dirwas set to anything else:COPYdestinations stayed at/homewhileWORKDIR, and the script that actually ran, moved to whereverhome_dirpointed.copy_rootis nowNULLin the mode registry for those four modes, meaning “fall back tohome_dir”, so the two now always agree."shiny_server"and"rstudio_shiny"are unaffected – their copy destination is still the fixed/srv/shiny-serverregardless ofhome_dir(#C24).generate_dockerfile(install_quarto = TRUE)now fetches and installs Quarto withcurl -LOanddpkg -i(falling back toapt-get install -fto resolve dependencies) instead ofwgetandgdebi. Neitherwgetnorgdebiis present inrocker/r-ver, soinstall_quarto = TRUEpreviously failed at build time on everyr_modeunless something in the lockfile happened to pull those two programs in as a side effect.curlis already installed unconditionally as a baseline system library, so the new approach adds no packages to the image.generate_dockerfile()now validates the requested R version against the tag repository the resolvedr_modewill actually buildFROM, instead of always checking it againstrocker/r-ver. A version that exists inrocker/r-verbut not in, say,rocker/versepreviously passed validation and only failed later, at theFROMinstruction itself; the “version does not exist” error now also points at the right repository’s page instead of always linking torocker/r-ver’s.expose_port’s “only used whenr_modeisrstudio” warning is now based on whether the argument was supplied at all (missing(expose_port)), not on whether its value differs from the default. Previously, explicitly passingexpose_port = "8787"under a non-rstudior_modeproduced no warning even though the value is still ignored there.generate_dockerfile()now createsoutput, including any missing parent directories, if it does not already exist. Previously, a nonexistentoutputdirectory surfaced as a raw file-connection error fromreadr::write_lines()rather than an informative message.generate_dockerfile()’soutputargument now defaults to".", the current working directory, instead oftempdir(). The old default meantgenerate_dockerfile()andbuild_image()– whosedockerfileargument is always resolved againstgetwd()– pointed at two different places by default, so calling both with no arguments, the most natural thing a new user does, failed on the second call with a file it could not find. The two defaults now compose without either argument having to be supplied.Fixed a bug where
push_image()’s pre-push login check always failed under Docker regardless of whether the user was actually logged in.<tool> login --get-login <registry>is a Podman-only flag; running it under Docker always exits with a usage error (125), which is whycheck_login = FALSEwas previously necessary as a Docker workaround. Podman keeps its native--get-logincheck; Docker (and, permissively, any othertool_preferencevalue) now checks~/.docker/config.jsondirectly for a cached credential, the same approach most CI tooling uses since Docker itself has no query subcommand for this. This is a best-effort local check either way – it confirms a credential exists, not that it’s still valid; an expired token can still fail at push time.list_images()calledprint(parsed)unconditionally before returninginvisible(parsed), soimgs <- list_images()printed a data frame as a side effect of assignment. Printing is now gated behindverbose, matching every other user-facing message in the package:imgs <- list_images()assigns quietly, andlist_images(verbose = TRUE)prints (#C17).generate_dockerfile()now warns whenrenv.lockrecords no packages at all, regardless ofauto_syslibs. Previously this was silent:.fetch_sysreqs()short-circuits on an empty package vector, the image builds with only the baselinecurlinstalled, and the build succeeds while the analysis inside it cannot run (#C04).generate_dockerfile(comments = TRUE)now writes each instruction’s explanatory comment on the line above it, rather than below. This matches the orderingsubmitr’s ownhtc_gen_submit()andhtc_gen_executable()have always used, so a generated Dockerfile and a submitr-generated.sh/.subfile now read the same way round – explanation first, instruction second. Previously the two packages disagreed silently:submitr’s README already promised this ordering beforecontainractually matched it (#C15, #S09).
Documentation
push_image()’sprojectargument was documented as “the GitLab project name that hosts the container registry,” with its own examples usingproject = "container-registry"as if that’s a single shared project set up to host the registry generally. The README, and every one of its own examples, useproject = "my-analysis"and produceregistry.doit.wisc.edu/erwin.lares/my-analysis:1.0.0– the project is the image. Fixed the roxygen, the missing-argument error message, and every example inpush-image.R, plus thepush_image()example embedded inlist_images()’s own docs, to say so and useproject = "my-analysis"throughout (#C18).commentsmeans something different depending on which function you’re looking at:generate_dockerfile()’scommentsargument writes annotations into the generatedDockerfile, whilebuild_image()’s andpush_image()’scommentsarguments print explanatory guidance to the console instead, matching howcommentsis used acrosssubmitr’s own functions. A roxygen note on all three arguments now states this directly rather than renaming anything (#C15).“Before you start” in the README now says the lockfile has to record the analysis’s own packages, and
tooleroitself if the containerized script callstoolero::save_output()ortoolero::resolve_input_path()– those functions maketooleroa runtime dependency of the analysis, not just a development convenience (#C04).The README’s
generate_dockerfile()section now notes thatembed-resources: trueintoolero’s Quarto templates reduces, but does not eliminate, the need to remembermisc_file = "assets/"– a self-contained.htmlstill requiresassets/styles.css,assets/header.html, andassets/footer.htmlto be present at render time (#C06). The same section’s description of wheredata_file,code_file, andmisc_fileland was also updated to sayhome_dirrather than a hardcoded/home/, reflecting thecopy_rootfix above (#C24).
Internal changes
The three previously-independent, hand-maintained mappings of
r_modeto image name (generate_dockerfile()), Docker Hub repo (.get_r_ver_tags()), and valid-values list (.r_ver_exists()) are now a single shared registry (.r_mode_registry). No user-facing effect beyond thetidystudioremoval and the two new modes above.tool_preferenceis not validated against a fixed list of tool names – any string on the system’s PATH that responds to<tool> infois accepted. This is intentional:tool_preferenceshould not need a companion validation update every time a new container tool gains support (Apptainer support is planned for a future release). Structural validation still applies –tool_preferencemust be a non-empty character vector with no missing values. Error messages for an unrecognized tool that’s installed but not responding fall back to generic guidance rather than Docker- or Podman-specific instructions that would be wrong for a different tool;dockerandpodmankeep their existing specific guidance.Removed a redundant internal check:
build_image(),push_image(), andlist_images()each called.check_tool_responsive()immediately after.resolve_tool(), which already guarantees the resolved tool is responsive. No user-facing behavior change.Added integration tests (
CONTAINR_INTEGRATION_TESTS=true) forbuild_image()andpush_image(), backfilling the two that were previously only covered at the argument-validation and command- construction layers.push_image()’s integration test additionally requiresCONTAINR_TEST_NAMESPACEandCONTAINR_TEST_PROJECTto be set, so it never pushes a test image to an unintended destination. These now run automatically in CI on every push/PR touching the relevant files, via a newcontainer-integration-tests.yamlGitHub Actions workflow.
Testing
Added
tests/testthat/test-readme-workflow.R, which extracts the actualgenerate_dockerfile()call from the README’s “A first workflow” section, runs it exactly as printed, and confirms the resultingDockerfilepins the requested R version and copies in the referenced files. The other three steps in that workflow (build_image(),list_images(),push_image()) need a live container engine and registry and can’t run in CI, butgenerate_dockerfile()needs nothing but a lockfile in a temporary directory, and it’s the step everything else depends on. This test reads the README rather than holding a copy of its example, so the two can’t quietly drift apart, and skips (rather than fails) whenREADME.mdisn’t on disk, such as from a built tarball (#C20).Added a test that generates a Dockerfile for every
r_modecrossed with both"/home"and"/workspace"home_dirvalues, parses the actualWORKDIRandCOPYlines out of the result, and asserts thatrenv.locklands underWORKDIRand that project files land under the mode’scopy_root. Previously each of those facts was tested in isolation, which is exactly how #C10 stayed invisible: every individual assertion was true at once. Updated for thecopy_rootfix (#C24) to resolve aNULLregistry value againsthome_dirrather than asserting the old hardcoded behavior.Added a test asserting the position of instructions relative to each other (
syslibsbeforequarto,renv_lockbefore the restore step, the restore step before the firstCOPYof project content) rather than comparing against a fixed expectedDockerfile, so it survives future additions to the instruction list without needing to be rewritten (#C21).Added tests for the
copy_root/home_dirfix (#C24):COPYdestinations track a customhome_dirfor the four Phase 1 modes, and still default to/homewhenhome_diris left at its own default.Added tests for the
config-derivedmkdir -pblock (#C07): every declared folder appears in oneRUN mkdir -pline positioned afterWORKDIR, the destinations track a customhome_dir, the line is absent whenconfigis not supplied or declares no folders, and a foldercontainrhas no other special meaning for (neitherdata-raw,script_dir, norassets) is still included.Added
.resolve_os_version()unit tests covering each Ubuntu-version threshold and its boundary, plus"latest"/"devel". Added tests assertinggenerate_dockerfile()derivesos_versionand passes it to.fetch_sysreqs(), that an explicitos_versionoverrides the derivation, and that neither derivation nor the sysreqs call happens whenauto_syslibs = FALSE(#C14).Added tests asserting
list_images()does not print whenverbose = FALSE(the default), does print whenverbose = TRUE, and returns the correct data frame either way (#C17).Added tests asserting the empty-lockfile warning fires (and mentions
toolero), fires even whenauto_syslibs = FALSE, and does not fire whenrenv.lockrecords at least one package (#C04). Every existing test’srenv.lockfixture now records a dummy package (previously an empty"Packages":{}), so this new warning does not fire incidentally across the rest of the suite.
containr 0.1.3.9000
New functions
build_image()builds a container image from aDockerfileusing eitherpodmanordocker. Auto-detects which tool is available, preferringpodman. Newplatformargument defaults to"linux/amd64"for HPC/HTC cluster compatibility. When the target platform differs from the host architecture (e.g. buildinglinux/amd64on Apple Silicon), the function automatically usesdocker buildx buildwith--loadfor Docker, or passes--platformdirectly for Podman. A warning is emitted for cross-platform builds to alert the user about potential QEMU emulation issues. Supportsdry_run = TRUEto preview the build command without executing it.verboseandcommentsfollow the same contract asgenerate_dockerfile().push_image()tags a locally built container image with a full registry path and pushes it to a container registry in a single call, handling both thepodman tagandpodman pushsteps internally. Arguments:image_id,netid,project,tag(defaults to"latest"),registry(defaults to"registry.doit.wisc.edu"). Supports login verification,dry_run = TRUE, and guided output viaverboseandcomments.list_images()returns a data frame of container images in the local image store, as reported bypodman image lsordocker image ls. Useful for finding the image ID to pass topush_image()after building an image withbuild_image(). Prints the data frame to the console and returns it invisibly.Two new internal helpers shared by
build_image(),push_image(), andlist_images():.resolve_tool()auto-detectspodmanordockeron the PATH, and.check_tool_responsive()verifies the daemon is running before attempting any build, push, or list operation.
Changes to generate_dockerfile()
Breaking change:
COPYinstructions generated bydata_file,code_file, andmisc_filenow preserve the local directory structure inside the container under/home/. Previously, all files were flattened into/home/data/or/home/regardless of their source path. For example,data_file = "data-raw/sample.csv"now producesCOPY data-raw/sample.csv /home/data-raw/sample.csvinstead ofCOPY data-raw/sample.csv /home/data/sample.csv. This means R scripts inside the container can use the same relative paths they use locally.Breaking change:
COPYsource paths are now always written as relative to the build context (the current working directory). Previously, absolute paths could leak into the Dockerfile if.validate_file_arg()normalized them, causingpodman buildto fail with “no such file or directory.”generate_dockerfile()refactored:r_modevalidated before file and network operations,dplyrdependency removed,expose_portnow warns whenr_modeis not"rstudio", build loop simplified,invisible(NULL)added to return value.generate_dockerfile()now requires anrenv.lockfile in the current working directory. Errors informatively if none is found, with instructions to runrenv::snapshot().New
auto_syslibsargument (defaultTRUE) readsrenv.lock, queries the Posit Package Manager sysreqs database viaremotes::system_requirements(), and automatically includes the system libraries required by all packages in the lock file. Warns and continues without auto-detection if the lookup fails.New
install_syslibsargument (defaultNULL) accepts a character vector of additionalaptpackage names to install on top of the auto-detected set, e.g.install_syslibs = c("libuv1-dev", "libwebp-dev").curlis now always installed as a baseline system package regardless ofauto_syslibsorinstall_syslibs. It is required byrenvfor package downloads inside the container.Breaking change: the hardcoded system library list (
cmake,libcurl4-openssl-dev,libssl-dev, etc.) has been removed. Libraries are now determined entirely byauto_syslibsandinstall_syslibs. The oldinstall_syslibs = TRUEargument no longer works – pass a character vector of library names instead.generate_dockerfile()now callsrenv::status()defensively and warns if the lock file appears to be out of sync with the project library.A success message now reports the path where the
Dockerfilewas written whenverbose = TRUE.
Tests and documentation
- Added
tests/testthat/test-generate-dockerfile-content.Rcovering Dockerfile output content for all arguments. - Updated tests to expect directory-preserving
COPYdestinations instead of the old flattened/home/data/pattern. - Added lifecycle and Codecov badges.
- Updated hex sticker and favicon.
- Added tests for
build_image()platformparameter: invalid platform validation,--platformflag inclusion/omission,docker buildxvsdocker buildselection,--loadflag for cross-arch Docker builds, cross-compilation warning, and same-architecture no-warning behavior. -
.validate_file_arg()now returns paths relative to the working directory instead of absolute paths. Files outside the build context (including files on a different drive on Windows) produce an informative error. This fixes cross-drive build failures on Windows CI wherefs::path_rel()could not compute a relative path
Dependency changes
-
httr2added toImports. Used by.get_r_ver_tags()for Docker Hub API calls. -
httrremoved fromImports. All HTTP calls now usehttr2. -
remotesadded toImports. Used by.fetch_sysreqs()to query system library requirements. -
jsonliteadded toImports. Used by.read_renv_packages()to parserenv.lock. -
dplyrremoved fromImports. Ther_modelookup ingenerate_dockerfile()now uses a named vector instead ofdplyr::case_when().
Internal changes
-
.get_r_ver_tags()migrated fromhttrtohttr2. - New internal helpers
.read_renv_packages()and.fetch_sysreqs()added inR/sysreqs-helpers.R. - New internal helpers
.resolve_tool()and.check_tool_responsive()added inR/container-helpers.R.
containr 0.1.3
CRAN release: 2026-04-26
Changes
- Internal helpers renamed with a dot prefix:
get_r_ver_tags()->.get_r_ver_tags(),r_ver_exists()->.r_ver_exists(), andvalidate_file_arg()->.validate_file_arg(). These are not user-facing but the change enforces the package convention for internal functions. -
tidystudioadded as a validr_modeingenerate_dockerfile(),.get_r_ver_tags(), and.r_ver_exists(). Maps torocker/verse.
Bug fixes
-
generate_dockerfile(): replacedstop()andprint()/Sys.sleep()calls withcli::cli_abort()andcli::cli_inform()throughout for consistent, styled error and progress messages. -
generate_dockerfile(): fixedcommentscondition for Quarto block fromquarto_install_line == TRUEtoinstall_quarto. -
generate_dockerfile(): fixedcommentsconditions forcode_fileandmisc_fileblocks to check the correct variables. -
generate_dockerfile(): RStudio run instructions split into two cleaner comment lines. -
.get_r_ver_tags(): replacedstopifnot()withcli::cli_abort()andmessage()withcli::cli_inform(). Removed barereturn()from final list expression. -
.r_ver_exists(): replacedstop()withcli::cli_abort()andmessage()withcli::cli_inform(). -
.validate_file_arg(): replacedstop()withcli::cli_abort().
containr 0.1.2
CRAN release: 2026-04-07
- Added
inst/CITATIONwith DOI for proper academic citation viacitation("containr") - Added
inst/WORDLISTfor spell check consistency - Added
Language: en-UStoDESCRIPTION - Improved documentation and README
- Added rhub v2 GitHub Actions workflow for cross-platform checks
