<!--
  canonical-url: https://www.avonture.be/blog/quarto-devcontainer
  published:     2025-11-03
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Make your Quarto project Devcontainer-Ready — No More Setup Headaches

> Convert any Quarto project into a fully portable, VSCode-powered devcontainer in minutes.

---
![Make your Quarto project Devcontainer-Ready — No More Setup Headaches](/img/v2/devcontainer_quarto.webp)

**TL;DR**

This article details how to convert any Quarto project into a portable, VSCode-powered devcontainer, eliminating setup headaches. By adding three specific configuration files (`compose.yaml`, `Dockerfile`, `devcontainer.json`), users can create a fully equipped development environment. This setup includes pre-installed Quarto, configured VSCode extensions, and features like automatic preview and optional pre-commit hooks, simplifying documentation workflow.

I love using Quarto and I've plenty of *documentation* projects on my disk. I'm writing user documentation, developer ones and, too, installation guide for almost every project I'm coding.

If you've never run Quarto through Docker before, [Running Quarto Markdown in Docker](/blog/docker-quarto) is a good starting point; this article builds on that idea to give you a full VSCode devcontainer.

I quickly came up with the idea of creating a Docker image so that I could develop my documentation in peace, as well as a devcontainer configuration for VSCode.  Now, when I want to write my documentation, in less than a minute, I can create an environment entirely dedicated to writing documentation.

Among other things, I install Quarto, install and configure a number of extensions for VSCode, and launch Quarto in preview mode: as soon as I save a change, Quarto regenerates the documentation on its own.

Let's see how to do this; you'll see, it's so simple.

*The same devcontainer, specialized for slideshows, is used in [Level Up Your Presentations with Quarto, reveal.js, Decktape, Docker and DevContainers](/blog/running-revealjs-with-docker); and taken to industrial scale in [Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects](/blog/quarto-industrialisation).*

