Aller au contenu
Q
QuoteNode

Installation

Installer avec Portainer, QNAP ou NAS

Déployer QuoteNode sur un NAS ou un hôte Docker géré par Portainer avec des instructions d'installation complètes.

Installer avec Portainer, QNAP ou NAS

Utilisez ce chemin si vous exécutez Docker via Portainer, QNAP Container Station, Synology ou une plateforme similaire. L’interface exacte diffère selon le vendeur, mais le stack Docker Compose sous-jacent est identique. Les étapes décrivent le Web editor de Portainer ; une plateforme qui lance Compose depuis un dossier utilise à la place le paquet de version inchangé (étape 2).

Quand l’utiliser

  • Vous opérez déjà des stacks Docker via une interface de gestion de conteneurs.
  • Vous comprenez comment la plateforme mappe les volumes et les ports.
  • Vous pouvez vérifier les sauvegardes en dehors de l’interface NAS.

Ce qui peut ne pas fonctionner

  • Les liens publics ne fonctionnent pas hors LAN sauf si NAT, DNS, proxy et HTTPS sont configurés.
  • Les modes veille du NAS peuvent interrompre le service.
  • Certaines interfaces Docker NAS réécrivent les fichiers compose ou les variables d’environnement ; vérifiez la stack rendue avant de vous y fier.
  • Les sauvegardes stockées uniquement sur le même NAS ne protègent pas contre une défaillance NAS.

Prérequis

  • Support Docker Compose v2 sur votre plateforme.
  • Support de volumes persistants pour les données PostgreSQL, les uploads, les PDFs et les sauvegardes.
  • Au moins 2 Go de RAM disponibles et 10 Go d’espace disque libre.
  • Une URL stable : IP LAN pour usage interne, ou domaine public derrière un reverse proxy pour les liens clients.
  • Un hôte x86 64 bits (linux/amd64). Les appareils ARM, y compris les NAS à processeur ARM, ne sont pas pris en charge.

Étape 1 — Préparer les réglages

Préparez les réglages ci-dessous. Remplacez l’URL par votre adresse NAS réelle ou domaine public. Dans Portainer, ils deviennent les variables d’environnement de la stack (étape 2), que Portainer enregistre dans stack.env ; dans une installation basée sur un dossier, ils forment le fichier .env à côté de docker-compose.yml :

Pour un déploiement NAS interne uniquement :

# Database
DB_URL=jdbc:postgresql://postgres:5432/quotenode
DB_USERNAME=quotenode
DB_PASSWORD=change-me-to-something-random-32-chars
DB_NAME=quotenode

# ============================================================
# SECURITY SECRETS — UNIQUE to this installation.
# SAVE THIS FILE and keep a secure backup.
# ============================================================
DB_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
TIMING_TOKEN_SECRET=change-me-timing-secret-min-32-chars
PUBLIC_LINK_PASSWORD_SESSION_SECRET=change-me-session-secret-min-32

# Internal NAS URL
CORS_ALLOWED_ORIGINS=http://nas.local
DOMAIN=http://nas.local

# Client IP is auto-detected behind internal proxies and CDNs (Cloudflare included) — leave empty.
# Set only for an unusual proxy whose transport peer is a PUBLIC address to trust (IP/CIDR or hostname).
SECURITY_TRUSTED_PROXIES=

# Application
LOG_LEVEL=INFO
SPRING_PROFILES_ACTIVE=prod

Pour un domaine public derrière votre propre reverse proxy (ici à l’adresse 192.168.1.5) qui termine HTTPS :

CORS_ALLOWED_ORIGINS=https://crm.example.com
DOMAIN=:80
EDGE_TRUSTED_PROXIES=192.168.1.5

Sauvegardez immédiatement une copie de ces réglages en dehors du NAS. Si vous perdez DB_ENCRYPTION_KEY, les champs chiffrés deviennent définitivement illisibles.

Étape 2 — Créer la stack

