SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-28

Installer Paperless-ngx sur un VPS avec Docker Compose

Installez Paperless-ngx sur un VPS avec Docker Compose : stack PostgreSQL officielle, PAPERLESS_URL, dossier consume, langues OCR, HTTPS et sauvegardes.

Ce que vous allez construire

Paperless-ngx sur un VPS transforme un dossier de documents numérisés en archive interrogeable. Vous déposez un PDF dans un répertoire surveillé. Le serveur exécute une OCR (reconnaissance optique de caractères), en extrait le texte, déduit une date et un correspondant, puis le classe. L’installation repose sur un seul fichier Docker Compose avec quatre services. Tout le reste concerne la configuration. Ce guide lui consacre l’essentiel de son contenu, car c’est à cette étape que les installations échouent. Paperless-ngx n’est pas une photothèque : l’OCR et la déduction du correspondant ne servent à rien pour un dossier de JPEG de vacances. Placez plutôt ces fichiers dans un serveur de photos adapté et réservez Paperless aux documents. La même distinction s’applique aux vidéos : une collection de films extraits appartient à un serveur multimédia, où une interface comme Jellyfin présentée comme un vidéoclub des années 90 met l’accent sur la navigation plutôt que sur la recherche.

Paperless-ngx est le fork communautaire maintenu du projet Paperless d’origine. Il est gratuit, auto-hébergé et stocke vos documents sous forme de fichiers classiques sur le disque. Vous ne perdez donc jamais l’accès à votre propre archive. L’exécuter sur un VPS plutôt que sur un ordinateur à domicile permet d’accéder à vos numérisations depuis n’importe où, sans ouvrir de port sur le routeur de votre domicile. Il s’associe bien à une instance Nextcloud privée pour les fichiers qui ne sont pas sur papier. Le même principe s’applique à l’ordinateur auquel votre scanner est connecté : un relais RustDesk que vous gérez sur ce VPS vous permet de piloter cette machine à distance, là encore sans ouvrir de port sur le routeur.

Ce que la stack exécute réellement

Le fichier Compose officiel démarre quatre conteneurs. Comprendre le rôle de chacun facilite la lecture des journaux.

  • webserver : l’image paperless-ngx elle-même. Elle exécute l’interface web, l’API, le consumer qui surveille votre dossier d’entrée et les workers de tâches Celery qui effectuent l’OCR.
  • db : PostgreSQL. Il stocke les métadonnées, les tags, les correspondants et les tables d’indexation de la recherche plein texte. Il ne stocke pas vos PDF.
  • broker : Valkey, un magasin clé-valeur compatible avec Redis. Il fournit la file de tâches entre le processus web et les workers.
  • gotenberg et tika : facultatifs, uniquement dans les variantes Compose -tika. Ils convertissent les documents Office (.docx, .xlsx, .odt) en PDF afin que paperless puisse les indexer.

En juillet 2026, le fichier Compose postgres épingle docker.io/library/postgres:18 et docker.io/valkey/valkey:9-alpine, et récupère l’application depuis ghcr.io/paperless-ngx/paperless-ngx:latest.

Prérequis

  • Un VPS KVM Ubuntu 24.04 avec un accès sudo, ainsi que Docker et le plugin Compose déjà installés. Si cette partie est nouvelle pour vous, commencez par les bases de Docker Compose pour un VPS, puis revenez ici.
  • Un nom de domaine avec un enregistrement A pointant vers le VPS. Paperless refuse de servir un nom d’hôte qui ne lui a pas été communiqué. Ce point intervient donc plus tôt que prévu.
  • La mémoire est la principale contrainte. PostgreSQL, Valkey, gunicorn et un worker OCR Tesseract résidents simultanément tiennent dans 2 GB pour un usage léger. Prévoyez 4 GB si vous comptez importer un arriéré de centaines de scans, car l’OCR d’un PDF volumineux de plusieurs pages provoque le pic de mémoire susceptible de faire tuer un worker par l’OOM killer du kernel.
  • Disque : votre archive est stockée en double, avec le fichier original et un PDF d’archive traité par OCR. Prévoyez donc environ deux fois la taille de vos scans.

