A Shiny app depends on its R runtime and package library as well as its source files. Packaging those parts in one Docker image gives the app a repeatable process, port, and health signal on each host that runs the image.
The official r-base image supports common Linux architectures, while Debian's r-cran-shiny package installs Shiny and its runtime dependencies through the image package manager. The final process runs as the unprivileged nobody account, while root owns the read-only application source.
The published host port is restricted to 127.0.0.1 so the app is reachable from the Docker host without listening on every network interface. Put an authenticated TLS reverse proxy in front of the loopback port before exposing an app that handles private data.
Related: How to build a Docker image
Related: How to configure Nginx as a reverse proxy
$ mkdir -p shiny-demo/app
library(shiny)
fluidPage(
titlePanel("Shiny container check"),
mainPanel(
sliderInput("bins", "Histogram bins", 5, 30, 12),
plotOutput("histogram"),
textOutput("status")
)
)
function(input, output, session) {
output$histogram <- renderPlot({
hist(faithful$eruptions, breaks = input$bins, col = "#4c78a8")
})
output$status <- renderText({
paste("Rendered with", input$bins, "bins")
})
}
FROM r-base:4.6.1
RUN apt-get update \
&& apt-get install --yes --no-install-recommends r-cran-shiny \
&& rm -rf /var/lib/apt/lists/*
COPY app/ /srv/shiny/
RUN chmod -R a-w /srv/shiny
ENV HOME=/tmp
USER nobody
EXPOSE 3838
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3838/ || exit 1
CMD ["Rscript", "-e", "shiny::runApp('/srv/shiny', host='0.0.0.0', port=3838)"]
The application files remain root-owned and read-only, HOME=/tmp gives the runtime a writable cache location, and USER nobody prevents the R process from starting as root.
Tool: Dockerfile Security Basics Checker
$ cd shiny-demo
$ docker build --tag shiny-demo:4.6.1 . ##### snipped ##### #9 naming to docker.io/library/shiny-demo:4.6.1 done #9 DONE 0.1s
$ docker image inspect --format '{{.Config.User}}' shiny-demo:4.6.1 nobody
$ docker run --detach --name shiny-demo --publish 127.0.0.1:3838:3838 shiny-demo:4.6.1 ef625edff38bf742eb93caebfa431de5bb02a2e0211db4e15da665dc1b79a878
$ docker exec shiny-demo stat --format '%U %G %A %n' /srv/shiny/ui.R /srv/shiny/server.R root root -r--r--r-- /srv/shiny/ui.R root root -r--r--r-- /srv/shiny/server.R
$ docker inspect --format '{{.State.Health.Status}}' shiny-demo healthy

