How to Set Up Quarto with Docker, Part 1: Static 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.

Quarto.org

Quarto documents follow literate programming principles where the code "chunks" are weaved together with text chunks. From an R programming perspective, Quarto documents with a .qmd file extension are very similar to R Markdown documents (.Rmd).

Both .Rmd and .qmd files have a YAML header between the triple dashes. The main difference between the two is how the options are specified. Here is a Quarto example:

---
title: "Quarto Demo"
format: 
  html:
    code-fold: true
---

## Air Quality

@fig-airquality further explores the impact of temperature on ozone level.

```{r}
#| label: fig-airquality
#| fig-cap: Temperature and ozone level.
#| warning: false

library(ggplot2)

ggplot(airquality, aes(Temp, Ozone)) + 
  geom_point() + 
  geom_smooth(method = "loess"
)
```

In this post, I am going to walk you through how to containerize Quarto documents. You will learn how to install Quarto inside a Docker container and how to use the command line tool to render and eventually serve static Quarto documents.

This post builds on a previous R Markdown-focused article:

Containerizing Interactive R Markdown Documents
R Markdown is a reproducible authoring format supporting dozens of static and dynamic output formats. Let’s review why and how you should containerize Rmd files.

Prerequisites

The code from this post can be found in the analythium/quarto-docker-examples GitHub repository:

GitHub - analythium/quarto-docker-examples: Quarto Examples with Docker
Quarto Examples with Docker. Contribute to analythium/quarto-docker-examples development by creating an account on GitHub.

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.

Create a Quarto parent image

