Econometrics and Free Software by Bruno Rodrigues.
RSS feed for blog post updates.
Follow me on Mastodon, twitter, or check out my Github.
Check out my package that adds logging to R functions, {chronicler}.
Or read my free ebooks, to learn some R and build reproducible analytical pipelines..
You can also watch my youtube channel or find the slides to the talks I've given here.
Buy me a coffee, my kids don't let me sleep.

Why you should consider working on a dockerized development environment

R

Last year I wrote a post about dockerizing {targets}’s pipelines (link to post) and between that blog post and this one, I’ve written a whole book on building reproducible analytical pipelines that you can read here (for free!). In the book I explain how you can build projects that will stay reproducible thanks to Docker. With Docker, you don’t only ship the code to your project, but ship a whole computer with it, and your project will be executed inside that computer. By whole computer I mean the whole computational environment: so a version of R, the required packages your project depends on, all of it running on a Linux distribution (usually Ubuntu). The whole project can then be executed like any program from your computer (whether you’re running Windows, macOS or Linux) or even on the cloud with a single command.

In this blog post, I’ll discuss something that I’ve been trying for some time now: working directly from a dockerized environment. The idea is to have a Docker image that comes with R, all the usual packages I use for development, Quarto, a LaTeX distribution (that I installed with {tinytex}) and finally, my IDE of choice, Spacemacs (if you use RStudio, just read on, I’ll explain how you can achieve the same thing but with RStudio instead). Why do this? Well because this way I can deploy the exact same environment anywhere. If I get a new computer, I’m only one command line away from a functioning environment. If I want to dockerize a {targets} pipeline, I can write a new Dockerfile that builds upon this image which ensures that there are no discrepancies between the development environment and the production environment. And because I’m building the image on top of a Rocker image, everything just work. If I need to install a package that might be tricky to install (for example, a package that depends on {rJava}, using Docker might be the simplest way to get it to work.

So, here’s the Dockerfile:

# This builds upon the Rocker project's versioned image for R version 4.3
FROM rocker/r-ver:4.3

# install `gpg-agent` and `software-properties-common` which are needed to add an Ubuntu ppa to install Emacs
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    gpg-agent software-properties-common

# add the ppa which includes the latest version of Emacs
RUN add-apt-repository ppa:kelleyk/emacs

# Install `git`, `wget` and the latest `Emacs`
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    git \
    wget \
    emacs28-nox

# Install spacemacs by cloning its repository
RUN git clone -b develop https://github.com/syl20bnr/spacemacs ~/.emacs.d

# Download my .spacemacs config file
RUN wget https://raw.githubusercontent.com/b-rodrigues/dotfiles/master/dotfiles/.spacemacs -O ~/.spacemacs

# This launches emacs in daemon mode. This is needed to initialize spacemacs.
# Running it in daemon mode is required because a Dockerfile must be setup non-interactively
RUN emacs --daemon

# Install a bunch of Ubuntu dependencies. These are typical dependencies required to use some
# R packages on Linux.
RUN apt-get update \
   && apt-get install -y --no-install-recommends \
   aspell \
   aspell-en \
   aspell-fr \
   aspell-pt-pt \
   libfontconfig1-dev \
   libglpk-dev \
   libxml2-dev \
   libcairo2-dev \
   libgit2-dev \
   default-libmysqlclient-dev \
   libpq-dev \
   libsasl2-dev \
   libsqlite3-dev \
   libssh2-1-dev \
   libxtst6 \
   libcurl4-openssl-dev \
   libharfbuzz-dev \
   libfribidi-dev \
   libfreetype6-dev \
   libpng-dev \
   libtiff5-dev \
   libjpeg-dev \
   libxt-dev \
   unixodbc-dev \
   pandoc

# Download the latest version of Quarto
RUN wget https://github.com/quarto-dev/quarto-cli/releases/download/v1.3.340/quarto-1.3.340-linux-amd64.deb -O ~/quarto.deb

# Install the latest version of Quarto
RUN apt-get install --yes ~/quarto.deb

# Remove the installer
RUN rm ~/quarto.deb

# Create a directory to host my projects
RUN mkdir /root/projects/

# Write a bunch of lines to the .Rprofile
# This makes sure that the httpgd server runs on localhost and on the port 8888
RUN echo 'options(httpgd.host = "0.0.0.0", httpgd.port = 8888, httpgd.token = "aaaaaaaa")' >> /root/.Rprofile

# This option clones renv cache folders inside the root folder of the projects. This makes
# sure that they stay persistent across reboots.
RUN echo 'options(renv.config.cache.symlinks = FALSE)' >> /root/.Rprofile

# Serve shiny apps through localhost and port 8888
RUN echo 'options(shiny.host = "0.0.0.0", shiny.port = 8888)' >> /root/.Rprofile

# Set the CRAN package repositories to the PPPM at the 28th of April
RUN echo 'options(repos = c(REPO_NAME = "https://packagemanager.rstudio.com/cran/__linux__/jammy/2023-04-28"))' >> /root/.Rprofile

# Install the usual packages I use
RUN R -e "install.packages(c('quarto', 'remotes', 'tinytex', 'tidyverse', 'arrow', 'chronicler', 'janitor', 'targets', 'tarchetypes', 'openxlsx', 'shiny', 'flexdashboard', 'data.table', 'httpgd', 'blogdown', 'bookdown'))" 

# Install the g2r package (not yet available on CRAN)
RUN R -e "remotes::install_github('devOpifex/g2r')"

# Install a LaTeX distro using tinytex
RUN R -e "tinytex::install_tinytex()"

# Install hugo for blogging
RUN R -e "blogdown::install_hugo()"

# Expose port 8888
EXPOSE 8888

(and here’s the repository where you can find it).

I’ve explained each line of the Dockerfile using comments in the Dockerfile itself. But before explaining it in more detail, here’s a word from this blog post’s sponsor: me, I’m this post’s sponsor.

If you have read until here dear reader, let me express my gratitude by offering you a discount code to purchase a DRM-free Epub and PDF version of my book, Building reproducible analytical pipelines with R (that you can also read for free here by the way). Using the discount code you can get a DRM-free epub and PDF version of the book for 14.99 instead of 19.99! If you want a good old physical book instead, you’ll need to wait some more, I still need to get the formatting right before making it available through Amazon self-publishing service.

Now back to our Dockerfile. There are several decisions that I took that I need explain: first, why use a versioned image, and why use the PPPM at a specific date? I did this so that it doesn’t matter when I build the image, I always know which version of R and packages I get. Then, what’s with all the options that I write to the .Rprofile? Well, don’t forget that when I will be running the Docker container defined by this image, I will be using Emacs inside a terminal. So if I want to see plots for example, I need to use the {httpgd} package. This package provides a graphics device that runs on a web server, so if I tell {httpgd} to serve the images over port 8888, and then expose this port in the Dockerfile, I can access {httpgd} from my web browser by pointing it to http://localhost:8888. Here’s how this looks like:

The terminal on top of the image is running my dockerized environment, and below you see my web browser on to the http://localhost:8888/live?token=aaaaaaaa url to access the {httpgd} web server that is running inside the Docker container. And it’s the same logic with Shiny: if I’m working on a Shiny app from inside the container, I can access it by going to http://localhost:8888/. Now, I have to do all of this because I’m running Emacs, but if you’re developing with RStudio, you could instead run RStudio server, access it on http://localhost:8888/, and then no need to think about configuring on which ports {httpgd} serves images, or on which port Shiny apps should run. Everything will be directly visible from within RStudio. Here is the Dockerfile to run R version 4.3 with RStudio as the IDE. If you want to use this, you could simply start from the above Dockerfile and then add the stuff you need, for example:

FROM rocker/rstudio:4.3.0

# and add what you want below like installing R packages and whatnot

There is still one important thing that you should know before using a dockerized development environment: a running Docker container can be changed (for example, you could install new R packages), but once you shut it down and restart it, any change will be lost. So how do you save your work? Well, you need to run the Docker image with a volume. A volume is nothing more than a folder on your computer that is linked to a folder inside the Docker container. Anything that gets saved there from the Docker container will be available on your computer, and vice-versa. Here is the command that I use to run my container:

docker run --rm -it --name ess_dev_env -p 8888:8888 -v /home/path_to_volume/folder:/root/projects:rw brodriguesco/ess_dev_env:main-cdcb1719d emacs

Take note of the -v flag, especially what comes after: /home/path_to_volume/folder:/root/projects:rw. /home/path_to_volume/folder is the folder on my computer, and it is linked to the /root/projects folder inside the Docker container. When I run the above command inside a terminal, Spacemacs starts and I can get to work! If you build a development environment based on RStudio, you would essentially use the same command, you would only need to set a password to login first (read the instructions here).

Also, if you forgot to add a package and want to install it and make this change permanent, the best way is to add it to the Dockerfile and rebuild the image. I’ve streamlined this process by using Github Actions to build images and push them to Docker Hub. Take a look at the Github repository where my Dockerfile is hosted, and if you are familiar with Github Actions, take a look at my workflow file. You’ll see that I’ve set up Github Actions to build the Docker image and push it to Docker Hub each time I commit, and name the Docker image ess_dev_env:main-xxxxx where xxxxx is the corresponding commit hash on Github (so I can easily know which image was built with which commit).

I’ll be using this dockerized image for some time, and see how it feels. For now, it works quite well!

Hope you enjoyed! If you found this blog post useful, you might want to follow me on Mastodon or twitter for blog post updates and buy me an espresso or paypal.me, or buy my ebooks. You can also watch my videos on youtube. So much content for you to consoom!

Buy me an EspressoBuy me an Espresso