**Quick Jump:** [Let's get a simple Quarto project first](#lets-get-a-simple-quarto-project-first) | [We'll need to create three files](#well-need-to-create-three-files)

## Let's get a simple Quarto project first

For sure, if you've one, just jump in your project. Here, for this blog post, we'll just reuse an existing one.

By running the commands below in a console, you'll download just the `brand` folder from the `quarto-examples` project:

```bash
$ cd /tmp
git clone --filter=blob:none --sparse https://github.com/quarto-dev/quarto-examples
cd quarto-examples
git sparse-checkout set brand
cd brand/brand-simple
```

Run `code .` to open the project in VSCode and you'll get this:

![Opening the brand-simple project in VSCode](./images/opening-in-vscode.webp)

Here's where we're headed: open this same project as a devcontainer, run `quarto preview .`, and the site is already live — no Quarto install on your host required.

![Running quarto preview](./images/running_in_preview_mode.webp)

![The site is already running](./images/preview.webp)

Crazy easy no? Let's build it, starting with three files.

## We'll need to create three files

To get the best performance in terms of Docker image build speed and use of the Docker cache mechanism, we will need to create three files:

The `.devcontainer/compose.yaml` file is necessary to tell VSCode to create our Docker image if it does not already exist, or to reuse it if it does. This file is therefore essential in terms of performance.

The `.devcontainer/Dockerfile` file defines our Docker image: the binaries we need, user configuration, etc.

The `.devcontainer/devcontainer.json` file is used by VSCode to understand and build our working environment.

**Project: /tmp/quarto-examples/brand/brand-simple**

**.devcontainer/compose.yaml**

```yaml
---
# cspell:ignore tinytex

services:
  quarto-doc-base:
    image: quarto-devcontainer:latest

    build:
      context: .
      dockerfile: Dockerfile
      args:
        # We use default values here. The actual UID/GID mapping is handled
        # by the VS Code Dev Container CLI at runtime, overriding these defaults
        # when the container starts via the user: vscode directive in the Dockerfile.
        USER_UID: 1000
        USER_GID: 1000
        # Flags you'll need to adjust depending on your project
        INSTALL_CHROMIUM: "true"
        INSTALL_CSPELL: "false"
        INSTALL_GRAPHVIZ: "true"
        INSTALL_PRECOMMIT_HOOKS: "false"

    user: vscode

    working_dir: /workspace

    volumes:
      - ..:/workspace:cached
      - ./history/.bash_history:/home/vscode/.bash_history:rw
      - type: bind
        source: ${HOME}/.quarto
        target: /home/vscode/.quarto
      - type: bind
        source: ${HOME}/.ssh/id_ed25519
        target: /home/vscode/.ssh/id_ed25519
        read_only: true

    # Base command to keep the container running indefinitely
    command: /bin/sh -c "while sleep 1000; do :; done"
```

**.devcontainer/Dockerfile**

```docker
# syntax=docker/dockerfile:1.4

# cspell:disable
FROM ghcr.io/quarto-dev/quarto:1.8.25 AS base

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=1000

ARG INSTALL_CHROMIUM=false
ARG INSTALL_CSPELL=false
ARG INSTALL_GRAPHVIZ=false
ARG INSTALL_PRECOMMIT_HOOKS=false

# Yaml tooling
ARG YQ_VERSION=v4.42.1

# ---------- Stage 1: Install system dependencies ----------
#
# openssh-client  : used by git when pushing/pulling using git@ protocol
# sudo            : to easily install additional dependencies if needed
# texlive-*       : to render as PDF using "quarto render . --to pdf"
# yamllint        : to validate YAML frontmatter in .qmd files
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    set -eux; \
    apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        git \
        libnss3 libfontconfig1 libgbm1 \
        libsecret-1-dev \
        libx11-dev \
        libxkbfile-dev \
        openssh-client \
        texlive-xetex texlive-fonts-recommended texlive-plain-generic \
        unzip \
        sudo \
        yamllint \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Allow our user to run sudo without password
RUN echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Install yq (YAML validator) for validating YAML frontmatter
RUN --mount=type=cache,target=/var/cache/yq \
    set -eux; \
    curl -L --fail --silent --show-error \
        https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64 \
        -o /var/cache/yq/yq_linux_amd64; \
    install -m 0755 /var/cache/yq/yq_linux_amd64 /usr/local/bin/yq

# ---------- Stage 2: Extras - pre-commit, Node.js + cspell ----------
FROM base AS extras

ARG INSTALL_CSPELL
ARG INSTALL_GRAPHVIZ
ARG INSTALL_PRECOMMIT_HOOKS
ARG USERNAME

# Install pre-commit (without installing hooks at build time)
RUN --mount=type=cache,target=/root/.cache/pip \
    if [ "$INSTALL_PRECOMMIT_HOOKS" = "true" ]; then \
        set -eux; \
        echo "Installing Python and pre-commit..." && \
        apt-get update && \
        apt-get install -y --no-install-recommends python3-pip && \
        apt-get clean && \
        rm -rf /var/lib/apt/lists/* && \
        pip3 install --no-cache-dir --upgrade pip && \
        pip3 install --no-cache-dir pre-commit && \
        pip cache purge || true; \
    fi

# Install Node.js + cspell (optional)
RUN --mount=type=cache,target=/home/${USERNAME}/.npm \
    if [ "${INSTALL_CSPELL}" = "true" ]; then \
        set -eux; \
        curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
        apt-get install -y --no-install-recommends nodejs && \
        npm config set cache /home/${USERNAME}/.npm && \
        npm install -g cspell && \
        npm cache clean --force && \
        apt-get clean && \
        rm -rf /var/lib/apt/lists/*; \
    fi

# Install Graphviz for Quarto (dot diagrams support)
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    if [ "${INSTALL_GRAPHVIZ}" = "true" ]; then \
        set -eux; \
        apt-get update \
        && apt-get install -y --no-install-recommends \
            libcairo2 \
            libpango-1.0-0 \
            graphviz \
        && apt-get clean \
        && rm -rf /var/lib/apt/lists/*; \
    fi

# ---------- Stage 3: Finalization ----------
FROM extras AS final

ARG USERNAME
ARG USER_UID
ARG USER_GID
ARG INSTALL_CHROMIUM

# ---------- User and permissions --------------------------------------------
RUN set -eux && \
    USER_UID=${USER_UID:-1000} && \
    USER_GID=${USER_GID:-1000} && \
    groupadd -g "${USER_GID}" "${USERNAME}" && \
    useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash "${USERNAME}" && \
    chown -R "${USERNAME}":"${USERNAME}" "/home/${USERNAME}"

# Create .ssh directory and set permissions for the non-root user
RUN set -eux && \
    mkdir -p /home/${USERNAME}/.ssh && \
    chown -R "${USERNAME}":"${USERNAME}" "/home/${USERNAME}/.ssh" && \
    chmod 700 /home/${USERNAME}/.ssh

# Switch to non-root user for Chromium and Quarto tools
USER ${USERNAME}

# Install Quarto Chromium tool (optional)
RUN --mount=type=cache,target=/home/${USERNAME}/.quarto/tools \
    if [ "${INSTALL_CHROMIUM}" = "true" ]; then \
        set -eux; \
        echo "Installing Quarto's Chromium tool..." && \
        quarto install tool chromium; \
    fi

# ---------- Shell and history setup -----------------------------------------
# Configure bash to use this file
RUN echo "export HISTFILE=/home/${USERNAME}/.bash_history" >> "/home/${USERNAME}/.bashrc" && \
    echo 'export HISTSIZE=10000' >> "/home/${USERNAME}/.bashrc" && \
    echo 'export HISTFILESIZE=10000' >> "/home/${USERNAME}/.bashrc" && \
    echo 'shopt -s histappend' >> "/home/${USERNAME}/.bashrc" && \
    echo "alias render_graphviz='find . -name \"flow.dot\" -exec sh -c '\''for f; do dir=\$(dirname \"\$f\"); dot -Tpng \"\$f\" -o \"\$dir/flow.png\"; done'\'' sh {} +'" >> "/home/${USERNAME}/.bashrc"

# Set up the shell prompt
RUN set -eux && \
    echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> "/home/${USERNAME}/.bashrc"

# Welcome message and tips
RUN set -eux && \
    cat <<EOF >> "/home/${USERNAME}/.bashrc"
echo -e "\n📘 Welcome to the Quarto Docs Dev Container!"
echo -e ""
echo -e "Below are some useful commands:"
echo -e ""
echo -e "  \e[1mQuarto Commands\e[0m"
echo -e "  ───────────────"
echo -e "  ✨ \e[32mquarto preview .\e[0m                             Start a hot-reloading preview in your browser."
echo -e "  ✨ \e[32mquarto render .\e[0m                              Build the documentation (based on _quarto.yml)."
echo -e "  ✨ \e[32mquarto render . --profile docx --to docx\e[0m     Build a .docx file (requires _quarto-docx.yml)."
EOF

# Append conditional commands depending on optional tools
RUN set -eux && \
    if [ "${INSTALL_CSPELL}" = "true" ] || [ "${INSTALL_PRECOMMIT_HOOKS}" = "true" ]; then \
        echo -e 'echo -e ""' >> "/home/${USERNAME}/.bashrc"; \
        echo -e 'echo -e "  \e[1mData quality checks\e[0m"' >> "/home/${USERNAME}/.bashrc"; \
        echo -e 'echo -e "  ───────────────────"' >> "/home/${USERNAME}/.bashrc"; \
    fi && \
    if [ "${INSTALL_CSPELL}" = "true" ]; then \
        echo 'echo -e "  ✍️  \e[32mcspell lint . --config .vscode/cspell.json\e[0m  Run spell checks.\n"' >> "/home/${USERNAME}/.bashrc"; \
    fi && \
    if [ "${INSTALL_PRECOMMIT_HOOKS}" = "true" ]; then \
        echo 'echo -e "  ⚙️  \e[32mpre-commit run --all-files\e[0m                  Run code quality checks on all files.\n"' >> "/home/${USERNAME}/.bashrc"; \
    fi

# Tools
RUN set -eux && \
    if [ "${INSTALL_GRAPHVIZ}" = "true" ]; then \
        echo 'echo -e ""' >> "/home/${USERNAME}/.bashrc"; \
        echo 'echo -e "  \e[1mTools\e[0m"' >> "/home/${USERNAME}/.bashrc"; \
        echo 'echo -e "  ─────"' >> "/home/${USERNAME}/.bashrc"; \
        echo 'echo -e "  📊 \e[32mrender_graphviz\e[0m                              Scan your documentation, process .dot files and generate .png ones.\n"' >> "/home/${USERNAME}/.bashrc"; \
    fi
```

**.devcontainer/devcontainer.json**

```json
{
    "name": "Quarto Docs Project",
    "dockerComposeFile": "compose.yaml",
    "service": "quarto-doc-base",
    "workspaceFolder": "/workspace",
    "remoteUser": "vscode",
    "containerEnv": {
        "GIT_SSH_COMMAND": "ssh -i /home/vscode/.ssh/id_ed25519 -o 'UserKnownHostsFile=/dev/null'"
    },
    "initializeCommand": "mkdir -p .devcontainer/history && touch .devcontainer/history/.bash_history && echo '.bash_history' > .devcontainer/history/.gitignore",
    "postAttachCommand": "if which pre-commit >/dev/null 2>&1; then pre-commit install || true; fi",
    "forwardPorts": [
        8000
    ],
    "customizations": {
        "vscode": {
            "settings": {
                "[dockerfile]": {
                    "files.eol": "\n",
                    "editor.defaultFormatter": "ms-azuretools.vscode-docker"
                },
                "[json]": {
                    "editor.defaultFormatter": "vscode.json-language-features"
                },
                "[jsonc]": {
                    "editor.defaultFormatter": "vscode.json-language-features",
                    "editor.wordWrap": "wordWrapColumn",
                    "editor.wordWrapColumn": 80,
                    "editor.wrappingIndent": "indent"
                },
                "[markdown]": {
                    "editor.defaultFormatter": "yzhang.markdown-all-in-one",
                    "editor.wordWrap": "wordWrapColumn",
                    "editor.wordWrapColumn": 80,
                    "editor.wrappingIndent": "indent"
                },
                "[yaml]": {
                    "editor.defaultFormatter": "redhat.vscode-yaml"
                },
                "cSpell.language": "en,fr,nl",
                "cSpell.words": [
                    "Quarto",
                    "devcontainer",
                    "frontmatter",
                    "Pandoc",
                    "Gitlens"
                ],
                "editor.formatOnSave": true,
                "editor.guides.bracketPairs": "active",
                "editor.guides.bracketPairsHorizontal": "active",
                "editor.guides.highlightActiveIndentation": true,
                "editor.guides.indentation": true,
                "editor.multiCursorModifier": "ctrlCmd",
                "editor.renderWhitespace": "all",
                "editor.rulers": [
                    120,
                    70
                ],
                "editor.stickyScroll.enabled": true,
                "editor.tabCompletion": "on",
                "editor.tabSize": 4,
                "editor.wordBasedSuggestionsMode": "allDocuments",
                "editor.wordWrapColumn": 120,
                "explorer.compactFolders": false,
                "explorer.confirmDelete": true,
                "explorer.confirmDragAndDrop": true,
                "extensions.autoCheckUpdates": false,
                "files.associations": {
                    "resources/*.json": "jsonc"
                },
                "files.autoSave": "onFocusChange",
                "files.defaultLanguage": "${activeEditorLanguage}",
                "files.eol": "\n",
                "files.exclude": {
                    "**/.cache": true,
                    "**/.git": true,
                    "**/.quarto": true,
                    "**/*.html": true,
                    "**/readme_files/**": true,
                    "**/site_libs/**": true
                },
                "files.insertFinalNewline": true,
                "files.trimTrailingWhitespace": true,
                "maptz.regionfolder": {
                    "[dockerfile]": {
                        "foldEnd": "[\\S]*\\#[\\s]*endregion",
                        "foldEndRegex": "[\\S]*\\#[\\s]*endregion[\\s]*(.*)$",
                        "foldStart": "[\\S]*\\#[\\s]*region [NAME]",
                        "foldStartRegex": "[\\S]*\\#[\\s]*region[\\s]*(.*)$"
                    }
                },
                "markdown.extension.toc.levels": "2..6",
                "markdown.extension.tableFormatter.enabled": true,
                "markdownlint.config": {
                    "MD033": false,
                    "MD036": false
                },
                "quarto.preview.renderOnSave": true,
                "telemetry.telemetryLevel": "off",
                "terminal.integrated.profiles.linux": {
                    "bash": {
                        "path": "/bin/bash",
                        "icon": "terminal-bash"
                    }
                },
                "terminal.integrated.defaultProfile.linux": "bash",
                "terminal.integrated.fontFamily": "MesloLGS NF",
                "terminal.integrated.tabs.defaultLocation": "editorGroup",
                "yaml.schemas": {
                    "https://json.schemastore.org/json-schema-v7.json": [
                        "_quarto.yml",
                        "_quarto-*.yml"
                    ],
                    "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
                },
                "window.title": "${dirty}${rootName} ${separator} [DevContainer]",
                "workbench.colorTheme": "Visual Studio Light"
            },
            "extensions": [
                "DavidAnson.vscode-markdownlint",
                "esbenp.prettier-vscode",
                "gitkraken.gitlens",
                "maptz.regionfolder",
                "mde.select-highlight-minimap",
                "mikestead.dotenv",
                "ms-vscode.makefile-tools",
                "quarto.quarto",
                "redhat.vscode-yaml",
                "sirtori.indenticator",
                "streetsidesoftware.code-spell-checker-dutch",
                "streetsidesoftware.code-spell-checker-french",
                "streetsidesoftware.code-spell-checker",
                "yzhang.markdown-all-in-one"
            ]
        }
    }
}
```

> **Information:**
>
> Strictly speaking, we don't need the `.devcontainer/compose.yaml` file but this is the only way to build the Docker image **once** and reuse it across projects.
>
> By opening a Devcontainer, even if the `Dockerfile` is strictly the same across your documentation projects, the context will be different (**project1**, **project2**, ...) and VSCode will rebuild the image for that context. To avoid this, we need to build the image and **give it a name**; this can only be done using a `.devcontainer/compose.yaml` file.

## Extra Information (optional — skip if the defaults work for you)

### Installation of sudo

During the creation of the Docker image, we also install the `sudo` command and allow our `vscode` user to run `sudo su root` in the devcontainer without having to fill in a password. The reason here is that, sometimes, Quarto will complain about a missing dependency (like when we are exporting to PDF and there is a missing library). To allow the developer to quickly add the dependency and do some tests without having to rebuild the container again and again, `sudo` is installed.

## Build Arguments (optional — read only the ones you need)

### Installation of Chromium

If you pay attention to `.devcontainer/compose.yaml` file, you'll see an argument called `INSTALL_CHROMIUM`. Depending on your project, you'll need it or not.

Indeed, under certain circumstances, when rendering your documentation to Word (i.e. by running f.i. `quarto render . --profile docx --to docx`), Quarto can ask you to install Chromium. To avoid doing this every time, simply open the `.devcontainer/devcontainer.json` file, search for `INSTALL_CHROMIUM` and initialize it to `true`.

Note: if you change the `.devcontainer/Dockerfile` code or the `.devcontainer/devcontainer.json` file, you'll need to rebuild the container as explained here below.

> **Make sure you need it:**
>
> Before installing Chromium, make sure you need it i.e. first render your documentation without it and see if Quarto complains about Chromium. This is because Chromium requires a lot of dependencies and it will make your Docker image size much bigger.

### Installation of Code Spell Checker

The same way, you have a variable called `INSTALL_CSPELL`. Initialize it to `true` if you want to install the Code-spell check tool (requires Node.js).

### Installation of GraphViz

The same way, you have a variable called `INSTALL_GRAPHVIZ`. Initialize it to `true` if you want to install the GraphViz converter.

This is needed when, in your `.qmd` file, you have a graph like this one:

```markdown
digraph flow {
  rankdir = LR;

  A [shape=box, label="Source"];
  B [shape=ellipse, label="Processor"];
  C [shape=box, label="Destination"];

  A -> B;
  B -> C;
}
```

> **GraphViz:**
>
> If you have a Graphviz graph and didn't install GraphViz, you'll get an error like this: `ERROR: AssertionError: Error occurred during cleanup: TypeError: Child process has already terminated`.

### Installation of pre-commit-hooks

The third variable is `INSTALL_PRECOMMIT_HOOKS` and, if your documentation has its own `.git` folder, it'll be a good idea to initialize the variable to `true` so, when committing your changes, a few data quality controls / formatting tools will be applied.

## Opening our project as a Devcontainer

Right now, just press `F1` (or `CTRL`+`SHIFT`+`P`) and select **Dev Containers: Rebuild and Reopen in Container**. If you don't have that command, please make sure to install the `ms-vscode-remote.remote-containers` extension from Microsoft.

> **The remote container of Microsoft:**
>
> You can install the extension from the console by running `code --install-extension ms-vscode-remote.remote-containers` or by opening this page: [Marketplace - Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).

So, once you've fired the **Dev Containers: Rebuild and Reopen in Container** command, VSCode will close your current session and open a new one. The very first time, VSCode will have to make some initializations (like building the Docker image and downloading extensions so it'll be slower).

