---
title: "Using Nix inside Docker"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Using Nix inside Docker}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r, include=FALSE}
library(rix)
```

## Introduction

It might look like Nix is an alternative to Docker, but that's not really the
case. Docker is a containerisation tool, while Nix is a package manager. You can
use Nix in such a way that you don't need Docker anymore, but if you're already
invested in Docker, you don't have to abandon it and can still benefit from Nix.

## A generic Dockerfile

This `Dockerfile` uses `ubuntu:latest` as a base image, and then uses the Nix
package manager to set up a complete development environment:

```
FROM ubuntu:latest

RUN apt update -y

RUN apt install curl -y

# We don't have R nor {rix} in this image, so we can bootstrap it by downloading
# the default.nix file that comes with {rix}. You can also download it beforehand
# and then copy it to the Docker image
RUN curl -O https://raw.githubusercontent.com/ropensci/rix/main/inst/extdata/default.nix

# Copy a script to generate the environment of interest using {rix}
COPY generate_env.R .

# The next 4 lines install Nix inside Docker. See the Determinate Systems installer's documentation
RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \
  --extra-conf "sandbox = false" \
  --init none \
  --no-confirm

# Adds Nix to the path, as described by the Determinate Systems installer's documentation
ENV PATH="${PATH}:/nix/var/nix/profiles/default/bin"
ENV user=root

# Set up rstats-on-nix cache
# Thanks to the rstats-on-nix cache, precompiled binary packages will
# be downloaded instead of being compiled from source
RUN mkdir -p /root/.config/nix && \
    echo "substituters = https://cache.nixos.org https://rstats-on-nix.cachix.org" > /root/.config/nix/nix.conf && \
    echo "trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= rstats-on-nix.cachix.org-1:vdiiVgocg6WeJrODIqdprZRUrhi1JzhBnXv7aWI6+F0=" >> /root/.config/nix/nix.conf

# This will overwrite the default.nix we downloaded previously with a new
# expression generated from running `generate_env.R`
RUN nix-shell --run "Rscript generate_env.R"

# We now build the environment
RUN nix-build

# Finally, we run `nix-shell`. This will get executed when running
# containers from this image. You can of course put anything in here
CMD nix-shell
```

It doesn't matter that we are using `ubuntu:latest` as a base image, which is
usually not recommended for reproducibility purposes, since it is Nix that takes
care of ensuring that our environment is reproducible.

Here is an example of the `generate_env.R` file:

```{r, eval = FALSE}
library(rix)

rix(
  r_ver = "4.3.1",
  r_pkgs = c("dplyr", "ggplot2"),
  ide = "none",
  project_path = ".",
  overwrite = TRUE
)
```

Using Nix to handle the setup of the environment, even inside Docker, creates a
nice separation of concerns. On one hand, you can continue using Docker to serve
applications, and on the other hand, you can use Nix to ensure you don't have to
store images, as you can always rebuild the correct environment in a completely
reproducible manner.

You can build the image using `docker build -t my_image .` and then run a
container with `docker run --rm -it --name my_rix_container my_image` which will
drop you in an interactive Nix shell. Of course, you can replace the last `CMD`
statement with whatever you need.

## Dockerizing a Shiny application

*Dockerizing* a Shiny application using Nix is very easy as well. You can keep
almost exactly the same `Dockerfile` as above, you only need to add the required
`ui.R` and `server.R` files (and any other files needed by your app), and expose
the port you want:

```
FROM ubuntu:latest

RUN apt update -y

RUN apt install curl -y

# Get a default.nix with R and rix
RUN curl -O https://raw.githubusercontent.com/ropensci/rix/main/inst/extdata/default.nix

# Copy a script to generate the environment of interest using rix
COPY generate_env.R .

# Copy the required scripts for the app
COPY ui.R .
COPY server.R .

RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux \
  --extra-conf "sandbox = false" \
  --init none \
  --no-confirm

ENV PATH="${PATH}:/nix/var/nix/profiles/default/bin"
ENV user=root

# Set up rstats-on-nix cache
# Thanks to the rstats-on-nix cache, precompiled binary packages will
# be downloaded instead of being compiled from source
RUN mkdir -p /root/.config/nix && \
    echo "substituters = https://cache.nixos.org https://rstats-on-nix.cachix.org" > /root/.config/nix/nix.conf && \
    echo "trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= rstats-on-nix.cachix.org-1:vdiiVgocg6WeJrODIqdprZRUrhi1JzhBnXv7aWI6+F0=" >> /root/.config/nix/nix.conf

# This will overwrite the default.nix we downloaded with a new one
RUN nix-shell --run "Rscript generate_env.R"

EXPOSE 3838

RUN nix-build

CMD nix-shell --run 'Rscript -e "shiny::runApp(port = 3838, host = \"0.0.0.0\")"'
```

adapt the `generate_env.R` script:

```{r, eval = FALSE}
library(rix)

rix(
  r_ver = "4.2.2",
  r_pkgs = "shiny",
  ide = "none",
  project_path = ".",
  overwrite = TRUE
)
```

Here is the code of a simple Shiny app (it's the K-means app from the Shiny
examples gallery):

- ui.R:

```{r, eval = FALSE}
# k-means only works with numerical variables,
# so don't give the user the option to select
# a categorical variable
vars <- setdiff(names(iris), "Species")

pageWithSidebar(
  headerPanel("Iris k-means clustering"),
  sidebarPanel(
    selectInput("xcol", "X Variable", vars),
    selectInput("ycol", "Y Variable", vars, selected = vars[[2]]),
    numericInput("clusters", "Cluster count", 3, min = 1, max = 9)
  ),
  mainPanel(
    plotOutput("plot1")
  )
)
```

- server.R:

```{r, eval = FALSE}
function(input, output, session) {
  # Combine the selected variables into a new data frame
  selectedData <- reactive({
    iris[, c(input$xcol, input$ycol)]
  })

  clusters <- reactive({
    kmeans(selectedData(), input$clusters)
  })

  output$plot1 <- renderPlot({
    palette(c(
      "#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",
      "#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999"
    ))

    par(mar = c(5.1, 4.1, 0, 1))
    plot(selectedData(),
      col = clusters()$cluster,
      pch = 20, cex = 3
    )
    points(clusters()$centers, pch = 4, cex = 4, lwd = 4)
  })
}
```

Build the image with:

```
docker build -t shiny_app .
```

and run a container with:

```
docker run --rm -p 3838:3838 --name my_container shiny_app
```

## NixOS

You can also your image from the [NixOS Docker
image](https://hub.docker.com/r/nixos/nix/) instead of Ubuntu, in which
case you don't need to install Nix. NixOS is a full GNU/Linux
distribution that uses Nix as its system package manager.
