10 min read

'dockr': easy containerization for R

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 <lars_kjeldgaard@hotmail.com>", 
                    ""),
                    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 <lars_kjeldgaard@hotmail.com>
#> 
#> # 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