Introduction – Pourquoi industrialiser un wiki auto-hébergé ?

Depuis plusieurs années, j’utilisais Microsoft OneNote pour centraliser ma documentation technique personnelle : procédures d’installation, architecture de mon infra HomeLab, snippets de scripts, etc. C’était pratique… jusqu’à ce que je perde l’accès à certaines notes critiques lors d’une synchronisation ratée entre appareils.

Je travaille à la fois sur un poste HP Windows (infra, scripting, ops) et sur un MacBook Pro (dev front, IA, prototypage). Passer d’un environnement à l’autre rendait l’usage de OneNote de plus en plus instable, notamment en multi-appareil.

En parallèle, j’utilisais GitLab pour le versioning de mes projets. Bien qu’il permette d’ajouter des fichiers Markdown, je constatais vite ses limites : peu ergonomique pour des utilisateurs non développeurs, gestion des images peu fluide, recherche approximative.

C’est alors que le besoin d’un wiki auto-hébergé, accessible, structuré et intégré à mon écosystème K8s s’est imposé. Mon objectif : documenter mon infrastructure et mes outils de manière durable, versionnée, avec authentification centralisée via mon serveur LDAP local, et persistance fiable sur une base MariaDB.


Mise en œuvre – Comment j’ai déployé BookStack dans Kubernetes

Comparatif rapide de solutions

Nom Stack DB supportée Auth intégrée Avantages Limitations
BookStack PHP + Laravel MySQL/MariaDB LDAP, SAML Interface claire, très stable DB unique, pas de Markdown natif
Wiki.js Node.js PostgreSQL/MySQL OIDC, LDAP Moderne, extensible Complexité accrue à maintenir
DokuWiki PHP (flat files) Aucune Basique Ultra léger, sans DB Peu adapté aux clusters modernes

→ Choix : BookStack pour sa maturité, sa clarté d’interface et son intégration simple dans Kubernetes.

Prérequis DevOps (non couverts ici)

Avant de déployer le chart Helm présenté ci-dessous, certaines étapes d’infrastructure et de CI/CD sont nécessaires :

  • 📁 Créer un projet Git : personnellement, j’utilise GitLab auto-hébergé pour versionner mes charts Helm et mes manifests Kubernetes.
  • 🧑‍💻 Cloner et éditer le projet localement sur mes postes (Windows HP et MacBook Pro), avec VS Code + Git CLI.
  • 🤖 Configurer Jenkins pour déclencher automatiquement les déploiements :
    • pipeline Docker pour builder une image si besoin
    • publication vers Nexus (registry privé Docker + Helm)
    • script Helm (helm upgrade --install) exécuté depuis Jenkins sur mon cluster K8s
  • 🔐 Les secrets "bookstack-db-secret" sont injectés manuellement au cluster ou via kubectl.

👉 Ces étapes sont décrites dans un README.md à part, avec des scripts d’automatisation et des hooks de validation Helm.


Stack technique utilisée

  • Kubernetes 1.34 (auto-hébergé)
  • Helm Chart customisé
  • BookStack via linuxserver/bookstack
  • Base MariaDB externe (VM + NAS)
  • Authentification LDAP (OpenLDAP local)
  • Ingress avec Gateway API (NGINX Gateway Controller)
  • cert-manager pour HTTPS

Préparation de la base MariaDB externe

Avant toute chose, il est nécessaire de créer la base de données, l'utilisateur et les droits associés dans MariaDB. Sur la VM ou le serveur où MariaDB est installé, exécuter les commandes suivantes :

mysql -u root --execute="CREATE DATABASE bookstack;"
mysql -u root --execute="CREATE USER 'bookstack'@'%' IDENTIFIED WITH mysql_native_password BY 'superSecure123';"
mysql -u root --execute="GRANT ALL ON bookstack.* TO 'bookstack'@'%'; FLUSH PRIVILEGES;"

Adapter superSecure123 à la valeur que vous utiliserez dans le secret Kubernetes correspondant (DBPASS).


Déploiement Helm – values.yaml

replicaCount: 1
image:
  repository: linuxserver/bookstack
  tag: latest
  pullPolicy: Always

service:
  type: ClusterIP
  port: 80
  targetPort: 80
  protocol: TCP

envFromSecret:
  name: bookstack-db-secret

ingress:
  enabled: true
  hostname: bookstack
  gateway:
    name: nginx-gateway
    namespace: gateway

serviceAccount:
  create: false
  name: "default"

imagePullSecrets:
  - name: registry-secret

resources:
  limits:
    cpu: 0.5
    memory: 0.5Gi
  requests:
    cpu: 0.1
    memory: 0.25Gi

autoscaling:
  enabled: true
  minReplicas: 1
  maxReplicas: 3
  targetCPUUtilizationPercentage: 80

persistence:
  enabled: true
  accessModes:
    - ReadWriteOnce
  size: 5Gi
  storageClassName: main-provisioner

Secrets requis (bookstack-db-secret)

Variables d’environnement attendues :

kubectl create secret generic bookstack-db-secret \
  --from-literal=DB_HOST=mariadb.svc.cluster.local \
  --from-literal=DB_PORT=3306 \
  --from-literal=DB_USER=bookstack \
  --from-literal=DB_PASS=superSecure123 \
  --from-literal=DB_DATABASE=bookstackdb \
  --from-literal=APP_URL=https://wiki.egilberny.com \
  --from-literal=APP_KEY=base64:6ZQaXoC8g2V9sRMaZ9rYtpKakW3uWVwv6NND+Lgfd0k= \
  --from-literal=AUTH_METHOD=ldap \
  --from-literal=LDAP_SERVER=ldap://ldap.local \
  --from-literal=LDAP_BASE_DN=dc=example,dc=org \
  --from-literal=LDAP_DN=cn=admin,dc=example,dc=org \
  --from-literal=LDAP_PASS=adminPass123