Récupérer les fichiers Compose officiels

Il existe un installateur interactif :

bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"

Il pose des questions et écrit les fichiers à votre place. Le faire manuellement prend quatre commandes et vous permet de savoir où se trouve chaque élément. C’est préférable sur un serveur que vous devrez administrer.

mkdir -p ~/paperless && cd ~/paperless
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
curl -fsSL -o docker-compose.env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.env
curl -fsSL -o .env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env

Les variantes se trouvent dans le même répertoire : docker-compose.sqlite.yml, docker-compose.mariadb.yml et une version -tika de chacune. Choisissez postgres pour une nouvelle installation. SQLite convient pour quelques centaines de documents, mais l’index de recherche en texte intégral ralentit bien avant PostgreSQL.

Le fichier .env contient une ligne : COMPOSE_PROJECT_NAME=paperless. Ce nom devient le préfixe de chaque conteneur et volume. Ne le supprimez donc pas avant de vous demander pourquoi docker compose down -v ne trouve plus vos données.

Configurer docker-compose.env avant le premier démarrage

Deux paramètres sont obligatoires. Générez la clé secrète avec la commande indiquée dans la documentation du projet :

python3 -c "import secrets; print(secrets.token_urlsafe(64))"

Modifiez ensuite docker-compose.env :

PAPERLESS_SECRET_KEY=<the long string you just generated>
PAPERLESS_URL=https://paperless.example.com
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu+eng
USERMAP_UID=1000
USERMAP_GID=1000

PAPERLESS_SECRET_KEY contient par défaut la valeur littérale change-me. Cette clé signe les cookies de session. Si vous la laissez telle quelle, toute personne qui connaît la valeur par défaut peut falsifier une session. Définissez-la avant le premier démarrage, car sa modification ultérieure déconnecte tous les utilisateurs.

PAPERLESS_URL est le paramètre qui vous évite une heure de diagnostic. Paperless est une application Django, et Django valide l’en-tête Host de chaque requête. Définissez PAPERLESS_URL : Django renseigne alors automatiquement ALLOWED_HOSTS, CORS_ALLOWED_HOSTS et CSRF_TRUSTED_ORIGINS. Si vous le laissez vide, définissez un domaine qui pointe vers le serveur : chaque page renverra Bad Request (400), et le journal du conteneur contiendra DisallowedHost. Indiquez la valeur sans slash final ni chemin.

USERMAP_UID et USERMAP_GID définissent l’utilisateur sous lequel le conteneur s’exécute. Faites-les correspondre à votre propre compte, vérifié avec id -u et id -g. S’ils ne correspondent pas, les fichiers que vous copiez dans le dossier consume ne sont pas lisibles par le consumer. Le journal affiche alors une erreur de permissions au lieu de lancer un import.

Démarrer la stack et créer le premier utilisateur

docker compose pull
docker compose up -d
docker compose run --rm webserver createsuperuser
docker compose logs -f webserver

createsuperuser demande un nom d’utilisateur, une adresse e-mail et un mot de passe. Il n’existe aucun identifiant par défaut. Si vous ignorez cette étape, vous arrivez sur une page de connexion qui n’acceptera jamais aucune tentative. Attendez que le journal indique que le serveur écoute sur le port 8000 avant d’ouvrir le site dans le navigateur. Le tout premier démarrage exécute également les migrations de la base de données, ce qui prend une à deux minutes.

Vérifiez le service localement avant d’utiliser un domaine :

curl -I http://127.0.0.1:8000

Une redirection 302 vers /accounts/login/ signifie que la stack fonctionne correctement.

Placez HTTPS devant l’application

Le fichier Compose fourni publie 8000:8000, qui est lié à toutes les interfaces. Sur un VPS public, cela expose toute votre archive de documents en HTTP non chiffré à quiconque découvre l’adresse. Modifiez la ligne du port pour la lier uniquement à loopback :

    ports:
      - "127.0.0.1:8000:8000"