Dans Portainer, ouvrez Stacks → Add stack, nommez la stack (par exemple quotenode) et choisissez Web editor. Le nom de la stack devient le nom du projet Compose et préfixe les noms des volumes ; conservez-le lors des mises à jour ultérieures de la stack. Collez le bloc ci-dessous : le docker-compose.yml de la version v0.9.8-alpha (chaque image épinglée par digest) avec une adaptation pour Portainer, expliquée dans ses premières lignes de commentaire — env_file lit stack.env au lieu de .env. Images, services, ports et volumes sont inchangés.

# Portainer Web editor adaptation of the release docker-compose.yml: env_file reads stack.env (the stack's
# environment variables) instead of .env in postgres, backend and backup-worker. Images, services, ports and
# volumes are unchanged. For a folder-based installation use the unchanged file of the release package.
x-json-file-logging: &json-file-logging
  driver: json-file
  options:
    max-size: "${DOCKER_LOG_MAX_SIZE:-10m}"
    max-file: "${DOCKER_LOG_MAX_FILE:-5}"

# ==========================================================================
# QuoteNode — standalone deployment (docker compose)
# GENERATED by scripts/render-deployment.mjs — do not edit by hand.
# Release v0.9.8-alpha (commit ff74453ca68f3c40bd86a32f0b0b813aaffc7aea, build 28); platforms: linux/amd64.
# API image:      ghcr.io/lesisty7/quotenode/quotenode-api@sha256:0ed701f952c27ea0e1db168a00de75facb231f7c6d5f6b4f0f3b77ef98d4f140
# Frontend image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
#
# Settings: the .env file in this directory (start from env.example, the commented catalogue of every
# setting; postgres, backend and backup-worker read it). Start: docker compose up -d
# DOMAIN: public host name for automatic HTTPS, or :80 / http://<address> for plain HTTP on a trusted LAN.
# Only the caddy entry point publishes ports (HTTP_PORT/HTTPS_PORT, default 80/443).
# Upgrades: replace this file with the next release package version; volumes keep the data.
# ==========================================================================

