Code

On this page we will:

  • See how to add code cells to a Quarto document.
  • Learn how to deploy sites via GitHub actions when they contain executable code.

1 Code in Quarto pages

In a Quarto document, you can include code cells and choose whether they run when you render the site.

1.1 Executable code

A code cell is a fenced block like:

```{python}
print("Hello from Python")
```

If the cell has a language (for example {python}, {r}, {bash}), Quarto can execute it when you render.

That means Quarto actually runs the code, captures the output (and any figures), and inserts those results into the page. This is powerful, because your website and your analysis stay in sync - if the code or data change, you just re‑render.

Below are some examples from the Quarto documentation showing a map and an interactive Plotly plot:

1.2 Non-executable code

If you just want to show code (not run it), you can either:

  1. Add a dot before the language.

{.python} print("Hello from Python")

  1. Make the cell as eval: false.
```{python}
#| eval: false
print("Hello from Python")
```
  1. Set all cells in a Quarto document to eval: false in the YAML:
---
title: "Title"
execute:
  eval: false
---

2 Deploying your site with executable code

As explained on the Hosting, we can deploy a Quarto site using a GitHub Actions workflow, allowing us to host it on GitHub pages.

If your pages contain executable code, the workflow must be amended to installed the required programming language and dependencies.

2.1 Python

2.1.1 requirements.txt

Use this pattern when you have a requirements.txt file listing your Python packages.

This example file is from: https://github.com/pythonhealthdatascience/stars_wp1_summary/.

It is based on the suggested file in the Quarto documentation.

name: Update GitHub pages
run-name: Render Quarto website and publish on GitHub pages

# Source: https://quarto.org/docs/publishing/github-pages.html

on:
  push:
    branches: main
  workflow_dispatch:

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
        
      - name: Set up Quarto
        uses: quarto-dev/quarto-actions/setup@v2
      
      - name: Install python and dependencies
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
          cache: 'pip'
      - run: pip install -r requirements.txt

      - name: Render and publish to GitHub pages
        uses: quarto-dev/quarto-actions/publish@v2
        with:
          target: gh-pages
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2.1.2 conda

If you are using a Conda environment with an environment.yml/environment.yaml file, you can use this action.

It is taken from: https://github.com/ambmodels/ambdes.

name: Render quarto site and publish on GitHub pages
run-name: Render quarto site and publish on GitHub pages

on:
  push:
    branches: main
  workflow_dispatch:

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Quarto
        uses: quarto-dev/quarto-actions/setup@v2

      - name: Set up Conda environment
        uses: conda-incubator/setup-miniconda@v3
        with:
          environment-file: environment.yaml
          activate-environment: ambdes
          auto-activate-base: false
          channels: conda-forge
  
      - name: Render and publish to GitHub pages
        uses: quarto-dev/quarto-actions/publish@v2
        with:
          target: gh-pages
          path: docs
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2.1.3 uv

If you are using uv, you can use a file like below, which is from: https://github.com/lintquarto/lintquarto/.

name: docs
run-name: Render Quarto website and publish on GitHub pages

on:
  push:
    branches: main
  workflow_dispatch:

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Check out repository
        uses: actions/checkout@v4
        
      - name: Set up Quarto
        uses: quarto-dev/quarto-actions/setup@v2
      
      - name: Set up Python from .python-version
        uses: actions/setup-python@v4
        with:
          python-version-file: ".python-version"

      - name: Install uv
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true

      - name: Sync project from uv.lock
        run: uv sync --locked --all-extras --dev

      - name: Quarto render
        working-directory: docs
        run: uv run quarto render

      - name: Quarto publish to gh-pages
        if: github.ref == 'refs/heads/main'
        working-directory: docs
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          uv run quarto publish gh-pages --no-prompt --no-browser

2.2 R

If you are using R with renv, you can use an action like below, which is copied from the Quarto documentation.

on:
  workflow_dispatch:
  push:
    branches: main

name: Quarto Publish

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Quarto
        uses: quarto-dev/quarto-actions/setup@v2

      - name: Install R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: '4.2.0'

      - name: Install R Dependencies
        uses: r-lib/actions/setup-renv@v2
        with:
          cache-version: 1

      - name: Render and Publish
        uses: quarto-dev/quarto-actions/publish@v2
        with:
          target: gh-pages
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2.3 Python and R

For sites running both Python and R, we found the following approach to work:

  1. Build a Docker image that contains everything needed.
  2. Push that image to the GitHub Container Registry (GHCR).
  3. Run the Quarto render inside the Docker container.

We have used this approach in two of our projects: the DES RAP Book and the Python package vidigi.

2.3.1 Workflow file

name: Publish docker on GHCR and quarto on GitHub pages
run-name: Publish docker on GHCR and quarto on GitHub pages

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      force_docker_build:
        description: 'Force Docker build (skip change detection)'
        required: true
        type: boolean
        default: false
      skip_docker_build:
        description: 'Skip Docker build (use last built image)'
        required: true
        type: boolean
        default: false

