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

This section describes how to install and run Podman services using systemd Quadlet, enabling containers to restart automatically. It allows the service to run in its own isolated rootless environment with a dedicated Linux user.

πŸ“‹ Requirements

Info

MkDocs requires the installation of


βš™οΈ Configuration

πŸ“‹ Service Setup

Define Service Variables

# define the service name
SERVICE_NAME=mkdocs
Initialize Service Environment
# built-in bash safety and SERVICE_NAME validation
set -euo pipefail; [ -n "${SERVICE_NAME:-}" ] || { echo "SERVICE_NAME is empty"; exit 1; }

# automatically generate variables
SERVICE_HOME="/media/ssd/podman-users/${SERVICE_NAME}"
SERVICE_DIR="/media/ssd/podman/${SERVICE_NAME}"
SERVICE_USER="${SERVICE_NAME}_svc"
SERVICE_ADMIN="${SERVICE_NAME}_admins"

πŸ” Service Isolation

Configure Rootless Podman UID/GID Namespace Mappings
# allocate rootless Podman UID/GID mappings if missing
SUBID_SIZE=65536
allocate_subids_if_missing() {
  local user="$1"
  local start
  if ! grep -q "^${user}:" /etc/subuid; then
    start="$(awk -F: 'BEGIN { max = 100000 } { end = $2 + $3; if (end > max) max = end } END { print max }' /etc/subuid)"
    sudo usermod --add-subuids "${start}-$((start + SUBID_SIZE - 1))" "$user"
  fi
  if ! grep -q "^${user}:" /etc/subgid; then
    start="$(awk -F: 'BEGIN { max = 100000 } { end = $2 + $3; if (end > max) max = end } END { print max }' /etc/subgid)"
    sudo usermod --add-subgids "${start}-$((start + SUBID_SIZE - 1))" "$user"
  fi
}
allocate_subids_if_missing "${SERVICE_USER}"

# verify rootless Podman UID/GID namespace mappings exist
grep -q "^${SERVICE_USER}:" /etc/subuid || { echo "ERROR: missing subuid mappings for ${SERVICE_USER}"; exit 1; }
grep -q "^${SERVICE_USER}:" /etc/subgid || { echo "ERROR: missing subgid mappings for ${SERVICE_USER}"; exit 1; }

# validate that all subordinate ID ranges in a file are non-overlapping
validate_subid_ranges() {
  local file="$1"
  awk -F: '
    {
      start = $2
      end   = $2 + $3 - 1
      for (i = 1; i <= count; i++) {
        if (start <= ends[i] && end >= starts[i]) {
          printf "ERROR: overlapping ranges in %s\n", FILENAME
          printf "  %s:%s:%s overlaps %s:%s:%s\n",
            $1, $2, $3,
            names[i], starts[i], sizes[i]
          exit 1
        }
      }
      count++
      names[count]  = $1
      starts[count] = start
      ends[count]   = end
      sizes[count]  = $3
    }
  ' "$file"
}
validate_subid_ranges /etc/subuid
validate_subid_ranges /etc/subgid

# configure the rootless storage driver BEFORE Podman first initializes its storage.
# fuse-overlayfs is a reliable rootless overlay backend and avoids kernel-version
# quirks of native rootless overlay on ARM/Armbian. It is optional with the default
# userns used here, but recommended; set it before the first storage init so no
# 'podman system reset' is needed later.
sudo -u "${SERVICE_USER}" mkdir -p "${SERVICE_HOME}/.config/containers"
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/storage.conf" >/dev/null <<'EOF'
[storage]
driver = "overlay"

