Stack d'orchestration Figures Libres (Docker Compose)

Câble ensemble le générateur (dépôt generator/) et le CMS Grav d'exemple
(dépôt grav-reference/, monté :ro), tous deux clonés côte à côte et non
versionnés ici. Contient : docker-compose dev + surcouche prod (rsync/SSH),
image Grav (docker/grav), doc de câblage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 18:38:04 +02:00
commit 48b7f97954
6 changed files with 273 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Dépôts séparés, clonés côte à côte (chacun son propre remote) :
# generator/ = l'app Nuxt = le livrable (gitea figureslibres.io)
# grav-reference/ = le CMS d'exemple = son propre dépôt, ~4,8 Go
generator/
grav-reference/
# Docs : PDF de référence + spec (déjà réalisée, désormais désynchro du code).
# Le Dockerfile de l'image Grav reste versionné (docker/grav/).
docs/
# Divers
.DS_Store
*.log
.env

46
CLAUDE.md Normal file
View File

@@ -0,0 +1,46 @@
# CLAUDE.md — stack Figures Libres (orchestration)
Dépôt d'**orchestration** de l'environnement du générateur de portfolios. Il ne
contient **pas** de code applicatif : il câble ensemble trois checkouts posés
côte à côte, via Docker Compose.
```
portfolio-maker/ ← CE dépôt (stack)
├── docker-compose.yml # grav-reference (8089) + generator (8091)
├── docker-compose.prod.yml # surcouche déploiement (build Nitro + rsync prod)
├── docker/grav/Dockerfile # image PHP+Apache servant grav-reference/
├── generator/ ← dépôt À PART (l'app) — cloné ici, git-ignoré
└── grav-reference/ ← dépôt À PART (CMS d'exemple) — cloné ici, git-ignoré
```
## Règle absolue : tout tourne dans Docker
Node, build Nuxt, Playwright/Chromium, Ghostscript, ffmpeg, rsync — **rien sur
l'hôte**. Exception connue : vérifier le rendu d'un PDF (`pdftoppm`) peut se faire
sur l'hôte, les templates print étant piégeux (voir `generator/CLAUDE.md`).
```bash
docker compose up -d # grav-reference + generator
```
- Générateur : http://localhost:8091 — mot de passe dev `figureslibres`.
- Grav de référence : http://localhost:8089 (CMS d'exemple, **pas** l'app à développer).
## Les trois composants
- **`generator/`** — l'app Nuxt = le livrable. Dépôt autonome (Dockerfile inclus),
poussé sur le gitea figureslibres.io. Toute la doc app est dans `generator/CLAUDE.md`.
- **`grav-reference/`** — le vrai site Grav, **son propre dépôt** (~4,8 Go), monté
`:ro`. Source de contenu en lecture seule, jamais modifié.
- **Ce dépôt** — compose, image Grav, doc de câblage. Ne versionne ni `generator/`
ni `grav-reference/` (ils ont chacun leur dépôt).
## Déploiement (mode prod)
Contenu tiré de la prod par rsync/SSH en lecture seule vers
`generator/data/content-cache/`, déclenché par le bouton « Rafraîchir ». Voir les
variables `.env` (non versionné) documentées dans `docker-compose.prod.yml`.
```bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d generator
```

97
README.md Normal file
View File

