How to Set Up Quarto with Docker, Part 2: Dynamic Content
Quarto is an open-source scientific and technical publishing system built on Pandoc. It is a cross-platform tool to create dynamic content with Python, R, Julia, and Observable.
In the previous post about Quarto, we reviewed static hosting options with Docker containers, including single files and projects, such as books and websites.
Most of these options do not really require the hosting to be container based, because the rendered HTML can be published to static hosting sites, such as GitHub pages, or Netlify.
Even interactive documents that rely on widgets (Jupyter Widgets or htmlwidgets) or Observable JS can be hosted as static files. But just like with R Markdown, you can use Shiny for interactivity. This is the focus of Part 2.
Prerequisites
The code from this post can be found in the analythium/quarto-docker-examples GitHub repository:
You will also need Docker Desktop installed.
If you want Quarto to be installed on your local machine, follow these two links to get started: Quarto docs, and RStudio install resources.
Server: Shiny
You can specify Shiny as the engine to run the dynamic Quarto document. Here is the shiny/index.qmd
file for the classic Old Faithful histogram with a slider input:
---
title: "Old Faithful"
execute:
echo: false
format: html
server: shiny
---
```{r}
sliderInput("bins", "Number of bins:",
min = 1, max = 50, value = 30)
plotOutput("distPlot")
```
```{r}
#| context: server
output$distPlot <- renderPlot({
x <- faithful[, 2] # Old Faithful Geyser data
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
```
The YAML header options specify server: shiny
and the context: server
option is added to the second code chunk. This specifies this chunk to be run within the Shiny server. This behaviour is analogous to the R Markdown Shiny runtime.
Now on to the Dockerfile:
FROM analythium/r2u-quarto:20.04
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /home/app
COPY shiny .
RUN chown app:app -R /home/app
USER app
EXPOSE 8080
CMD ["quarto", "serve", "index.qmd", "--port", "8080", "--host", "0.0.0.0"]
We use the analythium/r2u-quarto:20.04
parent image in the FROM
instruction – this is the image we build in Part 1. The rest of the Dockerfile is pretty standard, create a user, copy the shiny
folder contents into the image, set owner and user for the Docker runtime.
The render step will happen when the container is spin up. We use the quarto serve index.qmd --port 8080 --host 0.0.0.0
command to specify port and host IPv4 address. The quarto serve
command will render the document before serving. Note that in this example, we do not render the Quarto document up front.
Build and tag the image, then test:
docker build -f Dockerfile.shiny -t analythium/quarto:shiny .
docker run -p 8080:8080 analythium/quarto:shiny
In your browser, go to http://localhost:8080
to see the interactive Quarto document with the slider and histogram:
If you can't kill the container with Ctrl+C, try getting the container ID with docker ps
and then use docker kill $ID
.
If you want to serve this document from R, you can use the https://CRAN.R-project.org/package=quarto: quarto::quarto_serve("index.qmd")
.
Shiny prerendered
Depending on the complexity of your document, rendering at the container launch time will significantly increase "cold start" time, i.e. the time you have to wait between starting the container and seeing the HTML in the browser.
To remedy this, we can render the document as part of the Docker build process. This is analogous to the R Markdown prerendered Shiny runtime (shinyrmd
).
Let's try a different Quarto example this time showing K-means clustering with the Iris data set using a custom page layout. We have the same server: shiny
option as before:
---
title: "Iris K-Means Clustering"
format:
html:
page-layout: custom
server: shiny
---
```{r}
#| panel: sidebar
vars <- setdiff(names(iris), "Species")
selectInput('xcol', 'X Variable', vars)
selectInput('ycol', 'Y Variable', vars, selected = vars[[2]])
numericInput('clusters', 'Cluster count', 3, min = 1, max = 9)
```
```{r}
#| panel: fill
plotOutput('plot1')
```
```{r}
#| context: server
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)
})
```
This is the corresponding Dockerfile:
FROM analythium/r2u-quarto:20.04
RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /home/app
COPY shiny-prerendered .
RUN quarto render index.qmd
RUN chown app:app -R /home/app
USER app
EXPOSE 8080
CMD ["quarto", "serve", "index.qmd", "--port", "8080", "--host", "0.0.0.0", "--no-render"]
There are two differences compared to the previous example:
- we use
quarto render index.qmd
to render the document during the Docker build - then use the
--no-render
flag withquarto serve
to avoid unnecessary rendering when the container is started
When we render the file, the UI elements get rendered, while the code chunks marked as context: server
will wait until the rendered document is served. Read more about the render and server contexts.
Build and run the image:
docker build \
-f Dockerfile.shiny-prerendered \
-t analythium/quarto:shiny-prerendered .
docker run -p 8080:8080 analythium/quarto:shiny-prerendered
Then check you browser at http://localhost:8080
:
That is it!
Conclusions
Serving interactive Shiny server based Quarto documents is very similar to serving R Markdown with Docker. The Quarto logic follows the prerendered Shiny runtime of R Markdown. But you need to use the quarto render
and quarto serve
commands with the right flags to avoid unnecessary rendering of the UI elements in your document.
I am sure interactivity in Quarto will soon include other options, like Shiny for Python and Shinylive. As things advance and more of the data-heavy computations can be pushed to the client (i.e. your browser), the need for a Dockerized setup will be less important. But until then, the Quarto+Docker examples from this 2-part series will provide a starting point for your adventures.