We build a parent image with Quarto installed so that we can use this image in subsequent FROM instructions in the Dockerfiles. See the Dockerfile.base in the repository. The image is based on the `is based on the eddelbuettel/r2u image using Ubuntu 20.04.

FROM eddelbuettel/r2u:20.04

RUN apt-get update && apt-get install -y --no-install-recommends \
    pandoc \
    pandoc-citeproc \
    curl \
    gdebi-core \
    && rm -rf /var/lib/apt/lists/*

RUN install.r \
    shiny \
    jsonlite \
    ggplot2 \
    htmltools \
    remotes \
    renv \
    knitr \
    rmarkdown \
    quarto

RUN curl -LO https://quarto.org/download/latest/quarto-linux-amd64.deb
RUN gdebi --non-interactive quarto-linux-amd64.deb

CMD ["bash"]

The important bits are to have curl and gdebi installed so that we can grab the quarto-linux-amd64.deb file and install the Quarto command line tool. This will install the latest version.

If you want a specific Quarto version (like 0.9.522), use the following lines instead, and change the version to the one you want:

ARG QUARTO_VERSION="0.9.522"
RUN curl -o quarto-linux-amd64.deb -L https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb
RUN gdebi --non-interactive quarto-linux-amd64.deb

Now we can build the image:

docker build \
    -f Dockerfile.base \
    -t analythium/r2u-quarto:20.04 .

We can run the container interactively to check the installation:

docker run -it --rm analythium/r2u-quarto:20.04 bash

Type quarto check, you should see check marks:

root@d8377016be7f:/# quarto check

[✓] Checking Quarto installation......OK
      Version: 1.0.36
      Path: /opt/quarto/bin

[✓] Checking basic markdown render....OK

[✓] Checking Python 3 installation....OK
      Version: 3.8.10
      Path: /usr/bin/python3
      Jupyter: (None)

      Jupyter is not available in this Python installation.
      Install with python3 -m pip install jupyter

[✓] Checking R installation...........OK
      Version: 4.2.1
      Path: /usr/lib/R
      LibPaths:
        - /usr/local/lib/R/site-library
        - /usr/lib/R/site-library
        - /usr/lib/R/library
      rmarkdown: 2.14

[✓] Checking Knitr engine render......OK

We are missing Jupyter, but we won't need it for this tutorial. If you want to learn more about the available quarto commands and options, type quarto help in the shell to see what is available.

Type exit to quit the session.

Render a static file

Let's take that air quality example from above that you can find in the static-file/index.qmd file of the repository. The corresponding Dockerfile.static-file contains the following:

FROM analythium/r2u-quarto:20.04 AS builder

COPY static-file /app
WORKDIR /app
RUN mkdir output
RUN quarto render index.qmd
RUN rm index.qmd

FROM ghcr.io/openfaas/of-watchdog:0.9.6 AS watchdog

FROM alpine:latest
RUN mkdir /app
COPY --from=builder /app /app
COPY --from=watchdog /fwatchdog .
ENV mode="static"
ENV static_path="/app"
HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1
CMD ["./fwatchdog"]

This pattern is called a multi-stage Docker build, that we have covered in the previous R Markdown post:

  1. we use the Quarto image to copy the .qmd file, then use the quarto render command to render it to HTML
  2. we pick the of-watchdog that is a tiny HTTP server to host our HTML file
  3. the minimal Alpine Linux image is then used to put the of-watchdog into and serve the HTML file from the /app folder through port 8080.

We can build the image and make sure things work as expected by running the image and visiting http://localhost:8080:

docker build \
    -f Dockerfile.static-file \
    -t analythium/quarto:static-file .

docker run -p 8080:8080 analythium/quarto:static-file

You should see the rendered document:

Static Quarto demo served from a Docker container

Render a static project

Quarto projects are directories that contain multiple files that share the YAML metadata that is placed in the _quarto.yml file at the root of the project.

Currently, there are two types of projects: websites, and books.

Website

Quarto Websites are a convenient way to publish groups of documents. Documents published as part of a website share navigational elements, rendering options, and visual style.

Use the following command to create a website template locally inside the static-website directory:

quarto create-project static-website --type website

The command creates the following files:

static-website
├── _quarto.yml
├── about.qmd
├── index.qmd
└── styles.css

The _quarto.yml file contains the following:

project:
  type: website

website:
  title: "static-website"
  navbar:
    left:
      - href: index.qmd
        text: Home
      - about.qmd

format:
  html:
    theme: cosmo
    css: styles.css
    toc: true

You have two options to build the Docker image:

  1. render the HTML locally and copy the rendered files into the image
  2. copy the project into the image then render the HTML files inside the Docker image

Option 1: local rendering

Render the project as quarto render static-website --output-dir output into a folder called output then use this Dockerfile. We use the --output-dir output option to override the default _book output directory.

FROM ghcr.io/openfaas/of-watchdog:0.9.6 AS watchdog
FROM alpine:latest
RUN mkdir /app
COPY static-website/output /app
COPY --from=watchdog /fwatchdog .
ENV mode="static"
ENV static_path="/app"
HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1
CMD ["./fwatchdog"]

Option 2: render inside Docker

The Dockerfile for the second option is very similar to the single file rendering example, no need to explain much:

FROM analythium/r2u-quarto:20.04 AS builder

COPY static-website /app
WORKDIR /app
RUN mkdir output
RUN quarto render --output-dir output

FROM ghcr.io/openfaas/of-watchdog:0.9.6 AS watchdog

FROM alpine:latest
RUN mkdir /app
COPY --from=builder /app/output /app
COPY --from=watchdog /fwatchdog .
ENV mode="static"
ENV static_path="/app"
HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1
CMD ["./fwatchdog"]

Build and run the image:

docker build \
    -f Dockerfile.static-website \
    -t analythium/quarto:static-website .

docker run -p 8080:8080 analythium/quarto:static-website

Go to your browser to see the rendered website template:

Static Quarto website served from a Docker container

Book

Quarto Books are combinations of multiple documents (chapters) into a single manuscript. [...] HTML books are actually just a special type of Quarto Website and consequently support all of the same features as websites including full-text search.

Use the quarto create-project static-book --type book command to create a book template locally. The template is in the static-book folder that contains the following files:

static-book
├── _quarto.yml
├── cover.png
├── index.qmd
├── intro.qmd
├── references.bib
├── references.qmd
└── summary.qmd

The _quarto.yml file now has the following contents:

project:
  type: book

book:
  title: "static-book"
  author: "Jane Doe"
  date: "7/25/2022"
  chapters:
    - index.qmd
    - intro.qmd
    - summary.qmd
    - references.qmd

bibliography: references.bib

format:
  html:
    theme: cosmo
  pdf:
    documentclass: scrreprt

Local rendering of the book requires a LaTeX installation on your local machine. If you want to go that way, render the book with quarto render static-book --output-dir output and modify to previous website example to COPY static-book/output /app. We use the --output-dir output option to override the default _book output directory.

Inside Docker, we can install LaTeX with the quarto install tool tinytex command in our Dockerfile:

FROM analythium/r2u-quarto:20.04 AS builder

COPY static-book /app
WORKDIR /app
RUN mkdir output
RUN quarto install tool tinytex
RUN quarto render --output-dir output

FROM ghcr.io/openfaas/of-watchdog:0.9.6 AS watchdog

FROM alpine:latest
RUN mkdir /app
COPY --from=builder /app/output /app
COPY --from=watchdog /fwatchdog .
ENV mode="static"
ENV static_path="/app"
HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1
CMD ["./fwatchdog"]

Build and run the image:

docker build \
    -f Dockerfile.static-book \
    -t analythium/quarto:static-book .

docker run -p 8080:8080 analythium/quarto:static-book

Visit http://localhost:8080 to see the rendered HTML version of the book:

Static Quarto book served from a Docker container

Render an interactive file with widgets

Static files can be interactive via incorporating JavaScript into the HTML, like interactive Plotly or Echarts graphs, or Leaflet maps.

From R, we can do this with the htmlwidget R package. Here is the static-widget/index.qmd file's contents:

---
title: "Interactivity with HTML Widgets"
format: 
  html:
    code-fold: true
---

## Leaflet

Example following [this](https://quarto.org/docs/interactive/widgets/htmlwidgets.html) page.

```{r}
library(leaflet)
leaflet() %>%
  addTiles() %>%  # Add default OpenStreetMap map tiles
  addMarkers(lng=174.768, lat=-36.852, popup="The birthplace of R")