@@ -0,0 +1,97 @@
# portfolio-maker (stack)
Dépôt d'**orchestration** de l'environnement du générateur de portfolios PDF de
Figures Libres. Il ne contient pas de code applicatif : il câble ensemble, via
Docker Compose, l'app et le CMS Grav d'exemple qui lui sert de source de contenu.
## Trois dépôts, un environnement
Ce dépôt attend deux autres checkouts **posés côte à côte** (chacun a son propre
dépôt git et n'est pas versionné ici) :
```
portfolio-maker/ # CE dépôt (stack)
├── docker/grav/Dockerfile # Image PHP+Apache servant grav-reference/
├── docker-compose.yml # Services grav-reference (8089) + generator (8091)
├── docker-compose.prod.yml # Surcouche déploiement (build Nitro + rsync prod)
├── generator/ ←──────── dépôt À PART : l'app Nuxt (Dockerfile inclus) = le livrable
└── grav-reference/ ←──────── dépôt À PART : le vrai site Grav (~4,8 Go), monté :ro
```
- **`generator/`** — le générateur (Nuxt). Détails d'architecture : voir son propre
`README.md` / `CLAUDE.md`. Poussé sur le gitea figureslibres.io.
- **`grav-reference/`** — CMS Grav d'exemple, en **lecture seule** (monté `:ro`).
C'est la source de contenu, **pas** l'app à développer.
> ⚠️ Tout tourne dans Docker : Node, build Nuxt, Playwright/Chromium, Ghostscript,
> ffmpeg, rsync — rien n'est installé sur l'hôte.
> 📦 **Non versionné dans ce dépôt** : `generator/` et `grav-reference/` (dépôts
> séparés, à cloner côte à côte), ainsi que `docs/` (PDF de référence + spec déjà
> réalisée). Le `Dockerfile` de l'image Grav, lui, reste versionné dans `docker/grav/`.
## Lancer en dev
```bash
docker compose up -d # grav-reference + generator
```
- Grav de référence : http://localhost:8089
- Générateur de portfolios : http://localhost:8091 — mot de passe partagé :
`figureslibres` (variable `PORTFOLIO_PASSWORD`)
Le générateur lit directement `grav-reference/user/pages/02.projets/` (mode DEV
de `ContentSource`). Le bouton « Rafraîchir » relit le contenu local.
Commandes utiles (toujours via le conteneur) :
```bash
docker compose run --rm --no-deps generator npm install # dépendances
docker compose exec generator npm run build # build de prod
docker logs -f figureslibres-portfolio-generator # logs du dev server
```
## Parcours utilisateur
1. Se connecter (mot de passe partagé) — on arrive sur la liste des **portfolios**.
2. Créer un **portfolio** : ajouter des projets via la modale « Ajouter un
projet » (recherche plein texte, filtres catégorie/tag — seuls les projets
publiés sont proposés). Chaque projet = **deux pages ou plus**, fidèles au
gabarit `book_v3` (PDF de référence dans `docs/`, non versionné) :
- une page **couverture** : bandeau 55 mm couleur de la catégorie (titre en
fonte d'« utilité » — Playfair/Manifont/Avara —, pastille, présentation
Syne 12/16 pt) + image de couverture (celle de la grille du CMS) ;
- des pages **grille** : filet 5 mm, marges 8 mm, gouttières 3,8 mm, images
**entières** (jamais tronquées) calées sur la grille — 8 dispositions au
choix, présentées par icônes. Les vidéos ne sont pas sélectionnables.
Couleur de bandeau et texte noir/blanc réglables (palette du gabarit +
couleurs du site). Pages spéciales : couverture, sommaire, page libre.
Aperçu WYSIWYG A3 paysage, sauvegarde automatique.
3. **Générer le PDF** (nom de fichier personnalisable) : rendu Playwright/Chromium
au format **A3 paysage** (1190,55 × 841,89 pt, comme le gabarit), images
redimensionnées à 2× leur taille d'affichage (sharp), post-traitement
Ghostscript, métadonnées. Le contenu utilisé est **figé**
(`data/frozen/<id>/`) : régénérer redonne le même PDF ;
« Réactualiser depuis Grav » resynchronise volontairement.
## Déploiement (mode PROD)
Le contenu est tiré de la prod par **rsync/SSH en lecture seule** vers
`generator/data/content-cache/`, déclenché par le bouton « Rafraîchir ».
```bash
# .env (non versionné)
PORTFOLIO_PASSWORD=
SESSION_SECRET=
INTERNAL_TOKEN=
SYNC_REMOTE=user@prod:/chemin/vers/user/pages/02.projets/
SYNC_SSH_KEY_FILE=/chemin/local/vers/cle_privee
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d generator
```
## Données
Tout est sur disque dans `generator/data/` (sauvegarde = copie du dossier) :
`portfolios/*.json` (compositions), `exports/*.pdf`, `frozen/<id>/` (snapshots),
`cache/` (miniatures, posters vidéo — régénérable), `content-cache/` (rsync prod).

23
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,23 @@
# Surcouche de déploiement (spec §5.2) :
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d generator
#
# - contenu tiré de la prod Figures Libres par rsync/SSH (bouton « Rafraîchir »)
# - build Nuxt puis serveur Nitro (pas de serveur de dev)
# - variables à définir dans un fichier .env (non versionné) :
# PORTFOLIO_PASSWORD=… mot de passe partagé
# SESSION_SECRET=… secret de session (longue chaîne aléatoire)
# INTERNAL_TOKEN=… jeton interne du rendu print
# SYNC_REMOTE=user@host:/chemin/vers/user/pages/02.projets/
# SYNC_SSH_KEY_FILE=~/.ssh/portfolio_sync clé privée SSH (lecture seule)
services:
generator:
command: sh -c "npm run build && npm run preview"
environment:
NUXT_CONTENT_SOURCE: prod
NUXT_SYNC_REMOTE: ${SYNC_REMOTE:?SYNC_REMOTE requis en mode prod}
NUXT_SYNC_SSH_KEY: /run/secrets/sync-ssh-key
NITRO_HOST: 0.0.0.0
NITRO_PORT: 3000
volumes:
- ./generator:/app
- ${SYNC_SSH_KEY_FILE:?SYNC_SSH_KEY_FILE requis en mode prod}:/run/secrets/sync-ssh-key:ro

46
docker-compose.yml Normal file
View File

@@ -0,0 +1,46 @@
services:
# Instance Grav de référence (site "Figures Libres") servant de source de
# contenu pour le développement du générateur de portfolios.
# Ce n'est PAS l'app à développer : c'est le CMS d'exemple, en lecture.
grav-reference:
build:
context: ./docker/grav
args:
PUID: "1000"
PGID: "1000"
container_name: figureslibres-grav-ref
restart: unless-stopped
ports:
- "8089:80"
volumes:
- ./grav-reference:/var/www/html
# Générateur de portfolios PDF (app Nuxt) — voir docs/spec-generateur-portfolios.md.
# Tout tourne dans ce conteneur : Node, build Nuxt, Playwright/Chromium, Ghostscript.
generator:
build:
# L'app est un dépôt autonome cloné dans ./generator (Dockerfile inclus).
context: ./generator
image: figureslibres-portfolio-generator
container_name: figureslibres-portfolio-generator
restart: unless-stopped
user: "1000:1000"
working_dir: /app
command: npm run dev
environment:
HOME: /tmp
NUXT_SESSION_SECRET: ${SESSION_SECRET:-dev-secret-figures-libres}
NUXT_PORTFOLIO_PASSWORD: ${PORTFOLIO_PASSWORD:-figureslibres}
NUXT_INTERNAL_TOKEN: ${INTERNAL_TOKEN:-dev-internal-token}
NUXT_CONTENT_SOURCE: ${CONTENT_SOURCE:-dev}
NUXT_CONTENT_DIR: /content/02.projets
NUXT_DATA_DIR: /app/data
# Mode prod (voir docker-compose.prod.yml) : synchro rsync/SSH
NUXT_SYNC_REMOTE: ${SYNC_REMOTE:-}
NUXT_SYNC_SSH_KEY: ${SYNC_SSH_KEY:-}
ports:
- "8091:3000"
volumes:
- ./generator:/app
# Contenu Grav monté en LECTURE SEULE (contrainte ferme, §18).
- ./grav-reference/user/pages/02.projets:/content/02.projets:ro

47
docker/grav/Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
FROM php:8.2-apache
# Match the container's web user to the host user so Grav can write to the
# bind-mounted app/ folder (cache, logs, tmp, user uploads, ...).
ARG PUID=1000
ARG PGID=1000
RUN groupmod -o -g "${PGID}" www-data \
&& usermod -o -u "${PUID}" www-data
# System deps for Grav's PHP extensions
RUN apt-get update && apt-get install -y --no-install-recommends \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libcurl4-openssl-dev \
libonig-dev \
libxml2-dev \
unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" \
gd \
curl \
mbstring \
zip \
dom \
xml \
fileinfo \
exif \
opcache \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Grav ships its own .htaccess; enable rewrite + allow overrides
RUN a2enmod rewrite headers
RUN sed -ri -e 's!AllowOverride None!AllowOverride All!g' /etc/apache2/apache2.conf
# Sensible PHP settings for Grav
RUN { \
echo 'memory_limit=256M'; \
echo 'upload_max_filesize=32M'; \
echo 'post_max_size=32M'; \
echo 'max_execution_time=120'; \
} > /usr/local/etc/php/conf.d/grav.ini
WORKDIR /var/www/html