Terminez ensuite TLS (Transport Layer Security) dans un reverse proxy et redirigez les requêtes vers 127.0.0.1:8000. Si cette application est la seule sur le serveur, n’importe quel proxy doté d’un client ACME (Automatic Certificate Management Environment) convient. Si plusieurs conteneurs utilisent une même configuration de certificats, suivez le modèle de reverse proxy Traefik pour plusieurs applications Docker Compose et rattachez le service webserver au réseau du proxy, sans publier de port.

Quel que soit le proxy utilisé, il doit transmettre X-Forwarded-Proto: https. Sans cet en-tête, Django considère que la requête est arrivée en HTTP, le contrôle de l’origine du formulaire de connexion échoue et vous obtenez CSRF verification failed. Request aborted. sur une page qui semble correcte. L’autre partie de la correction consiste à définir PAPERLESS_URL sur l’adresse https:// exacte que vous saisissez dans le navigateur.

Augmentez également la limite de taille des téléversements du proxy. Un scan de 40 MB traversant un proxy qui limite les corps de requête à 1 MB est rejeté avant même que paperless ne le reçoive, et le navigateur affiche une erreur de téléversement générique.

Fonctionnement du répertoire consume

Le fichier compose monte ./consume dans le conteneur avec un bind mount. Tout fichier placé à cet emplacement est importé, puis supprimé du répertoire, car il se trouve désormais dans le volume media géré par paperless.

cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserver

Le consumer doit détecter le nom du fichier, exécuter l’OCR, puis terminer en indiquant que le document a été ajouté. Pour un scan d’une page, le cycle complet dure quelques secondes. Pour un document long, il peut durer une minute ou davantage.

Deux paramètres modifient la recherche des fichiers. PAPERLESS_CONSUMER_RECURSIVE=true demande à paperless de rechercher aussi dans les sous-répertoires. PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true transforme le nom de chaque sous-répertoire en tag. Ainsi, déposer un fichier dans consume/invoices/2026/ lui applique les tags invoices et 2026. C’est le système de classement le moins coûteux que vous puissiez mettre en place.

La détection constitue l’autre partie du fonctionnement. Par défaut, PAPERLESS_CONSUMER_POLLING_INTERVAL vaut 0. paperless utilise donc les notifications du système de fichiers du kernel, qui sont immédiates. Ces notifications ne traversent pas un système de fichiers réseau. Si votre répertoire consume est un partage NFS ou SMB dans lequel un scanner réseau peut écrire, aucun fichier n’est détecté. Pour corriger le problème, définissez l’intervalle sur un nombre de secondes positif afin que paperless analyse le répertoire à intervalles réguliers.

Langues OCR et coût associé

PAPERLESS_OCR_LANGUAGE attend le code Tesseract à trois lettres, eng par défaut. Combinez plusieurs langues avec un signe plus, par exemple deu+eng. Tesseract essaie alors chaque langue et conserve le meilleur résultat. Chaque langue supplémentaire multiplie donc le temps CPU consacré à chaque page. Sur un VPS avec vCPU partagé, cela peut faire la différence entre un scan terminé en dix secondes et un autre terminé en une minute. Indiquez uniquement les langues utilisées dans vos documents.

L’image inclut l’anglais, l’allemand, l’italien, l’espagnol et le français. Pour toute autre langue, ajoutez-la à PAPERLESS_OCR_LANGUAGES sous forme de liste séparée par des espaces, par exemple PAPERLESS_OCR_LANGUAGES=tur ces, puis redémarrez. Le conteneur télécharge les packs de données Tesseract au démarrage. Le premier démarrage après cette modification est donc plus long.

Sauvegarder la base de données et les fichiers multimédias

Copier les volumes Docker pendant l’exécution de PostgreSQL produit une sauvegarde qui peut être impossible à restaurer. Paperless fournit son propre outil d’exportation. Celui-ci écrit les documents ainsi qu’un manifeste JSON contenant toutes les métadonnées dans le bind mount ./export :

docker compose exec webserver document_exporter ../export --delete --no-progress-bar

