Skip to content

πŸ“˜ Installing MkDocs-Material

MkDocs

Material is a custom theme for the MkDocs static site generator that's geared towards building project documentation. Documentation source files are written in Markdown, and configured with a single YAML configuration file

Info

Both projects are open-source and can be downloaded here: https://github.com/squidfunk/mkdocs-material.


πŸ“₯ Installation

πŸ“‹ Requirements

Info

MkDocs requires the installation of


🐳 Install MkDocs

The use of Docker Compose will automate the installation of MkDocs container.

πŸ”§ Setup MkDocs Parameters

Before deploying, you need to define a few environment variables that will be used throughout the setup process.

  • HOST_PORT: external port used by NGINX to route traffic to the service
  • GIT_REPOS: path to the local git repository
# example of configuration for environment parameters
GIT_REPOS=/path/to/git-repos.git
HOST_PORT=10000
βš™οΈ Configure MkDocs for Docker Compose

MkDocs can be deployed using Docker Compose. The compose.yml file will automatically incorporate the environment variables configured in the previous step. You can copy, paste, and run all of the following commands directly in your terminal.

# create docker directory
mkdir mkdocs && cd mkdocs
mkdir build
# setup of compose.yml
tee compose.yml > /dev/null <<'EOF'
services:
  mkdocs-docs:
    build:
      context: ./build
      additional_contexts:
        - repo=${GIT_REPOS}
      args:
        - PUID=${PUID}
        - PGID=${PGID}
    image: mkdocs-docs:latest
    container_name: mkdocs-docs
    user: "${PUID}:${PGID}"
    volumes:
      - ${GIT_REPOS}:/git:ro
    ports:
      - ${HOST_PORT}:8000
    healthcheck:
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:8000/"]
      start_period: 180s
      start_interval: 5s
      interval: 60s
      timeout: 1s
      retries: 3
    restart: unless-stopped
EOF
# setup of .env file
tee .env > /dev/null <<EOF
###################################################################################
# Run as non-root user
###################################################################################
PUID=$(id -u)
PGID=$(id -g)

###################################################################################
# NGINX Proxy Configuration
###################################################################################
HOST_PORT=${HOST_PORT}

###################################################################################
# MkDocs Configuration
###################################################################################
GIT_REPOS=${GIT_REPOS}
EOF

Keep the .env file

All the secret informations will be stored in the .env file.

🐳 Create the custom MkDocs-Material docker container