services:
  postgres:
    # Database (PostgreSQL 18); internal network only, data in the database volume.
    image: postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873
    # JIT is off by default: for this OLTP workload most queries fall below jit_above_cost,
    # so JIT compilation rarely helps and can add latency. Set PG_JIT=on to re-enable.
    command: ["postgres", "-c", "jit=${PG_JIT:-off}"]
    env_file: stack.env
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_INITDB_ARGS: "--data-checksums"
      # postgres:18 refuses to start on a bare /var/lib/postgresql/data mount (new default layout).
      # Pin PGDATA to the legacy path so the existing volume mount keeps working; major upgrades go
      # through a logical dump/restore, never an in-place upgrade of the data directory.
      PGDATA: /var/lib/postgresql/data
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 10
    # CPU ceilings are opt-in (*_CPUS): 0 = no ceiling, all host cores. Docker refuses a ceiling above the
    # host's core count, so a fixed default would stop the stack on smaller hosts (minimum: 2 cores).
    cpus: ${POSTGRES_CPUS:-0}
    # Ceiling, not a reservation: PG sits at tens of MB until working set/connections grow.
    # Default 512m fits the 4 GB-VM profile; raise POSTGRES_MEM_LIMIT for larger tiers.
    mem_limit: ${POSTGRES_MEM_LIMIT:-512m}
    restart: unless-stopped
    logging: *json-file-logging
    # No published host port: the database is reachable only on the internal network.

  gotenberg:
    # PDF rendering (Chromium) for the backend; internal network only, keeps no data.
    image: gotenberg/gotenberg:8@sha256:aa552cc115db488e0f0bcb07446aac13071ad51505b642c84bb894500907b42b
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 5
    cpus: ${GOTENBERG_CPUS:-0}
    # Chromium PDF rendering: measured ~188 MiB peak rendering a real offer template, so
    # 448m keeps ~2.4x headroom. Freed budget goes to backup-worker (closer to its limit).
    mem_limit: ${GOTENBERG_MEM_LIMIT:-448m}
    restart: unless-stopped
    logging: *json-file-logging

  backend:
    # API server (Java): business logic and web jobs; uploads, PDFs and the GeoIP database live in volumes.
    image: ghcr.io/lesisty7/quotenode/quotenode-api@sha256:0ed701f952c27ea0e1db168a00de75facb231f7c6d5f6b4f0f3b77ef98d4f140
    # Time Docker gives the backend between SIGTERM and SIGKILL. 60 s = the SMTP handoff hard limit
    # (mail.handoff.deadline-ms + end-of-data-allowance-ms = 25 s) + 5 s to record its outcome + the HTTP
    # graceful drain phase (30 s). A ceiling, not a delay: an idle backend exits in seconds.
    stop_grace_period: 60s
    depends_on:
      postgres:
        condition: service_healthy
      gotenberg:
        condition: service_healthy
    env_file: stack.env
    environment:
      SPRING_PROFILES_ACTIVE: prod
      JOBS_MODE: web
      # The caddy edge below is the backend's only HTTP peer, so it is the trusted proxy by default (security
      # finding #80); without it every request would appear to come from the edge container. Override only
      # for a custom proxy chain.
      SECURITY_TRUSTED_PROXIES: ${SECURITY_TRUSTED_PROXIES:-caddy}
      BACKUP_LOCAL_DIR: /app/data/backups
    volumes:
      - backend_uploads:/app/data/uploads
      - backend_pdfs:/app/data/pdfs
      - backend_backups:/app/data/backups
      - backend_geoip:/app/data/geoip
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 45s
    cpus: ${BACKEND_CPUS:-0}
    # Heap auto-scales to 70% of this limit (MaxRAMPercentage); default 1536m → ~1 GB heap,
    # fits the 4 GB-VM profile. Raise BACKEND_MEM_LIMIT for heavier load (see resource docs).
    mem_limit: ${BACKEND_MEM_LIMIT:-1536m}
    restart: unless-stopped
    logging: *json-file-logging

  backup-worker:
    # Scheduled backups: same image as the backend, JOBS_MODE=backup-only; archives go to the backups volume.
    image: ghcr.io/lesisty7/quotenode/quotenode-api@sha256:0ed701f952c27ea0e1db168a00de75facb231f7c6d5f6b4f0f3b77ef98d4f140
    depends_on:
      postgres:
        condition: service_healthy
      backend:
        condition: service_healthy
    env_file: stack.env
    environment:
      SPRING_PROFILES_ACTIVE: prod
      JOBS_MODE: backup-only
      BACKUP_LOCAL_DIR: /app/data/backups
      # Worker runs pg_dump in a shell (out-of-JVM) and never does web/import heap work,
      # so it needs little heap. On this small 768m container the ~350 MB fixed non-heap
      # cost makes the global MaxRAMPercentage=70 (538 MB heap → 888 MB total) unsafe.
      # Cap heap lower so heap_max + non-heap stays well under the limit.
      JAVA_OPTS: "-XX:MaxRAMPercentage=45.0 -XX:+UseG1GC -XX:G1PeriodicGCInterval=300000"
    volumes:
      - backend_uploads:/app/data/uploads
      - backend_pdfs:/app/data/pdfs
      - backend_backups:/app/data/backups
      - backend_geoip:/app/data/geoip
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
      interval: 15s
      timeout: 10s
      retries: 5
      # JVM boots a large Spring context (CPU-bound); the grace window must outlast
      # cold start to avoid a false-unhealthy restart loop under load.
      start_period: 300s
    # CPU is a ceiling (idle cost ~0); give the worker enough to boot fast and to run
    # pg_dump + age-encryption/compression bursts without throttling. Idles near zero CPU.
    cpus: ${WORKER_CPUS:-0}
    # Default 832m: measured backup peak (JVM idle + pg_dump + tar + gzip) is ~607 MiB, so
    # this keeps a ~27% margin. Do NOT set WORKER_MEM_LIMIT below ~768m or backups risk OOM.
    mem_limit: ${WORKER_MEM_LIMIT:-832m}
    restart: unless-stopped
    logging: *json-file-logging

  frontend:
    # Web UI: the static build served by Caddy, reached through the caddy entry point.
    image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:80/"]
      interval: 15s
      timeout: 5s
      retries: 3
    cpus: ${FRONTEND_CPUS:-0}
    # Prod frontend is Caddy serving the static build (measured ~46 MiB peak under load); 96m is ample.
    # CPU is a free ceiling, so headroom for concurrent connections costs nothing at idle.
    mem_limit: ${FRONTEND_MEM_LIMIT:-96m}
    restart: unless-stopped
    logging: *json-file-logging

  caddy:
    # Entry point: the only published ports (HTTP_PORT/HTTPS_PORT, default 80/443), TLS for DOMAIN, routing, security headers.
    image: ghcr.io/lesisty7/quotenode/quotenode-frontend@sha256:40a1cdc4fca5cebd9f43d34daef4ba059a88000ff08af821e199566f07260606
    # The released frontend image carries the edge config (frontend/caddy/Caddyfile.edge) and the Cloudflare
    # ranges, so the edge needs no host files. Same image digest as the frontend service.
    command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile.edge", "--adapter", "caddyfile"]
    depends_on:
      backend:
        condition: service_healthy
      frontend:
        condition: service_healthy
    environment:
      # Site address for frontend/caddy/Caddyfile.edge: a public host name enables automatic HTTPS; ":80" or
      # "http://host" serves plain HTTP on a trusted LAN. Required: an empty site address is not a valid config.
      DOMAIN: ${DOMAIN:?set DOMAIN to the public host name, or :80 for plain HTTP on a LAN}
      # Optional: address(es) of an operator reverse proxy in front of this edge (default: trust none).
      EDGE_TRUSTED_PROXIES: ${EDGE_TRUSTED_PROXIES:-}
    ports:
      - "${HTTP_PORT:-80}:80"
      - "${HTTPS_PORT:-443}:443"
      - "${HTTPS_PORT:-443}:443/udp"      # HTTP/3 (QUIC)
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    # caddy is the single ingress (TLS, HTTP/3/QUIC, proxying all traffic); give it
    # cores for handshake/encryption bursts under load. CPU idle cost ~0.
    cpus: ${CADDY_CPUS:-0}
    # 96m fits the 4 GB / light-traffic profile. Under heavy TLS load (many concurrent
    # handshakes, large QUIC buffers) raise CADDY_MEM_LIMIT — this ingress is untested at scale.
    mem_limit: ${CADDY_MEM_LIMIT:-96m}
    restart: unless-stopped
    logging: *json-file-logging