```
## Layout

```{r}
#| layout: [[1,1], [1]]
library(dygraphs)
dygraph(fdeaths, "Female Deaths")
dygraph(mdeaths, "Male Deaths")
dygraph(ldeaths, "All Deaths")
```

The corresponding Dockerfile.static-widget file has a few lines to install a few packages: leaflet and dygraphs.

FROM analythium/r2u-quarto:20.04 AS builder

RUN install.r \
    dygraphs \
    leaflet

COPY static-widget /app
WORKDIR /app
RUN mkdir output
RUN quarto render index.qmd
RUN rm index.qmd

FROM ghcr.io/openfaas/of-watchdog:0.9.6 AS watchdog

FROM alpine:latest
RUN mkdir /app
COPY --from=builder /app /app
COPY --from=watchdog /fwatchdog .
ENV mode="static"
ENV static_path="/app"
HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1
CMD ["./fwatchdog"]

Finally, build and run the image:

docker build \
    -f Dockerfile.static-widget \
    -t analythium/quarto:static-widget .

docker run -p 8080:8080 analythium/quarto:static-widget

Open the rendered HTML in your browser to check the interactive parts:

Static but interactive Quarto document from a Docker container

Conclusions

We reviewed how to install Quarto inside Docker image, so that we can render static HTML documents and projects, including JavaScript-based interactivity.

You are now able to serve files, websites, and books from Docker containers, which gives you many self-hosting options beyond the publishing options mentioned in the official Quarto docs.

Further reading