jobs:
  publish-docker:
    name: Publish docker
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Check if Docker build needed
        if: ${{ !inputs.skip_docker_build && !inputs.force_docker_build }}
        id: changes
        uses: dorny/paths-filter@v3
        with:
          filters: |
            docker:
              - 'Dockerfile'
              - 'environment.yaml'
              - 'renv.lock'
              - 'renv/**'

      - name: Login to GitHub Container Registry
        if: >
          (!inputs.skip_docker_build) &&
          (inputs.force_docker_build || steps.changes.outputs.docker == 'true')
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build the Docker image
        if: >
          (!inputs.skip_docker_build) &&
          (inputs.force_docker_build || steps.changes.outputs.docker == 'true')
        uses: docker/build-push-action@v6
        with:
          context: .
          tags: |
            ghcr.io/${{ github.repository_owner }}/des_rap_book:latest
          push: true
          build-args: |
            GIT_PAT=${{ secrets.GITHUB_TOKEN }}
  
  build-deploy:
    runs-on: ubuntu-latest
    needs: [publish-docker]
    container:
      image: ghcr.io/${{ github.repository_owner }}/des_rap_book:latest
      credentials:
        username: ${{ github.repository_owner }}
        password: ${{ secrets.GITHUB_TOKEN }}
    permissions:
      contents: write
      packages: read
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Configure Git safe directory
        run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

      - name: Update YAML date with last commit
        run: bash ./update-quarto-date.sh

      - name: Render and publish to GitHub pages
        uses: quarto-dev/quarto-actions/publish@v2
        with:
          target: gh-pages
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2.4 Dockerfile

FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# ============================================================
# STAGE 1: Install ALL system dependencies FIRST
# ============================================================
# This must happen before Conda to ensure R compiles against
# system libraries, not conda libraries
RUN set -eux; \
    # retry apt-get update a few times in case mirrors are mid-sync
    for i in 1 2 3; do \
      apt-get update && break; \
      echo "apt-get update failed, retrying ($i/3)..."; \
      sleep 5; \
    done; \
    apt-get install -y --no-install-recommends \
        # Basic utilities
        wget ca-certificates gnupg software-properties-common \
        dirmngr locales git \
        # R compilation dependencies
        build-essential gfortran \
        libxml2-dev \
        libglpk-dev \
        libgmp-dev \
        libblas-dev \
        liblapack-dev \
        libcurl4-openssl-dev \
        libssl-dev \
        libfontconfig1-dev \
        libfreetype6-dev \
        libharfbuzz-dev \
        libfribidi-dev \
        libpng-dev \
        libtiff5-dev \
        libjpeg-dev \
        # Chrome dependencies (for Kaleido/plotly)
        fonts-liberation \
        libasound2t64 \
        libatk-bridge2.0-0 \
        libatk1.0-0 \
        libatspi2.0-0 \
        libcups2 \
        libdbus-1-3 \
        libgbm1 \
        libgtk-3-0 \
        libnspr4 \
        libnss3 \
        libvulkan1 \
        libxcomposite1 \
        libxdamage1 \
        libxkbcommon0 \
        libxrandr2 \
        xdg-utils && \
    locale-gen en_GB.UTF-8 && \
    rm -rf /var/lib/apt/lists/*

# Set locale environment variables for Python and other tools
ENV LANG=en_GB.UTF-8
ENV LC_ALL=en_GB.UTF-8
ENV PYTHONIOENCODING=utf-8

# ============================================================
# STAGE 2: Install R from CRAN
# ============================================================
RUN wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | \
    gpg --dearmor -o /usr/share/keyrings/r-project.gpg && \
    echo "deb [signed-by=/usr/share/keyrings/r-project.gpg] https://cloud.r-project.org/bin/linux/ubuntu noble-cran40/" \
        > /etc/apt/sources.list.d/r-project.list && \
    apt-get update && \
    apt-get install -y --no-install-recommends r-base r-base-dev && \
    rm -rf /var/lib/apt/lists/*

# ============================================================
# STAGE 3: Install Quarto
# ============================================================
RUN wget -qO /tmp/quarto.deb https://quarto.org/download/latest/quarto-linux-amd64.deb && \
    apt-get update && \
    apt-get install -y /tmp/quarto.deb && \
    rm /tmp/quarto.deb && \
    rm -rf /var/lib/apt/lists/*

# ============================================================
# STAGE 4: Install Google Chrome (required by Kaleido for plotly)
# ============================================================
RUN wget -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
apt-get update && \
apt-get install -y --no-install-recommends /tmp/chrome.deb && \
rm /tmp/chrome.deb && \
rm -rf /var/lib/apt/lists/*

# ============================================================
# STAGE 5: Install Miniconda (use explicit paths, not PATH)
# ============================================================
ENV CONDA_DIR=/opt/conda
RUN wget -qO /tmp/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \
    bash /tmp/miniconda.sh -b -p "$CONDA_DIR" && \
    rm /tmp/miniconda.sh

RUN $CONDA_DIR/bin/conda config --system --set always_yes yes && \
    $CONDA_DIR/bin/conda config --system --set changeps1 no

# ============================================================
# STAGE 6: Set up project and create conda environment
# ============================================================
WORKDIR /workspace
COPY . /workspace
RUN rm -f /workspace/.Renviron

# Accept Anaconda ToS for required channels (non-interactive)
RUN $CONDA_DIR/bin/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \
    $CONDA_DIR/bin/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r

# Create conda environment using explicit path (NOT in PATH yet)
RUN $CONDA_DIR/bin/conda env create -f environment.yaml

# ============================================================
# STAGE 7: Install R packages WITHOUT conda in PATH
# ============================================================
# CRITICAL: R package compilation must happen with system
# libraries, NOT conda libraries in PATH
ENV RENV_PATHS_LIBRARY=/workspace/renv/library

RUN Rscript -e "install.packages('renv', repos = 'https://cloud.r-project.org')" && \
    Rscript -e "renv::restore()"

# ============================================================
# STAGE 8: Activate conda environment for RUNTIME only
# ============================================================
# Now that R packages are installed, it's safe to add conda to PATH
ENV CONDA_DEFAULT_ENV=des-rap-book
ENV PATH="/opt/conda/envs/des-rap-book/bin:${PATH}"
ENV RETICULATE_PYTHON=/opt/conda/envs/des-rap-book/bin/python

RUN echo "conda activate des-rap-book" >> /root/.bashrc

CMD ["/bin/bash"]