--delete supprime les fichiers exportés qui ne correspondent plus à un document actuel. Le dossier reste ainsi un miroir au lieu de grossir indéfiniment. --no-progress-bar évite les sorties inutiles lorsque la commande est exécutée par cron.

La restauration s’effectue avec document_importer depuis ce même dossier sur une stack vierge. Le répertoire d’exportation est donc le seul élément à conserver en lieu sûr. Envoyez-le hors site selon un calendrier défini avec des sauvegardes restic chiffrées et dédupliquées depuis votre VPS, et exécutez d’abord l’exportation afin que restic ne capture jamais une archive partiellement écrite.

Vérifiez une sauvegarde en contrôlant que export/manifest.json existe et que le nombre de fichiers correspond au nombre de documents affiché dans l’interface. Une sauvegarde que vous n’avez jamais listée n’est pas une sauvegarde. Un export nocturne qui commence à échouer discrètement est encore plus problématique. Configurez donc la tâche cron pour envoyer son code de sortie à votre propre serveur ntfy. Vous découvrirez ainsi la panne la semaine où elle survient, plutôt que le jour où vous devez restaurer.

FAQ

Pourquoi chaque page renvoie-t-elle « Bad Request (400) » après avoir fait pointer mon domaine vers l’application ?

Django rejette l’en-tête Host parce que votre domaine ne figure pas dans ALLOWED_HOSTS. Définissez PAPERLESS_URL=https://paperless.example.com dans docker-compose.env, sans slash final, puis exécutez docker compose up -d pour recréer le conteneur. Modifier uniquement le fichier d’environnement ne suffit pas, car le conteneur en cours d’exécution conserve l’environnement avec lequel il a démarré.

J’ai déposé un PDF dans le dossier consume, mais rien ne s’est passé. Quel est le problème ?

Commencez par vérifier docker compose logs webserver. Une erreur de permissions signifie que USERMAP_UID et USERMAP_GID ne correspondent pas au compte propriétaire du fichier. Corrigez ces valeurs, puis recréez le conteneur. L’absence totale de ligne dans le journal signifie que l’événement associé au fichier n’est jamais arrivé. Cela se produit sur les partages réseau, car les notifications du noyau ne les traversent pas. Définissez PAPERLESS_CONSUMER_POLLING_INTERVAL sur une valeur telle que 30, et paperless analysera le dossier toutes les 30 secondes à la place.

Puis-je exécuter paperless-ngx avec SQLite au lieu de PostgreSQL ?

Oui, docker-compose.sqlite.yml est pris en charge et utilise moins de mémoire, ce qui convient à un petit VPS. Le compromis devient visible lorsque vos archives grossissent : la recherche en texte intégral et les modifications groupées des tags ralentissent sensiblement lorsque le nombre de documents atteint plusieurs milliers. Une migration ultérieure nécessite un export et un import. Choisissez donc PostgreSQL dès maintenant si vous prévoyez de continuer à faire croître vos archives.

De quel espace disque une archive de scans a-t-elle réellement besoin ?

Comptez environ le double de la taille de vos fichiers source. Paperless conserve l’original et stocke un second PDF traité par OCR avec une couche de texte interrogeable, ainsi que de petites miniatures. Un scan de 200 KB contenant uniquement du texte reste peu volumineux. Un scan couleur de 30 MB d’un long contrat occupe environ 60 MB. Ajoutez le répertoire d’export si vous le conservez sur le même disque : la même archive occupe alors trois fois plus d’espace disque.

Ai-je besoin des conteneurs Tika et Gotenberg ?

Uniquement si vous voulez indexer des fichiers Word, Excel ou OpenDocument avec vos PDF. Ils convertissent ces formats en PDF afin que paperless puisse les traiter par OCR et les rechercher. Ils ajoutent également deux conteneurs supplémentaires en fonctionnement et quelques centaines de mégaoctets de mémoire. Vous pouvez donc vous en passer sur une petite machine si tous vos fichiers sont déjà des PDF ou des images.

#paperless-ngx#documents#auto-hébergement#docker#ocr