# Persistent data: database, uploads, generated PDFs, local backup archives, GeoIP database and the entry
# point's certificates/config. Docker prefixes each name with the Compose project name (by default the
# directory name), so keep the project name across upgrades.
volumes:
  postgres_data:
  backend_uploads:
  backend_pdfs:
  backend_backups:
  backend_geoip:
  caddy_data:
  caddy_config:

Sous Environment variables, passez en Advanced mode et collez les réglages de l’étape 1.

Ne collez pas et ne téléversez pas le fichier téléchargé dans Portainer. Le fichier de version inchangé lit .env, que Portainer ne crée pas ; le déploiement s’arrête avec env file … .env not found.

Installation basée sur un dossier (SSH, ou une interface NAS qui lance Compose depuis un dossier que vous gérez) : téléchargez le paquet de version comme dans le guide Linux LAN, gardez son docker-compose.yml inchangé (télécharger) et enregistrez les réglages dans un fichier .env dans le même dossier.

Problème connu dans v0.9.5-alpha : son env.example contient une ligne d’exemple active, BACKUP_RCLONE_REMOTE=s3:mybucket/backups, qui fait échouer chaque sauvegarde. Les réglages de ce guide ne la contiennent pas ; si vous copiez des réglages depuis env.example, omettez cette ligne (détails).