Fichiers Helm – templates du chart

Voici les principaux fichiers Helm nécessaires pour déployer BookStack dans Kubernetes :

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "bookstack.fullname" . }}
  labels:
    {{- include "bookstack.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "bookstack.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "bookstack.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "bookstack.serviceAccountName" . }}
      containers:
        - name: bookstack
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          envFrom:
            - secretRef:
                name: {{ .Values.envFromSecret.name }}
          ports:
            - containerPort: {{ .Values.service.targetPort }}
              name: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - name: bookstack-data
              mountPath: /config
      volumes:
        - name: bookstack-data
          persistentVolumeClaim:
            claimName: {{ include "bookstack.fullname" . }}-pvc
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}

templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ include "bookstack.fullname" . }}
  labels:
    {{- include "bookstack.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}
      protocol: {{ .Values.service.protocol }}
  selector:
    {{- include "bookstack.selectorLabels" . | nindent 4 }}

templates/httproute.yaml

{{- if .Values.ingress.enabled }}
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: {{ include "bookstack.fullname" . }}-route
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "bookstack.labels" . | nindent 4 }}
spec:
  parentRefs:
    - name: {{ .Values.ingress.gateway.name }}
      namespace: {{ .Values.ingress.gateway.namespace }}
  hostnames:
    - "{{ .Values.ingress.hostname }}.svc.{{ .Release.Namespace }}.k8s.lab"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: {{ include "bookstack.fullname" . }}
          port: {{ .Values.service.port }}
{{- end }}

templates/pvc.yaml

{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ include "bookstack.fullname" . }}-pvc
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "bookstack.labels" . | nindent 4 }}
spec:
  accessModes:
    {{- toYaml .Values.persistence.accessModes | nindent 4 }}
  resources:
    requests:
      storage: {{ .Values.persistence.size }}
  storageClassName: {{ .Values.persistence.storageClassName }}
{{- end }}

templates/_helpers.tpl

{{- define "bookstack.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{- define "bookstack.selectorLabels" -}}
app.kubernetes.io/name: {{ include "bookstack.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{- define "bookstack.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "bookstack.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "bookstack.name" -}}
{{- .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "bookstack.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{ include "bookstack.fullname" . }}
{{- else }}
{{- .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Composants créés

Le chart Helm que j’ai construit génère plusieurs ressources Kubernetes essentielles à un déploiement robuste et maintenable. Voici les principaux composants, expliqués avec un regard d’ingénierie.

Deployment

C’est le cœur de la stratégie de déploiement. Le Deployment assure que le bon nombre de pods BookStack sont toujours en ligne. Il gère aussi les mises à jour, les redémarrages et permet de spécifier des ressources minimales/optimales (CPU, mémoire). Ce manifeste référence le container Docker principal, les volumes nécessaires et injecte toutes les variables d’environnement via un secret sécurisé.

Service

Le Service expose BookStack à l’intérieur du cluster via un port HTTP standard. Il agit comme un proxy stable vers les pods BookStack, et constitue la cible logique pour les composants de routage comme les Ingress ou les HTTPRoute. Ce découplage garantit que l'adresse du pod ne change pas entre redémarrages.

PersistentVolumeClaim (PVC)

Le PVC monte un volume durable sur le chemin /config. Ce volume conserve les fichiers de configuration, les médias uploadés, les logs, et tout autre état persistant. En cas de redéploiement ou de crash du pod, les données ne sont pas perdues.

HTTPRoute (Gateway API)

Plutôt que d’utiliser un Ingress traditionnel, j’ai opté pour un HTTPRoute moderne. Ce composant, défini par la Gateway API, permet un routage HTTP/S fin, indépendant du contrôleur sous-jacent. Il gère notamment :

  • le nom DNS de l’application (en local et en prod)
  • la sécurité TLS via cert-manager
  • l’aiguillage des requêtes vers le Service Kubernetes correspondant

En résumé, ces composants assurent l’autonomie, la résilience, la modularité et la conformité réseau d’un déploiement BookStack moderne dans Kubernetes.


  • Deployment BookStack
  • Service exposé en ClusterIP
  • PersistentVolumeClaim pour /config
  • HTTPRoute pour Gateway API (ingress moderne)

Schéma de l’architecture

graph TD
  User[Utilisateur] --> Ingress[NGINX Ingress]
  Ingress --> BookStack[Pod BookStack]
  BookStack --> PVC[(Volume Persistant)]
  BookStack --> DB[(MariaDB sur VM)]
  BookStack --> LDAP[(Serveur LDAP local)]

Test local

kubectl port-forward svc/bookstack 8080:80
xdg-open http://localhost:8080

Et après ?

Ce qui fonctionne bien

  • Auth LDAP immédiate, interface fluide
  • Sauvegarde via dump MariaDB + PVC
  • Accès performant malgré DB externe

Leçons & améliorations

  • Monitoring à prévoir (Prometheus, Velero)
  • Amélioration continue avec alerting, backups planifiés, HA possible

Références

Prochaines étapes : intégration Prometheus, sauvegarde Velero, notifications via Alertmanager, intégration dans n8n.