# Using Nix inside Docker

## 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:

    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:

    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:

    # 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:

    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 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.