‘dockr’: easy containerization for R

[This article was first published on Posts on pRopaganda by smaakagen, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

dockr 0.8.6 is now available on CRAN. dockr is a minimal toolkit to build a
lightweight Docker container image for your R package, in which the package
itself is available. The Docker image seeks to mirror your R session as close as
possible with respect to R specific dependencies. Both dependencies on CRAN
R packages as well as local non-CRAN R packages will be included in the Docker
container image.

If you want to know, how Docker works, and why you should consider using Docker,
please take a look at the Docker website.

Installation

Install the development version of dockr with:

remotes::install_github("smaakage85/dockr")

Or install the version released on CRAN:

install.packages("dockr")

Workflow

When you work on an R project, it is often desirable to organize the code in the
R package structure. dockr facilitates easy creation of a Docker container
image that mirrors your current R session and includes all of the R dependencies
needed to run your R package.

First, load the dockr package.

library(dockr)

In order do create the files, that constitute the Docker image, simply invoke
the prepare_docker_image() function and point to the folder with your package.

The workflow of prepare_docker_image() is summarized below:

  1. Build and install the package on your system
  2. Identify R package dependencies of the package
  3. Detect the version numbers of the loaded and installed versions of these
    packages on your system
  4. Write Dockerfile and create all other files needed to build the Docker image

Now, I will let dockr do its magic and create the files for a Docker image
container, in which dockr is installed together with all of the R package
dependencies, dockr needs to run.

Beware that the files are created as side-effects of the function call. Since
my ‘dockr’ package lives in a folder called ‘docker’ misleadingly, I call the
function like this:

image_dockr <- prepare_docker_image("~/docker", 
                                    dir_image = "~",
                                    dir_install = "auto")
#> v Deleting existing folder for files for Docker image: ~/dockr_0.8.6
#> v Creating folder for files for Docker image: ~/dockr_0.8.6
#> v Creating folder for source packages: ~/dockr_0.8.6/source_packages
#> v Creating empty Dockerfile: ~/dockr_0.8.6/Dockerfile
#> --- Building, installing and loading package...
#> Writing NAMESPACE
#> Writing NAMESPACE
#> --- Writing Dockerfile...
#> v Preparing FROM statement
#> v Identifying and mirroring R package dependencies
#> v Matching dependencies with CRAN packages
#> v Preparing install statements for specific versions of CRAN packages
#> v Preparing install statement for the package itself
#> v Writing lines to Dockerfile
#> v Closing connection to Dockerfile
#> - in R : 
#> => to inspect Dockerfile run:
#> dockr::print_file("~/dockr_0.8.6/Dockerfile") 
#> => to edit Dockerfile run:
#> dockr::write_lines_to_file([lines], "~/dockr_0.8.6/Dockerfile") 
#> - in Shell : 
#> => to build Docker image run:
#> cd C:\Users\Lars\Documents\dockr_0.8.6 
#> docker build -t dockr_0.8.6 . 
#> Please note that Docker must be installed in order for you to build image.

Note, argument ‘dir_image’ decides, where the files for the docker image will
be saved. ‘dir_install’ is the directory, where your package will be installed
on your system. You can choose to install the package in a temporary folder
by setting dir_install = tempdir().

Great, all necessary files for the Docker image have been created, and you
can build the Docker image right away by following the instructions. It is as
easy as that! Yeah!

Files for Docker image

Let us just take a quick look into the folder with the files for the Docker
image to see the works of dockr.

list.files(image_dockr$paths$dir_image)
#> [1] "Dockerfile"      "source_packages"

It contains a Dockerfile and a folder named ‘source_packages’.

Dockerfile

The resulting Dockerfile can be printed with the print_file() function, that
comes with dockr:

print_file(image_dockr$paths$path_Dockerfile)
#> # load rocker base-R image
#> FROM rocker/r-ver:3.6.0
#> 
#> # install specific versions of CRAN packages from MRAN snapshots
#> RUN R -e 'install.packages("remotes")'
#> RUN R -e 'remotes::install_version("askpass", "1.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("assertthat", "0.2.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("brew", "1.0-6", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("clisymbols", "1.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("commonmark", "1.7", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("crayon", "1.3.4", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("desc", "1.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("fs", "1.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("gh", "1.0.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("glue", "1.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("gtools", "3.8.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("ini", "0.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("jsonlite", "1.6", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("magrittr", "1.5", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("memoise", "1.1.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("pkgload", "1.0.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("prettyunits", "1.0.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("ps", "1.3.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rcmdcheck", "1.3.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rprojroot", "1.3-2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rstudioapi", "0.10", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("sessioninfo", "1.1.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("stringi", "1.4.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("stringr", "1.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("withr", "2.1.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("xopen", "1.0.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("yaml", "2.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("backports", "1.1.4", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("callr", "3.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("cli", "1.1.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("clipr", "0.6.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("curl", "3.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("devtools", "2.0.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("digest", "0.6.20", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("git2r", "0.25.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("httr", "1.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("mime", "0.6", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("openssl", "1.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("pkgbuild", "1.0.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("processx", "3.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("purrr", "0.3.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("R6", "2.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("Rcpp", "1.0.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("remotes", "2.0.4", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rlang", "0.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("roxygen2", "6.1.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("sys", "3.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("usethis", "1.5.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("whisker", "0.3-2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("xml2", "1.2.0", dependencies = FALSE)'
#> 
#> # copy source packages (*.tar.gz) to container
#> COPY source_packages /source_packages
#> 
#> # install 'dockr' package
#> RUN R -e 'install.packages(pkgs = "source_packages/dockr_0.8.6.tar.gz", repos = NULL)'
#>

As you see, the versions of the R packages, that will be installed in the Docker
container image, are all given explicitly. They will mirror the versions of the
dependencies, that are in fact loaded or installed on your system. In this way,
the Docker container image seeks to reflect your current R session as close as
possible and by doing so create an environment, where you will be able to
reproduce results from your current R session.

Also note, that CRAN R packages will be installed from relevant
MRAN snapshots – using the
remotes::install_version() function.

Folder with Source Packages

The ‘source_packages’ folder contains the local (non-CRAN) packages, that have
to be installed in the Docker container image in order for dockr to run.

Since dockr does not depend on any local (non-CRAN) packages,
source_packages only contains a source package version of dockr itself,
i.e.:

list.files(image_dockr$paths$dir_source_packages)
#> [1] "dockr_0.8.6.tar.gz"

How to edit Dockerfile further

If there is need for adding additional lines to/editing the Dockerfile (e.g.
if you have to install any non-R dependencies, this can
be achieved with the write_lines_to_file() function. write_lines_to_file()
enables you to add new lines to the beginning or the end of the Dockerfile.

Let us try it out and write a couple of additional lines to the Dockerfile.

# write three lines to beginning of file.
write_lines_to_file(c("# set maintainer",
                    "MAINTAINER Lars KJELDGAARD <[email protected]>", 
                    ""),
                    image_dockr$paths$path_Dockerfile,
                    prepend = TRUE,
                    print_file = FALSE)

# write lines to the end of the file.
write_lines_to_file(c("# check out smaakage85.netlify.com >:-]~~"),
                    image_dockr$paths$path_Dockerfile,
                    prepend = FALSE,
                    print_file = FALSE)

Take a look at the resulting Dockerfile.

print_file(image_dockr$paths$path_Dockerfile)
#> # set maintainer
#> MAINTAINER Lars KJELDGAARD <[email protected]>
#> 
#> # load rocker base-R image
#> FROM rocker/r-ver:3.6.0
#> 
#> # install specific versions of CRAN packages from MRAN snapshots
#> RUN R -e 'install.packages("remotes")'
#> RUN R -e 'remotes::install_version("askpass", "1.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("assertthat", "0.2.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("brew", "1.0-6", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("clisymbols", "1.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("commonmark", "1.7", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("crayon", "1.3.4", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("desc", "1.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("fs", "1.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("gh", "1.0.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("glue", "1.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("gtools", "3.8.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("ini", "0.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("jsonlite", "1.6", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("magrittr", "1.5", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("memoise", "1.1.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("pkgload", "1.0.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("prettyunits", "1.0.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("ps", "1.3.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rcmdcheck", "1.3.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rprojroot", "1.3-2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rstudioapi", "0.10", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("sessioninfo", "1.1.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("stringi", "1.4.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("stringr", "1.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("withr", "2.1.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("xopen", "1.0.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("yaml", "2.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("backports", "1.1.4", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("callr", "3.2.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("cli", "1.1.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("clipr", "0.6.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("curl", "3.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("devtools", "2.0.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("digest", "0.6.20", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("git2r", "0.25.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("httr", "1.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("mime", "0.6", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("openssl", "1.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("pkgbuild", "1.0.3", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("processx", "3.3.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("purrr", "0.3.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("R6", "2.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("Rcpp", "1.0.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("remotes", "2.0.4", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("rlang", "0.4.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("roxygen2", "6.1.1", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("sys", "3.2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("usethis", "1.5.0", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("whisker", "0.3-2", dependencies = FALSE)'
#> RUN R -e 'remotes::install_version("xml2", "1.2.0", dependencies = FALSE)'
#> 
#> # copy source packages (*.tar.gz) to container
#> COPY source_packages /source_packages
#> 
#> # install 'dockr' package
#> RUN R -e 'install.packages(pkgs = "source_packages/dockr_0.8.6.tar.gz", repos = NULL)'
#> 
#> # check out smaakage85.netlify.com >:-]~~

Dealing with local non-CRAN R package dependencies

If your package depends on local non-CRAN R packages, dockr will also include
these packages in the Docker container image. Local non-CRAN R packages must be
available as source packages ([packageName]_[packageVersion].tar.gz) in one
or more user specified local directories. These paths have to be specified in the
‘dir_src’ argument, when invoking the prepare_docker_image(), e.g.:

# image for my package 'recorder'.
image_recorder <- prepare_docker_image("~/recorder",
                                       dir_image = "~",
                                       dir_install = "auto",
                                       dir_src = c("~/src"))

Note, that you can store multiple versions of the same package in your local
repos. In this way ‘dockr’ comes with a lot of flexibility.

What about non-R dependencies?

dockr does not deal with any non-R dependencies what so ever at this point.
In case that, for instance, your package has any Linux specific dependencies,
you will have to install them yourself in the Docker container image.

Contact

I hope, that you will find dockr useful.

Please direct any questions and feedbacks to me!

If you want to contribute, open a PR.

If you encounter a bug or want to suggest an enhancement, please open an issue.

Best,
smaakagen

To leave a comment for the author, please follow the link and comment on their blog: Posts on pRopaganda by smaakagen.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)