To create a custom Docker container for Mkdocs-Material based on python, follow these steps:

  1. Configure the requirements.txt File
    Modify the requirements.txt file to specify the required MkDocs plugins that will be installed in the Docker container.

    # setup requirements.txt file
    tee build/requirements.txt > /dev/null <<EOF
    mkdocs-material
    mkdocs-awesome-pages-plugin
    mkdocs-minify-plugin
    mkdocs-macros-plugin
    mkdocs-mermaid2-plugin
    EOF
    
  2. Create the run.sh Script
    Develop a run.sh script that will be executed upon container startup.
    This script should perform the following actions:

    • πŸ“ Check if a Git repository needs to be cloned.
    • ⬇️ Fetch the latest commits from the Git repository.
    • 🧹 Use git pull --rebase to fetch and apply the latest commits, handling cases where the commit history has been rewritten.
    • βš™οΈ Build the MkDocs-material documentation if necessary.
    • 🌐 Launch a Python HTTP server to serve the static website created.
    # create run.sh script
    tee build/run.sh > /dev/null <<'EOF'
    #!/bin/sh
    # Update + build only when the Git HEAD changed.
    # The build hash is stored in .env ONLY if the build succeeds.
    
    set -u  # error on undefined variables
    
    #############################################
    # Configuration
    #############################################
    LOGS="/tmp/update-logs.log"
    RETRY_DELAY=60
    ENV_FILE=".env"
    BRANCH="master"
    
    #############################################
    # Helpers
    #############################################
    log_step() {
      printf "=== %s: " "$1"
    }
    
    fail_with_logs() {
      echo "failed ==="
      echo "---- logs ----"
      cat "$LOGS"
      echo "--------------"
      echo "$1"
      sleep "$RETRY_DELAY"
      exit 1
    }
    
    get_current_hash() {
      git rev-parse --short HEAD
    }
    
    get_previous_hash() {
      if [ -f "$ENV_FILE" ]; then
        grep -E '^LATEST_COMMIT=' "$ENV_FILE" 2>/dev/null | head -n1 | cut -d= -f2-
      else
        echo ""
      fi
    }
    
    store_hash() {
      new_hash="$1"
      if [ -f "$ENV_FILE" ] && grep -q '^LATEST_COMMIT=' "$ENV_FILE" 2>/dev/null; then
        sed -i "s|^LATEST_COMMIT=.*|LATEST_COMMIT=$new_hash|" "$ENV_FILE"
      else
        printf "\nLATEST_COMMIT=%s\n" "$new_hash" >> "$ENV_FILE"
      fi
    }
    
    run_cmd() {
      "$@" > "$LOGS" 2>&1
      return $?
    }
    
    #############################################
    # Main Script
    #############################################
    # Update repository
    log_step "git pull"
    run_cmd git pull origin "$BRANCH" --rebase
    if [ $? -ne 0 ]; then
      fail_with_logs "git pull failed"
    fi
    echo "succeed ==="
    echo ""
    
    # Compare hashes
    current_hash="$(get_current_hash)"
    previous_hash="$(get_previous_hash)"
    if [ "$current_hash" != "$previous_hash" ]; then
      echo "hash change -> building"
      echo ""
    
      # Build only when hash changed
      log_step "building mkdocs-material documentation"
      rm -rf ./site
      run_cmd mkdocs build --no-directory-urls
      if [ $? -ne 0 ]; then
        fail_with_logs "build failed (hash NOT updated)"
      fi
      echo "succeed ==="
      echo ""
    
      # Add apple-touch-icon, android and manifest files
      if [ -f "./site/index.html" ]; then
        log_step "adding apple/android/manifest icons files"
        sed -i '/<link rel="icon" href="assets\/images\/favicon.png">/d' "./site/index.html"
        sed -i '20i\
            <link rel="manifest" href="/app.webmanifest">\
            <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">\
            <link rel="icon" type="image/png" href="/favicon.png">\
            <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">\
        ' ./site/index.html
        echo "succeed ==="
        echo ""
      fi
    
      # Store the hash ONLY if build succeeded
      store_hash "$current_hash"
    else
      echo "no hash change -> skipping build"
      echo ""
    fi
    
    # Launch python http server
    echo "starting python http-server on port 8000..."
    run_cmd python -m http.server 8000 -d ./site
    if [ $? -ne 0 ]; then
      fail_with_logs "python http-server failure"
    fi
    EOF
    
  3. Create the Dockerfile:
    Create a Dockerfile that includes the following steps:

    • Copy the requirements.txt file into the Docker container.
    • Copy the run.sh script into the Docker container.
    # setup of Dockerfile
    tee build/Dockerfile > /dev/null <<'EOF'
    #############################################
    ##### Builder stage: install Python deps
    #############################################
    FROM python:3.12-slim AS builder
    
    # work dir for building Python dependencies
    WORKDIR /tmp
    
    # install required MkDocs plugins and extensions
    COPY requirements.txt /tmp/requirements.txt
    RUN set -eux; \
        pip install --no-cache-dir --upgrade pip; \
        pip install --no-cache-dir --prefix=/install -r /tmp/requirements.txt
    
    #############################################
    ##### Final stage: runtime image
    #############################################
    FROM python:3.12-slim
    
    # configuration variables
    ARG PUID=1000
    ARG PGID=1000
    
    # container paths
    ENV APP_DIR=/docs
    ENV RUN_SCRIPT=/usr/src/app/run.sh
    
    # install base runtime tools
    RUN set -eux; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
          git \
          curl \
          tini \
        ; \
        rm -rf /var/lib/apt/lists/*
    
    # copy installed Python packages from builder stage
    COPY --from=builder /install /usr/local
    
    # non-root user setup
    RUN set -eux; \
        groupadd --gid "${PGID}" debian; \
        useradd  --uid "${PUID}" --gid "${PGID}" --shell /bin/bash --create-home debian; \
        mkdir -p "${APP_DIR}"; \
        chown -R debian:debian "${APP_DIR}"; \
        chmod 2775 "${APP_DIR}"
    
    # runtime configuration
    USER debian
    WORKDIR ${APP_DIR}
    
    # clone directory from a local Git context
    COPY --from=repo / /git
    RUN set -eux; \
        git config --global --add safe.directory /git; \
        git clone /git .
    USER root
    RUN rm -rf /git
    USER debian
    
    # copy the runtime script that updates repos, builds docs, and starts the server
    COPY --chown=${PUID}:${PGID} run.sh ${RUN_SCRIPT}
    RUN chmod 0755 ${RUN_SCRIPT}
    
    # runtime configuration
    EXPOSE 8000
    ENTRYPOINT ["/usr/bin/tini", "--", "/usr/src/app/run.sh"]
    EOF
    
🐳 Install MkDocs with Docker Compose

Now that the compose.yml file has been generated, it's time to install all the containers.

# install and start the container
docker compose up --build -d

πŸš€ Deploy MkDocs

Install NGINX

NGINX needs to be installed, follow the NGINX section.

Configure NGINX

NGINX needs to be configured using a file in /etc/nginx/sites-enabled directory.
This configuration file specify the documentation path:

server {
  server_name mkdocs.domain.fr;

  # setup 404 error_page
  error_page 404 /404.html;
  include snippets/error-404.conf;

  # reverse proxy
  location / {
    proxy_pass http://127.0.0.1:10000;

    # keep it HTTP/1.1
    proxy_http_version 1.1;

    # forwarded headers
    include proxy_params;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Port $server_port;

    # timeouts for long-running sessions
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
  }
}
# restart nginx
sudo nginx -t && sudo service nginx restart

Replace mkdocs.domain.fr by the name of your website.

Activate HTTPS

To activate HTTPS protocol, follow theΒ Let's Encrypt section.


πŸͺ Create Git hook script

The hook runs as www-data (the nginx/fcgiwrap user). After each successful push, it restarts the mkdocs-docs container so the documentation is rebuilt automatically.

# create the post-update hook in the bare repository hooks directory
tee post-update > /dev/null <<'EOF'
#!/bin/bash
echo "=== restarting docker container: mkdocs-docs ==="
sudo /usr/bin/docker restart mkdocs-docs
echo "=== done ==="
exit 0
EOF

# make the hook executable
sudo chmod +x post-update
# create a sudoers drop-in granting www-data permission to restart only this container
echo 'www-data ALL=(root) NOPASSWD: /usr/bin/docker restart mkdocs-docs' | sudo tee /etc/sudoers.d/www-data-docker

# sudoers files must be 440 - sudo silently ignores files with wrong permissions
sudo chmod 440 /etc/sudoers.d/www-data-docker

# verify the syntax is valid before relying on it
sudo visudo -c -f /etc/sudoers.d/www-data-docker

Do not add www-data to the docker group

Adding www-data to the docker group grants unrestricted access to the Docker daemon, which is functionally equivalent to root on the host.
The narrow sudoers rule above limits the permission to restarting this one specific container only.