[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF

# rebuild rootless Podman state to ensure existing containers/storage use current namespace mappings
# use a login shell (-i) so HOME points at the service user's home
# (storage now initializes with fuse-overlayfs; no 'podman system reset' needed)
sudo -iu "${SERVICE_USER}" podman system migrate
Configure Secure Service Directory Permissions
# Apply the admin/service ACL policy to one directory tree, deterministically.
#   $1 = tree to treat
#   $2 = (optional) a sub-path to skip entirely (e.g. a container data subtree)
#
# Why setfacl, not chmod: on an ACL'd tree, chmod edits only the mask, never group::,
# so stale execute bits survive. setfacl sets every entry explicitly (mask included),
# lowercase β€” the result is reproducible and no execute bit can resurface.
apply_acl_tree() {
    local tree="$1"
    local skip="${2:-}"
    local prune=()
    [ -n "$skip" ] && prune=( -path "$skip" -prune -o )                                         # skip this subtree if a 2nd arg is given

    sudo find "$tree" "${prune[@]}" -exec chown "${SERVICE_USER}:${SERVICE_ADMIN}" {} +         # service user owns; admin group is the owning group

    # directories β€” access ACL (perms on the existing dirs)
    sudo find "$tree" "${prune[@]}" -type d -exec setfacl    -m g:${SERVICE_ADMIN}:rwx {} +     # admin group: access existing dirs
    sudo find "$tree" "${prune[@]}" -type d -exec setfacl    -m u:${SERVICE_USER}:rwx {} +      # service user: access existing dirs
    sudo find "$tree" "${prune[@]}" -type d -exec setfacl    -m u::rwx,g::rwx,o::-,m::rwx {} +  # owner/group rwx, deny others, pin mask rwx

    # directories β€” default ACL (inherited by NEW files/dirs created later)
    sudo find "$tree" "${prune[@]}" -type d -exec setfacl -d -m g:${SERVICE_ADMIN}:rwx {} +     # admin group: inherit access on new items
    sudo find "$tree" "${prune[@]}" -type d -exec setfacl -d -m u:${SERVICE_USER}:rwx {} +      # service user: inherit access on new items
    sudo find "$tree" "${prune[@]}" -type d -exec setfacl -d -m u::rwx,g::rwx,o::- {} +         # default owner/group/other for new items

    # files β€” access ACL (rw only, never executable)
    sudo find "$tree" "${prune[@]}" -type f -exec setfacl    -m g:${SERVICE_ADMIN}:rw {} +      # admin group: access existing files
    sudo find "$tree" "${prune[@]}" -type f -exec setfacl    -m u:${SERVICE_USER}:rw {} +       # service user: access existing files
    sudo find "$tree" "${prune[@]}" -type f -exec setfacl    -m u::rw,g::rw,o::-,m::rw {} +     # owner/group rw, deny others, pin mask rw

    sudo find "$tree" "${prune[@]}" -type d -exec chmod g+s {} +                                # setgid: new items inherit the group (a MODE bit; ACLs can't set it)
}

# --- SERVICE_HOME: Podman's private runtime home ------------------------------
# Own the home + set setgid, then ACL ONLY the .config subtree (containers.conf,
# storage.conf, and the *.network / *.container quadlets you hand-edit) so admins
# can edit them sudo-less.
#
# Leave ~/.local (storage) and ~/.cache to Podman: ACLs on the overlay store break
# the runtime ("OCI permission denied" on the `merged` mount). Admin reaches them via sudo.
sudo chown "${SERVICE_USER}:${SERVICE_ADMIN}" "${SERVICE_HOME}"                       # service user owns; admin group is the owning group
sudo chmod u=rwx,g=rwx,o=,g+s "${SERVICE_HOME}"                                       # owner/admin access; deny others; inherit group on new items
apply_acl_tree "${SERVICE_HOME}/.config"                                              # .config subtree: full sudo-less admin ACL treatment

# --- bind-mount data dirs: created + owned BEFORE the SERVICE_DIR ACL pass ---------
# Each container writes as a mapped subuid, so its data dir must be owned by that subuid
# (chown as root; only root crosses the id range). We create + own them FIRST so they
# neither inherit SERVICE_DIR's default ACL (inheritance happens at creation) nor get
# walked by apply_acl_tree (which prunes data/).
#
# No ACLs here: Postgres refuses a group-accessible PGDATA. Admin reaches them via sudo;
# back Postgres up with pg_dumpall, not a raw file copy.
SUBUID_BASE="$(awk -F: -v u="${SERVICE_USER}" '$1==u {print $2}' /etc/subuid)"
SUBGID_BASE="$(awk -F: -v u="${SERVICE_USER}" '$1==u {print $2}' /etc/subgid)"

# data subdir : in-container uid/gid it must be owned by
data_dirs=(
    "mkdocs:1000"        # /docs      β€” mkdocs-server (pinned User=1000:1000)
)

mkdir -p "${SERVICE_DIR}/data"                                                        # the data/ parent: plain, no ACL
sudo chown "${SERVICE_USER}:${SERVICE_ADMIN}" "${SERVICE_DIR}/data"                   # owner/admin
sudo chmod 0750 "${SERVICE_DIR}/data"                                                 # children are set individually below
for entry in "${data_dirs[@]}"; do
    sub="${entry%%:*}"                                  # subdir name
    ids="${entry#*:}"                                   # "uid" or "uid:gid"
    uid="${ids%%:*}"                                    # uid
    gid="${ids#*:}"; [ "$gid" = "$ids" ] && gid="$uid"  # gid (defaults to uid)
    dir="${SERVICE_DIR}/data/${sub}"
    sudo mkdir -p "$dir"
    sudo chown -R "$((SUBUID_BASE + uid - 1)):$((SUBGID_BASE + gid - 1))" "$dir"   # map both to host subids
    sudo chmod 0700 "$dir"
done

# --- SERVICE_DIR: application data, env files, configuration -------------------
# Full treatment so the admin group gets sudo-less access AND the service user keeps
# access even on admin-created files. data/ is EXCLUDED: those dirs are owned by the
# containers' subuids and need engine-specific modes (Postgres rejects a group/other-
# accessible PGDATA), so they are handled separately just below.
apply_acl_tree "${SERVICE_DIR}" "${SERVICE_DIR}/data"                                 # treat everything EXCEPT data/
Configure Secure NGINX Static Assets Permissions
# Grant the NGINX worker user (www-data) read-only access to assets subtrees.
#   $1     = service directory β†’ traverse-only (x)
#   $2..$n = assets subtrees served directly by NGINX (configs, icons, favicon, manifest, ...)
apply_acl_nginx() {
    local tree="$1"; shift
    local assets

    sudo setfacl -m u:www-data:x "$tree"                                        # traverse only: pass through, no listing, no reading

    for assets in "$@"; do
        # assets directories β€” enter + list, existing and future
        sudo find "$assets" -type d -exec setfacl    -m u:www-data:rx {} +      # www-data: read existing dirs
        sudo find "$assets" -type d -exec setfacl -d -m u:www-data:rx {} +      # www-data: inherit read on new items

        # assets files β€” www-data read-only, owner/admin rw, never executable.
        # Entries AND mask are pinned explicitly: a bare `setfacl -m u:www-data:r`
        # would recalculate the mask to the union of all entries, resurfacing the
        # latent rwx inherited from the default ACL (files would show group rwx).
        sudo find "$assets" -type f -exec setfacl \
          -m u:www-data:r,u:${SERVICE_USER}:rw,g:${SERVICE_ADMIN}:rw,u::rw,g::rw,o::-,m::rw {} +
    done
}

# --- SERVICE_DIR/nginx + SERVICE_DIR/icons: served directly by NGINX -----------
# Created AFTER the apply_acl_tree pass so they inherit the default ACL + setgid
# group (service user/admin keep full access); www-data is then layered on top
# as a read-only named entry.
sudo mkdir -p "${SERVICE_DIR}/nginx" "${SERVICE_DIR}/icons"
sudo chown "${SERVICE_USER}:${SERVICE_ADMIN}" "${SERVICE_DIR}/nginx" "${SERVICE_DIR}/icons"
apply_acl_nginx "${SERVICE_DIR}" "${SERVICE_DIR}/nginx" "${SERVICE_DIR}/icons"

πŸ›‘οΈ Rootless Podman Defaults

Configure rootless Podman per-user configuration directory
# configure rootless Podman defaults for this service user
# - store per-user Podman configuration under ~/.config/containers
sudo -u "${SERVICE_USER}" mkdir -p "${SERVICE_HOME}/.config/containers/systemd"
Configure rootless Podman defaults
# configure rootless Podman defaults for this service user
# - use k8s-file logging for easier log rotation and inspection
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/containers.conf" >/dev/null <<EOF
[containers]
log_driver = "k8s-file"

[engine]
healthcheck_events=false
EOF

πŸ”§ Service Configuration

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
  • DOCS_REPO: path to the local git repository
  • DOCS_BRANCH: git branch (master/main)
###################################################################################
# NGINX Proxy Configuration
###################################################################################
HOST_PORT=10000
###################################################################################
# MkDocs Configuration
###################################################################################
DOCS_REPO=/path/to/git/repository.git
DOCS_BRANCH=master
Create Environment files
sudo -u "${SERVICE_USER}" tee "${SERVICE_DIR}/mkdocs.env" >/dev/null <<EOF
###################################################################################
# NGINX Proxy Configuration
###################################################################################
HOST_PORT=${HOST_PORT}

###################################################################################
# MkDocs Configuration
###################################################################################
DOCS_REPO=${DOCS_REPO}
DOCS_BRANCH=${DOCS_BRANCH}
EOF

Keep the .env files

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

πŸ” MkDocs Repo Access

Grant the container read access to the bare docs repository

The bare repo lives under /media/ssd/git, which denies all access to "others", so the mkdocs container cannot rely on world bits to read it. Two identities need explicit minimal access:

  • the container process reads the repo files as a subuid (container User=1000 β†’ host uid SUBUID_BASE + 1000 - 1)
  • podman/crun, running as the service user, must statfs the bind-mount source path before the container starts
# host subuid the container's User=1000 maps to
SUBUID_BASE="$(awk -F: -v u="${SERVICE_USER}" '$1==u {print $2}' /etc/subuid)"
DOCS_UID=$((SUBUID_BASE + 1000 - 1))

# traverse-only (x) on the path chain up to (not including) /media/ssd:
# both identities can pass through, neither can list or read other repos
d="${DOCS_REPO}"
while [ "$d" != "/media/ssd" ] && [ "$d" != "/" ]; do
    sudo setfacl -m "u:${SERVICE_USER}:x,u:${DOCS_UID}:x" "$d"
    d="$(dirname "$d")"
done

# repo content: subuid read-only, existing + future (pushes create new
# objects as www-data; the default ACL keeps them readable by the container)
sudo setfacl -R -m "u:${DOCS_UID}:rX" "${DOCS_REPO}"
sudo find "${DOCS_REPO}" -type d -exec setfacl -d -m "u:${DOCS_UID}:rX" {} +

The container can read only ${DOCS_REPO}: it cannot even list /media/ssd/git, and the other repositories stay invisible to it.

βš™οΈ Create MkDocs build

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
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
  • βš™οΈ 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
# One-shot: clone-or-update the docs repo, rebuild only when HEAD changed, then serve.
# Any failure aborts (set -e) -> systemd Restart=always retries after RestartSec.
set -eu

#############################################
# Configuration
#############################################
REPO_SRC=/repo               # read-only bare repo (bind-mounted at /repo)
REPO_DIR=/docs/repo          # working clone (on the persistent /docs bind mount)
HASH_FILE=/docs/.last_build  # last successfully-built short HEAD
BRANCH="${DOCS_BRANCH:-master}"
PORT="${MKDOCS_PORT:-8000}"

#############################################
# Update: clone on first run, fetch + hard reset afterwards
#############################################
# rootless: the :ro source repo shows up as a non-matching uid inside the container.
# git only honors safe.directory from system/global config (never from -c or env, by
# design), so write it to a global config. HOME=/tmp is always writable and ephemeral,
# so this stays out of the data volume and never accumulates duplicate entries.
export HOME=/tmp
git config --global --add safe.directory "$REPO_SRC"

if [ -d "$REPO_DIR/.git" ]; then
  echo "=== updating docs repo ==="
  # fetch + hard reset instead of pull: converges on the remote state even
  # after a force-push (the clone is never edited locally, nothing to lose)
  git -C "$REPO_DIR" fetch origin "$BRANCH"
  git -C "$REPO_DIR" reset --hard "origin/$BRANCH"
else
  echo "=== cloning from $REPO_SRC ==="
  git clone --branch "$BRANCH" "$REPO_SRC" "$REPO_DIR"
fi

cd "$REPO_DIR"

#############################################
# Build only when HEAD changed
#############################################
current="$(git rev-parse --short HEAD)"
previous="$(cat "$HASH_FILE" 2>/dev/null || true)"
if [ "$current" != "$previous" ]; then
  echo "=== building site ($previous -> $current) ==="
  rm -rf site
  mkdocs build --no-directory-urls
  printf '%s\n' "$current" > "$HASH_FILE"   # record only after a successful build
else
  echo "=== no change ($current); skipping build ==="
fi

#############################################
# Serve (exec: python becomes the process tini supervises; logs -> podman logs)
#############################################
echo "=== serving $REPO_DIR/site on :$PORT ==="
exec python -m http.server "$PORT" -d site
Create MkDocs multi-stage build 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'
#############################################
##### mkdocs builder + python static server (single stage)
#############################################
FROM python:3.12-slim

# pip runs as root into the image's site-packages; that's fine in a container build
ENV PIP_ROOT_USER_ACTION=ignore

# runtime tools: git (clone/pull) + tini (PID 1 / signal forwarding) + curl (healthcheck)
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends git tini curl; \
    rm -rf /var/lib/apt/lists/*

# MkDocs + plugins. Pure-Python wheels, installed as a normal RUN layer: its cache
# check is just command+parent (instant), unlike a COPY --from that hashes the tree.
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt

COPY --chown=1000:1000 run.sh /usr/src/app/run.sh
RUN chmod 0755 /usr/src/app/run.sh

USER 1000:1000
WORKDIR /docs
EXPOSE 8000
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/src/app/run.sh"]
EOF

🧩 Quadlet Service

Create Network Podman Quadlet
# podman quadlet: create network
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/systemd/mkdocs.network" >/dev/null <<EOF
[Network]
NetworkName=mkdocs
EOF
Create Build Podman Quadlet
# podman quadlet: create mkdocs build
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/systemd/mkdocs.build" >/dev/null <<'EOF'
[Unit]
Description=Build mkdocs image

[Build]
ImageTag=localhost/mkdocs:latest
File=/media/ssd/podman/mkdocs/build/Dockerfile
SetWorkingDirectory=/media/ssd/podman/mkdocs/build

[Service]
# After a successful (re)build, drop now-untagged old image versions.
# Dangling-only: the tagged mkdocs:latest and its layers (incl. the pip layer) are kept.
ExecStartPost=-/usr/bin/podman image prune -f
EOF
Create MkDocs Server Podman Quadlet
# podman quadlet: create mkdocs server
sudo -u "${SERVICE_USER}" tee "${SERVICE_HOME}/.config/containers/systemd/mkdocs-server.container" >/dev/null <<'EOF'
[Unit]
Description=mkdocs server
Requires=mkdocs-build.service
After=mkdocs-build.service

[Container]
EnvironmentFile=/media/ssd/podman/mkdocs/mkdocs.env
Image=mkdocs.build
ContainerName=mkdocs-server
Network=mkdocs.network

# python:3.12-slim declares no USER (runs as root by default). We pin
# it to 1000 so it runs unprivileged and owns the /docs bind mount.
User=1000:1000

# docs source: local bare repo, read-only -> /repo inside the container
Volume=${DOCS_REPO}:/repo:ro
# persistent clone + built site (:U chowns the bind mount to the container user)
Volume=/media/ssd/podman/mkdocs/data/mkdocs:/docs:U

PublishPort=127.0.0.1:${HOST_PORT}:8000

HealthCmd=curl -fsS -o /dev/null http://127.0.0.1:8000
HealthStartPeriod=120s
HealthInterval=60s
HealthTimeout=5s
HealthRetries=3
HealthOnFailure=kill

[Service]
EnvironmentFile=/media/ssd/podman/mkdocs/mkdocs.env
UMask=0007
SuccessExitStatus=143

Restart=always
RestartSec=30
TimeoutStartSec=600

[Install]
WantedBy=default.target
EOF

UMask=0007: removes all permissions for others while preserving read/write access for the service owner and admin group on newly created files and directories SuccessExitStatus=143: treat a SIGTERM-style exit (143) as a clean stop, not a failure


▢️ Start mkdocs

Enable and Start Quadlet Services
# open an interactive shell as the service user
sudo -iu "${SERVICE_USER}"

# reload systemd user units
systemctl --user daemon-reload

# start Podman Quadlet services
systemctl --user start mkdocs-server.service

# verify service status
systemctl --user status mkdocs-server.service

To follow the service logs in real time, run sudo journalctl _UID=$(id -u ${SERVICE_USER}) -f from the Debian account.


πŸš€ 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;

  # show maintenance page when backend is down
  error_page 502 503 504 /maintenance.html;
  include snippets/error-maintenance.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.

Define Service Variables

# define the service name
SERVICE_NAME=mkdocs
Initialize Service Environment
# built-in bash safety and SERVICE_NAME validation
set -euo pipefail; [ -n "${SERVICE_NAME:-}" ] || { echo "SERVICE_NAME is empty"; exit 1; }

# automatically generate variables
SERVICE_USER="${SERVICE_NAME}_svc"
Create the Git hook script
# create the post-receive hook in the bare repository hooks directory
tee post-receive > /dev/null <<'EOF'
#!/bin/sh
echo "=== restarting container: mkdocs-server ==="
runtime="/run/user/$(id -u SERVICE_USER)"
while read -r _old _new ref; do
  [ "$ref" = "refs/heads/master" ] || continue
  sudo -u SERVICE_USER XDG_RUNTIME_DIR="$runtime" \
    /usr/bin/systemctl --user restart --no-block mkdocs-server.service
done
echo "=== done ==="
exit 0
EOF

# bake the service account into the hook (placeholder -> value)
sed -i "s/SERVICE_USER/${SERVICE_USER}/g" post-receive

# give the right permission www-data:debian, executable by owner/group only
sudo chown www-data:debian post-receive
sudo chmod 0770 post-receive
Grant www-data permission to restart the container
# rootless podman: the container is owned by the service user's *user* systemd,
# so www-data sudo's to that user (not root) and runs systemctl --user.
# SETENV lets the hook pass XDG_RUNTIME_DIR through to the command.
echo "www-data ALL=(${SERVICE_USER}) NOPASSWD:SETENV: /usr/bin/systemctl --user restart --no-block mkdocs-server.service" \
  | sudo tee /etc/sudoers.d/www-data-mkdocs

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

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