DOMAIN détermine comment le point d’entrée caddy sert QuoteNode : un nom d’hôte public (crm.example.com) obtient automatiquement un certificat HTTPS ; http://<adresse> ou :80 sert du HTTP simple sur un réseau de confiance. Si les ports 80/443 sont déjà utilisés, définissez HTTP_PORT / HTTPS_PORT dans les réglages. Derrière votre propre reverse proxy, définissez DOMAIN=:80 et indiquez l’adresse du proxy dans EDGE_TRUSTED_PROXIES.

Conflits de ports : si votre NAS utilise déjà le port 80 ou 443, définissez HTTP_PORT=8080 (et HTTPS_PORT) dans les réglages et utilisez ce port dans CORS_ALLOWED_ORIGINS (par ex. http://nas.local:8080).

Étape 3 — Déployer et vérifier

Dans Portainer, cliquez sur Deploy the stack, puis ouvrez les journaux du conteneur backend. Dans une installation basée sur un dossier, exécutez :

docker compose up -d
docker compose logs -f backend

Attendez de voir Started QuoteNodeApplication.

Étape 4 — Ouvrir QuoteNode

Naviguez vers votre URL NAS (ex. http://nas.local ou https://crm.example.com).

Identifiants de connexion par défaut :

Champ Valeur
Email [email protected]
Mot de passe Admin123!

Changez le mot de passe par défaut immédiatement après la connexion.

Checklist de persistance

Vérifiez que ces éléments sont mappés sur des volumes persistants (pas sur un stockage éphémère du conteneur) :

Donnée Pourquoi c’est important
PostgreSQL data Base de données applicative principale
Uploads Images produit, logos, pièces jointes
Generated PDFs Fichiers d’offres et de documents
Backups Archives de backup locales (si le worker de backup est activé)
GeoIP database Données optionnelles de contrôle d’accès

Qu’est-ce qui tourne ?

Conteneur Rôle Utilisation RAM
postgres Stocke toutes vos données ~256 Mo
backend Serveur API Java, logique métier ~512 Mo – 1,5 Go
gotenberg Convertit HTML en PDF (basé sur Chromium) ~200 Mo
frontend Sert l’interface web ~20 Mo
backup-worker Exécute les sauvegardes planifiées (même image que le backend) ~300 MB
caddy Point d’entrée : certificats HTTPS, routage, en-têtes de sécurité ~20 MB

Total : environ 1,5 – 2,5 Go de RAM.

Sécuriser vos secrets

Secret Ce qu’il protège En cas de perte
DB_PASSWORD Accès à la base de données Récupérable — réinitialiser via les outils admin PostgreSQL
DB_ENCRYPTION_KEY Codes MFA, mot de passe SMTP, clé API FX, champs PII Définitivement illisible
TIMING_TOKEN_SECRET Tokens anti-timing-attack Régénérer — les utilisateurs se ré-authentifient
PUBLIC_LINK_PASSWORD_SESSION_SECRET Sessions de liens publics Régénérer — les sessions actives expirent

Conservez une copie des réglages (.env ou variables d’environnement de la stack) en dehors du NAS. Une défaillance NAS qui détruit votre seule copie de DB_ENCRYPTION_KEY rend les données chiffrées irrécupérables.

Étapes suivantes recommandées

  • Configurez des sauvegardes dans un emplacement en dehors du NAS.
  • Testez la génération de PDF après le premier déploiement.
  • Si vous voulez un chemin de déploiement public plus fluide, envisagez Coolify sur un VPS.

Last reviewed: Recently