After a few seconds, you'll get a screen like this:

![The Devcontainer has been opened](./images/devcontainer.webp)

1. See bottom left, you have, in the status bar, the text "Dev Container: xxx" (followed by the name of our container as configured in the `devcontainer.json` file).
2. In the **Terminal window** (if you don't see it press `CTRL`+`ù`), you'll see a cheatsheet with main commands like `quarto preview .` to run the site with hot reload.

So, still in the Terminal, please type `quarto preview .` then press `Enter` — you'll get the same output already teased at the top of this article.

In theory, your browser will be automatically fired and the site opened, showing the same result already shown earlier. If not, just follow the link by pressing `ALT` and clicking on the link.

Crazy easy no?

## What have we done?

At this stage, what have we done?  We retrieved a Quarto website project and made sure to “dockerize” it.

Let's see the `_quarto.yml` file of the project we've just cloned (you'll find more information on the official Quarto site: [Creating a website](https://quarto.org/docs/websites/)):

**\_quarto.yml**

```yaml
project:
  type: website

website:
  title: "Simple Brand Example"
  navbar:
    title: "Demo CO"
    tools:
      - icon: github
        href: https://github.com/quarto-dev/quarto-examples/tree/main/brand/brand-simple
```

We've added three files to the project in a special folder called `.devcontainer`. That folder will tell VSCode that we would like to use a Docker container when working on that project. The container will be based on a custom Docker image (as coded in `.devcontainer/Dockerfile`). The image will reuse the official Quarto image.

So, without having to install Quarto on our computer, we'll be able to use all features of Quarto.

Finally, we have to *switch to the devcontainer* i.e. just after we've opened VSCode, we have to run a special command to reopen the project as a devcontainer.

That command is, the first time, **Dev Containers: Rebuild and Reopen in Container** but as soon as the custom image has been created once, we can simply use **Dev Containers: Reopen in Container**.

## Writing our documentation

So far, we've already fired `quarto preview .` in a terminal. The documentation was rendered and our website is already displayed (on `http://localhost:7519` or any other port).

Let's check the hot reload feature: please edit the `index.qmd` file you'll retrieve in the root folder and change something.  For instance, change the word `Overview` to `Preface` and save.

![The synchronization is already enabled](./images/synchronization-enabled.webp)

As you can see on the image here above, I just need to make a change, save my file and wait less than one second and the website is updated automatically. Nothing to do (I don't need to refresh the page). Easy no?

## Using the terminal

Go to the **Terminal window** (if you don't see it press `CTRL`+`ù`).

Look now at the right part: there is a `Configuring...` terminal but too a `bash workspace` (the name of our project). If you don't see that one, look just above `Configuring...` and click on the `+` button.

![The terminal](./images/terminal.webp)

As you can see, you'll see a few *instructions*. These instructions were coded in the final part of the `.devcontainer/Dockerfile` file. They are there just to help the first time user.

So, if your objective is to render the final documentation, you can always see *Oh yes, I simply need to run `quarto render .`*

## What is pre-commit?

In this project, I've planned the installation of [pre-commit](https://pre-commit.com/). This is an optional tool that will run some validation controls on your project before you push it to your versioning system (like GitHub or GitLab). *I've dedicated a full article to it: [Git - pre-commit-hooks](/blog/git-precommit).*

By adding `pre-commit` in the devcontainer, you'll get the privilege to never again commit files with some fault like f.i. formatting issues in your Markdown content.

If you like this idea, please create the `.pre-commit-config.yaml` file in your project's root folder (so not in the `.devcontainer` folder but its parent folder).

Just copy/paste the content below:

**.pre-commit-config.yaml**

```yaml
# Pre-commit configuration for Quarto projects
exclude: "^.build/|^.git/|^.quarto/"

repos:
  # --- Universal hooks for clean files ---
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-added-large-files
      - id: check-case-conflict
      - id: check-json
      - id: check-merge-conflict
      - id: check-yaml
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: trailing-whitespace

  # Markdown linter
  - repo: https://github.com/igorshubovych/markdownlint-cli
    rev: v0.45.0
    hooks:
      - id: markdownlint
        args:
          - "--config=.config/.markdownlint.yaml"

  # Checks all of the hyperlinks in a markdown text to determine if they are alive or dead
  - repo: https://github.com/tcort/markdown-link-check
    rev: v3.13.7
    hooks:
      - id: markdown-link-check
        name: Check Markdown Links
        entry: markdown-link-check
        language: node
        types: [markdown]

  # The hook below hook extracts the YAML front matter from each .qmd
  # file and validates its syntax using yq.
  # It helps catch formatting errors (e.g. missing colons, invalid keys)
  # before committing.
  # Only the YAML block between the first pair of --- lines is checked.
  - repo: local
    hooks:
      - id: validate-qmd-yaml
        name: Validate YAML front matter in .qmd
        entry: bash -c 'awk "/^---/{f=!f;next}f" "$1" | yq eval "." - > /dev/null'
        language: system
        pass_filenames: true
        args: ["{}"]
        files: \.qmd$

  # --- Spelling ---
  # Requires to install cspell in the Docker file as a binary. This implies to install NPM tool
  # The .vscode/cspell.json file is shared with VSCode
  - repo: local
    hooks:
      - id: cspell
        name: Spell check with cspell
        entry: cspell
        language: system
        args:
          - "--config=.vscode/cspell.json"
          - "--no-progress"
          - "--no-summary"
        exclude: ^.*_extensions/.*
```

Once this file is in place, when you fire the `git commit` command from the terminal (`CTRL`+`ù`), `git` will first run `pre-commit` to run *hooks*. In the `.pre-commit-config.yaml` file above, we've defined two hooks: the first one to make generic checks on files, then a second one to ensure there are no linting errors with `.md` files (just check the official repo for more info).

These hooks are fired every time you commit files (and just on the committed files; not the entire project).

You can also force hooks to run without committing by running `pre-commit run --all-files` in the console.

This command will check all files.

`pre-commit` is a valuable tool for ensuring compliance with established industry quality standards.

## Conclusion

With just three files (`.devcontainer/compose.yaml`, `.devcontainer/Dockerfile` and `.devcontainer/devcontainer.json`), we've converted a simple project to a Devcontainer where Quarto is preinstalled, extensions installed and configured and some features like the automatic preview is running.

Cool no?
