260614 - procedure Joplin-compression
Contexte — instructions de Julien (260614)
J'utilise l'application Joplin sur tous mes appareils. C'est ma mémoire de tout (travail, hobbies, maison...) et donc à force, la base de données se remplit. Elle se compose de notes à l'intérieur desquelles on trouve des images. Même en n'important que des captures d'écran, le poids de ces images est devenu important — de l'ordre de 300 Mo initialement estimé (476 Mo constatés en réalité, voir ci-dessous).
Appareils synchronisés sous Joplin :
- VPS Juxjux.OVH (serveur de sync)
- PC Nexte (PC du travail de Julien)
- PC Elio+Jux (PC maison de Julien)
- Mobile Xiaomi T15pro de Julien
- Tablette Samsung S5 de Julien
- Session Ubuntu sur clé SSD de Julien
Objectif de la procédure :
- Compresser le stock d'images contenues dans la base de données Joplin via un process fiable de remplacement
- Monter un système de compression automatique quotidienne des nouvelles images
Principes définis par Julien :
- Tenir un carnet de bord (observatoire) du poids digital de la BDD Joplin dans la page BookStack Joplin, avec les 15 pièces jointes les plus lourdes
- Mettre en place sur le VPS une procédure duplication → compression → remplacement d'images, qui se propage via la sync Joplin sur tous les appareils
Exploration technique — Claude, 14/06/2026
Infrastructure Joplin sur le VPS
Contrairement à ce qu'on pourrait attendre, Joplin Server n'utilise pas SQLite mais PostgreSQL.
| Container | Image | Rôle |
|---|---|---|
joplin |
joplin/server:latest |
Serveur de sync Joplin |
joplin-db |
postgres:15-alpine |
Base de données |
joplin-nginx |
nginx:alpine |
Reverse proxy interne |
joplin_to_obsidian |
image custom | Container migration (actif, sans impact) |
Volume de données : /home/debian/joplin-data → /home/joplin/.config/joplin (bind mount)
Connexion DB : POSTGRES_HOST=joplin-db, POSTGRES_DATABASE=joplin, POSTGRES_USER=joplin
Important : le port 5432 de joplin-db n'est pas exposé à l'extérieur du réseau Docker. Tout script de manipulation doit tourner sur le VPS et se connecter via l'IP interne Docker.
Structure de la base de données
La table centrale est items (23 tables au total). Chaque note, ressource et paramètre Joplin est une ligne dans cette table.
Colonnes clés :
content(bytea) — données binaires brutescontent_size(integer) — taille en octetscontent_storage_id= 1 → stockage de typeDatabase(tout est dans PostgreSQL, pas de fichiers externes)jop_type— type d'item Joplinupdated_time— timestamp de dernière modification (utilisé par les clients pour détecter les changements à sync)
Répartition par type :
| jop_type | Signification | Nombre | Poids total |
|---|---|---|---|
| 0 | Ressource (image/fichier joint) | 736 | 476 MB |
| 1 | Note | 699 | 8,7 MB |
| 4 | Tag | 733 | 4,5 MB |
| 13 | NoteTag (relation note↔tag) | 250 | 3,9 MB |
| 2 | Carnet (Folder) | 94 | 707 KB |
| 6 | Master Key | 87 | 19 KB |
| 5 | Setting | 39 | — |
Analyse des ressources (jop_type = 0)
Les ressources sont stockées comme bytes bruts dans la colonne content. Le format est détectable via les magic bytes :
- PNG (
\x89PNG) — majorité des ressources - JPEG (
\xff\xd8) — portion significative - ZIP (
PK\x03\x04) — cas particulier : ZIP contenant plusieurs imagespage_1.png,page_2.png... (documents multi-pages)
La colonne mime_type est vide pour toutes les ressources — le type est implicite dans les bytes du contenu.
Top 15 ressources les plus lourdes (état initial) :
| Rang | ID | Taille | Format |
|---|---|---|---|
| 1 | 0xh87pWrRt82z0DbO9n3yQ | 12,2 MB | ZIP (page_1.png 7MB + page_2.png 5MB) |
| 2 | M0C32hKC2hMqPAxpRGiVgp | 7,2 MB | PNG |
| 3 | UbAqQY3cEbH62vioIHU6Pj | 6,0 MB | PNG |
| 4 | ZPQJVSjptMSxE71pV7JbAt | 5,9 MB | PNG |
| 5 | h4Lhw1Trx9wgmD7doX9NyZ | 5,9 MB | PNG |
| 6 | sbktcNb4IUj0yIovgDt0fL | 5,3 MB | PNG |
| 7 | jtzuGpvrRTR12iLzwhcSNn | 5,2 MB | PNG |
| 8 | VasIoF2e9EGuNQt38aOFMx | 4,9 MB | JPEG |
| 9 | ZACDcQWzQzDLdnV7Qnf933 | 4,3 MB | PNG |
| 10 | hExoJEEWT2tHz1demE5Nhm | 4,3 MB | PNG |
| 11 | veu4HT3bStx07gRUlAEYvG | 4,2 MB | PNG |
| 12 | bz9Twmb2F5lj0mPIQi48IB | 4,2 MB | JPEG |
| 13 | TzWK21r4n0yvbEtJh02DGB | 4,2 MB | JPEG |
| 14 | eD165w1bdEHJg0tq3qWok5 | 4,2 MB | PNG |
| 15 | HD4SeqIx252nP9nh8HDVnH | 4,1 MB | PNG |
Architecture retenue
Choix de compression — décision Julien, 14/06/2026
PNG → JPEG 85% (lossy). Toutes les images, qu'elles soient PNG ou JPEG à l'origine, sont converties/re-sauvegardées en JPEG qualité 85. Gain estimé : 50–75% par image. Acceptable pour des captures d'écran.
Pour les ZIP multi-pages : chaque image interne est convertie en JPEG 85%, le ZIP est reconstruit.
Phase 1 — Observatoire
Script joplin_observatoire.py sur le VPS :
- Connexion psycopg2 à joplin-db via IP réseau Docker interne
- Calcul des stats globales (taille totale, nombre d'items par format)
- Liste des 15 ressources les plus lourdes avec format et taille
- Mise à jour de la page BookStack Joplin (page 166) avec ces informations
Phase 2 — Compression (script principal)
Script joplin_compress.py sur le VPS :
Connexion : psycopg2 → IP Docker interne de joplin-db : 5432
Traitement par ressource :
- Lire le blob
contentdepuisitems(jop_type=0) - Détecter le format (magic bytes)
- Ouvrir avec Pillow, convertir en JPEG 85 (
quality=85, optimize=True) - Pour les ZIP multi-pages : dézipper → compresser chaque image → reconstruire le ZIP
- Si le gain est > 5% : mettre à jour
content,content_size,updated_timeen base - Logger le résultat (ID, taille avant, taille après, ratio)
Tracking des items traités : fichier JSON local /home/debian/joplin_compress_log.json — évite de retraiter les ressources déjà compressées lors des passages quotidiens.
Propagation sync : Joplin détecte les changements via updated_time. Lors de la prochaine synchronisation de chaque client, les ressources compressées sont re-téléchargées automatiquement.
Phase 3 — Service systemd (cron quotidien)
Timer systemd joplin-compress.timer → joplin-compress.service :
- Déclenchement quotidien (3h du matin)
- Traite uniquement les nouvelles ressources (non présentes dans le log JSON)
- Met à jour l'observatoire BookStack après chaque passage
Mise en production — 14/06/2026
Résultat du premier run (stock complet)
| Ressources traitées | 736 au total |
|---|---|
| Compressées | 424 |
| Ignorées (gain < 5%) | 312 |
| Erreurs | 0 |
| Poids avant | 476 Mo |
| Poids après | 119,7 Mo |
| Économie | 356 Mo (-78,9%) |
Scripts déployés sur le VPS
| Script | Rôle | Options |
|---|---|---|
/home/debian/joplin_compress.py |
Compression Pillow PNG/JPEG → JPEG 85%, ZIP multi-pages, mise à jour PostgreSQL | --dry-run (simulation) / --limit N (N ressources max) |
/home/debian/joplin_observatoire.py |
Stats DB + top 15 → mise à jour page BookStack Joplin (ID 166) | — |
Log tracking : /home/debian/joplin_compress_log.json — liste des ressources déjà traitées, évite les doublons aux runs suivants.
Timer systemd
| Service | joplin-compress.service |
|---|---|
| Timer | joplin-compress.timer |
| Déclenchement | Chaque nuit à 3h UTC (OnCalendar=*-*-* 03:00:00) |
| Logs | /var/log/joplin-compress.log |
| Statut | active (waiting) — prochain run : 15/06/2026 03:00 UTC |
Notes techniques
- joplin-db IP Docker interne :
172.27.0.4(peut changer si le container est recréé — vérifier avecdocker inspect joplin-db -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}') - La compression modifie
content,content_sizeetupdated_timedans la tableitems - Les clients Joplin re-téléchargent les ressources modifiées lors de la prochaine synchronisation (détection via
updated_time) - Les images à fond transparent (RGBA/LA/P) sont aplaties sur fond blanc avant conversion JPEG
Réexamen du 2026-08-27 — Caesium n'apporte rien, mais la base pèse 1 263 Mo
Question posée : la procédure Photos-Caesium (page 284), qui compresse les photos du PC avec caesiumclt, pourrait-elle profiter aussi à Joplin ? Réponse mesurée : non pour les images, mais l'examen a mis au jour un problème dix fois plus lourd.
1. Le gisement image est épuisé
État au 2026-08-27 : 729 ressources, 117,2 Mo (contre 476 Mo avant la procédure de juin, soit -79,3 %). Répartition par signature :
| Signature | Format | Nombre | Poids |
|---|---|---|---|
ffd8ffe0 | JPEG/JFIF — produits par Pillow | 551 | 105 Mo |
25504446 | 5 | 5,0 Mo | |
89504e47 | PNG restants | 42 | 4,3 Mo |
ffd8ffe1 | JPEG avec EXIF (originaux) | 24 | 1,7 Mo |
| autres | WebP, SVG, HTML, GIF, MP4 | 107 | 1,3 Mo |
Test réalisé sur deux échantillons extraits de la base (20 JPEG tirés au hasard, 5,20 Mo ; les 20 plus gros PNG, 3,84 Mo), compressés avec caesiumclt 1.4.0 sur le PC — aucune installation sur le VPS :
| Population | Passe Caesium | Gain | Coût qualité |
|---|---|---|---|
| 551 JPEG (105 Mo) | --lossless | -5,0 % | aucun, pixels intacts |
-q 85 (mozjpeg vs libjpeg) | -14,8 % | 2e génération de perte | |
-q 80 | -32,0 % | perte visible sur du texte | |
| 42 PNG (4,3 Mo) | --lossless | -27,7 % | aucun |
--lossless --zopfli | -29,8 % | aucun (mais très lent) | |
-q 80 | -69,1 % | quantification, mauvais pour du texte |
Conclusion : un passage Caesium sans risque récupérerait environ 6,5 Mo sur 117, soit -5,5 %. Le jeu n'en vaut pas la chandelle. La procédure de juin a pris l'essentiel du gain, et une seconde passe lossy sur des captures d'écran dégraderait le texte pour un bénéfice marginal.
Réserve méthodologique : la procédure actuelle convertit les PNG en JPEG q85. Pour des captures d'écran, le JPEG est un mauvais format — il crée du crénelage autour du texte. Une optimisation PNG sans perte (-27,7 % mesurés) serait meilleure en qualité pour un gain comparable. Priorité faible : il n'arrive que 3 à 4 images par semaine.
Vérification que le script quotidien fonctionne toujours : les 12 ressources les plus récentes sont toutes en ffd8ffe0 dès qu'elles ont une taille significative, donc bien passées par Pillow. L'image du 27/08 (113 ko) correspond exactement à la ligne de log [OK] NkiVrkTlkYIp033uedJsdd PNG 900KB -> 113KB (87.4%). Le dispositif est vivant.
2. Le vrai poids : la table events, 1 072 Mo pour 7,28 millions de lignes
La base joplin pèse 1 263 Mo. Les images, TOAST compris, n'en représentent que 172.
| Table | Total (avec index) | Table seule | Lignes vivantes |
|---|---|---|---|
events | 1 072 Mo | 500 Mo | 7 287 073 |
items (notes + images) | 172 Mo | 2 048 ko | 2 642 |
changes | 8 080 ko | 3 992 ko | 0 |
| tout le reste | ~2,5 Mo | — | — |
Les index d'events pèsent à eux seuls 573 Mo (events_id_unique 284 Mo, events_pkey 156 Mo, plus deux index secondaires).
Origine
TaskService.runTask() écrit un événement au début et à la fin de chaque tâche de fond (EventType.TaskStarted puis TaskCompleted). Une de ces tâches tourne toutes les ~10 secondes. Cadence mesurée : 23 400 lignes par jour, sans interruption depuis le 30/09/2025. Soit environ 1 Go par an.
Ces lignes ne servent à rien : EventModel ne lit jamais que le dernier événement par (type, nom), via lastEventByTypeAndName (orderBy('counter','desc').first()). Les 7,28 millions d'autres sont mortes.
Cause : un interrupteur laissé sur « off »
Joplin Server livre sa propre tâche de purge, désactivée par défaut. Dans l'image installée (joplin/server 3.7.1) :
dist/env.js:122 EVENTS_AUTO_DELETE_ENABLED: false
dist/env.js:123 EVENTS_AUTO_DELETE_AFTER_DAYS: 30
dist/utils/setupTaskService.js:105
if (config.EVENTS_AUTO_DELETE_ENABLED) {
tasks.push({
id: TaskId.DeleteOldEvents,
schedule: '0 0 * * *',
run: (models) => models.event().deleteOldEvents(
config.EVENTS_AUTO_DELETE_AFTER_DAYS * Day),
});
}
Le container joplin ne définit pas la variable (seul MAX_ITEM_SIZE figure parmi les réglages non standard). La tâche DeleteOldEvents n'a donc jamais été enregistrée, et rien n'a jamais été purgé.
Ce que ça coûte
- La sauvegarde nocturne. L'archive Joplin est la plus grosse du VPS (276 Mo) et le poste le plus lent du run (~14 min sur 21). D'après la largeur des lignes (~65 octets de texte par événement), les événements morts représentent grossièrement la moitié de cette archive.
- Le disque du VPS, à 65 % (61 Go utilisés sur 99), après l'incident de saturation du 24/06/2026.
- La mémoire et le swap de
joplin-db, qui figurait parmi les principaux détenteurs de swap au relevé du 03/08.
Correction — à appliquer
Purge à 30 jours : il resterait ~702 000 lignes au lieu de 7 282 457, soit -90,4 %. La sauvegarde maigrit immédiatement (pg_dump n'exporte que les lignes vivantes) ; le disque n'est rendu qu'après un VACUUM FULL.
- Activer le mécanisme officiel — stack Portainer 16, service
joplin, ajouter aux variables d'environnement :
puis mettre la stack à jour. La tâche s'exécute alors chaque nuit à minuit UTC — sans conflit avec la sauvegarde de 2 h.- EVENTS_AUTO_DELETE_ENABLED=1 - EVENTS_AUTO_DELETE_AFTER_DAYS=30 - Rendre l'espace au disque, une fois le premier passage effectué :
Verrou exclusif, mais bref une fois la table réduite.sudo docker exec joplin-db psql -U joplin -d joplin -c "VACUUM FULL events;"
Variante plus douce, si l'on préfère éviter que le premier passage supprime 6,58 millions de lignes en une seule instruction : faire le rattrapage par lots avant d'activer la tâche — créer CREATE INDEX CONCURRENTLY tmp_events_created ON events(created_time), boucler des DELETE … WHERE ctid IN (SELECT ctid … LIMIT 500000), retirer l'index, puis VACUUM FULL.
3. Bug corrigé le 2026-08-27 — l'observatoire ne tournait plus depuis 72 jours
joplin_observatoire.py plantait chaque nuit depuis la mi-juin : psycopg2.OperationalError: connection to server at "172.27.0.4" … Connection refused. L'IP Docker de joplin-db était passée à 172.27.0.3 lors d'une recréation du container.
Le correctif du 2026-06-19, qui remplaçait l'IP figée par une résolution dynamique, n'avait été appliqué qu'à joplin_compress.py — l'observatoire avait été oublié. La compression, elle, a continué de fonctionner normalement pendant toute la période.
Même correctif appliqué (docker inspect joplin-db au démarrage), sauvegarde joplin_observatoire.py.bak-260827. Exécution de contrôle réussie : page 166 réalimentée — 729 ressources, 117,2 Mo, -79,3 %.
Leçon : le compteur de tracebacks du log est un bon canari. sudo grep -c Traceback /var/log/joplin-compress.log renvoyait 72 — une par nuit — sans que personne ne s'en aperçoive, parce que la partie compression, elle, écrivait des lignes normales.
No comments to display
No comments to display