Installation
Aggiornamento di QuoteNode
Come verificare le nuove versioni, aggiornare al pacchetto di una nuova release e tornare indietro in sicurezza.
Aggiornare QuoteNode
QuoteNode verifica automaticamente la disponibilità di nuove release. Quando è disponibile una versione più recente, il pannello di amministrazione mostra una notifica con l’etichetta della release e un link alle note di rilascio.
Ogni release è pubblicata come pacchetto di distribuzione: docker-compose.yml (e docker-compose.coolify.yml) con le immagini della release fissate tramite digest, env.example, release.json e gli strumenti di operator/. Aggiornare significa sostituire il file Compose con quello del pacchetto successivo. La release attuale è v0.9.8-alpha (pacchetto).
Il modo di aggiornare dipende da come è stato installato QuoteNode. La lista di controllo e i comandi seguenti valgono per una directory di installazione (server, LAN, portatile o una cartella sul NAS con docker-compose.yml e .env). Uno stack Portainer non ha questa directory: vai a Stack Portainer. Coolify: vai a Coolify.
Problema noto in v0.9.5-alpha: destinazione di backup di esempio
Il file env.example di v0.9.5-alpha contiene una riga di esempio attiva, BACKUP_RCLONE_REMOTE=s3:mybucket/backups. Quella destinazione non è configurata: se la riga arriva nelle tue impostazioni, ogni backup fallisce e un caricamento fallito non lascia alcuna copia locale di quel backup. Le impostazioni di queste guide non la contengono mai; controlla l’installazione se hai creato .env da env.example o vi hai aggiunto impostazioni da quel file durante un aggiornamento.
In una directory di installazione, commenta esattamente quella riga di esempio (una destinazione reale resta invariata) e ricrea i container:
# Run in Bash (for example Linux, Git Bash or WSL); other shells can edit the exact placeholder manually.
(
set -eu
umask 077
temporary_env="$(mktemp .env.backup-remote.XXXXXX)"
trap 'rm -f "$temporary_env"' EXIT
carriage_return="$(printf '\r')"
# Match only the published placeholder, with either LF or Windows CRLF line endings.
sample_pattern="^BACKUP_RCLONE_REMOTE=s3:mybucket/backups${carriage_return}\{0,1\}$"
sed "s|${sample_pattern}|# &|" .env > "$temporary_env"
if LC_ALL=C grep -Eq "^BACKUP_RCLONE_REMOTE=s3:mybucket/backups${carriage_return}?$" "$temporary_env"; then
echo "Sample backup remote is still active; settings were not replaced." >&2
exit 1
else
match_status=$?
[ "$match_status" -eq 1 ] || exit "$match_status"
fi
# Keep real remote settings unchanged; publish a changed file atomically with private permissions.
if ! cmp -s .env "$temporary_env"; then
mv "$temporary_env" .env
fi
docker compose up -d
)
In Portainer, elimina la variabile dello stack BACKUP_RCLONE_REMOTE se ha quel valore di esempio, poi aggiorna lo stack. La definizione Coolify di v0.9.5-alpha non passa questa variabile all’applicazione; elimina il valore di esempio dalla risorsa prima di aggiornare a una release successiva.
I backup restano allora nel volume dei backup. Avvia un backup dal pannello di amministrazione (Admin → Security → Backup), verifica che termini correttamente e predisponi una copia fuori dal server: una destinazione rclone configurata o copie regolari degli archivi su un’altra macchina (Backup e ripristino).
Passare a PostgreSQL 18
I file Compose usano già postgres:18-alpine, quindi le nuove installazioni non richiedono nulla. Se una versione alpha precedente girava su PostgreSQL 16, migra i dati una volta — un server PostgreSQL 18 non si avvia su una directory dati più vecchia e l’applicazione non la riscrive mai automaticamente. Segui la procedura dedicata e sicura: Migrare i dati a PostgreSQL 18 (da riga di comando o solo da interfaccia, per piattaforma).
Come funziona il controllo della versione
L’interfaccia di amministrazione scarica https://quotenode.dev/releases/latest.json all’accesso di un amministratore. È una richiesta GET di sola lettura, senza autenticazione; nessun dato della tua istanza viene inviato. Se il manifest non è raggiungibile, il controllo fallisce in silenzio e il pannello funziona normalmente.
Il controllo e l’URL del manifest fanno parte dell’interfaccia web già costruita. Impostare VITE_RELEASE_CHECK_ENABLED o VITE_RELEASE_MANIFEST_URL in .env o in Coolify non ha effetto su una release; questi valori valgono solo se costruisci tu stesso l’immagine del frontend dai sorgenti. Per evitare che un’installazione contatti quotenode.dev, blocca quell’indirizzo nella tua rete.
Lista di controllo prima dell’aggiornamento
Per una directory di installazione; Portainer e Coolify hanno sezioni proprie più sotto.
- Annota la release attuale:
release.jsonnella directory di installazione, oppure Admin > Licenza. - Copia
.envfuori dal server — contiene segreti che non si possono rigenerare (soprattuttoDB_ENCRYPTION_KEY). - Scarica il nuovo pacchetto accanto all’installazione. Non cambia nulla di ciò che è in esecuzione:
mkdir -p ../next && cd ../next
curl -fsSLO https://quotenode.dev/releases/v0.9.8-alpha/quotenode-deploy-v0.9.8-alpha.tar.gz
tar -xzf quotenode-deploy-v0.9.8-alpha.tar.gz --strip-components=1
cd -
# Impostazioni aggiunte o rimosse dalla nuova release? Confronta il tuo .env con il nuovo modello.
diff <(grep -o '^[A-Z_]*=' .env | sort) <(grep -o '^[A-Z_]*=' ../next/env.example | sort)
- Aggiungi a
.envle impostazioni richieste dalla nuova release (il confronto qui sopra le elenca; installazioni precedenti ai pacchetti di release: vedi Installazioni precedenti ai pacchetti di release). L’installazione in esecuzione non le usa ancora. - Crea il checkpoint di aggiornamento e un backup verificato con lo strumento del nuovo pacchetto. Non cambia nulla di ciò che è in esecuzione e si ferma al primo errore:
BACKUP_ARCHIVE_ENCRYPTION_MODE=AGE_RECIPIENT BACKUP_AGE_RECIPIENT=age1... \
../next/operator/upgrade.sh prepare --export-to ../quotenode-backups
Il checkpoint (in .quotenode-upgrade/, usato solo da questa installazione) registra docker-compose.yml, .env e release.json installati, l’identità delle immagini in esecuzione e lo stato dello schema del database. Il backup viene creato con gli strumenti della nuova release sul progetto Compose e sui volumi di questa installazione, resta locale in questa esecuzione anche se sono configurati backup remoti e viene conservato in ../quotenode-backups solo quando il suo SHA-256 coincide. Copia quella directory fuori dal server prima di proseguire. Il destinatario age si trova nel tuo Recovery Kit (Backup e ripristino).
- Se i backup pianificati sono attivi, verifica anche che l’ultimo sia riuscito (pannello di amministrazione).
Procedura di aggiornamento
Docker Compose (server, LAN, portatile, cartella sul NAS)
Dopo la lista di controllo, esegui nella directory di installazione, così il progetto Compose e i suoi volumi restano gli stessi:
../next/operator/upgrade.sh apply
upgrade.sh apply scarica prima tutte le immagini della nuova release e si ferma al primo errore: se un download fallisce, l’installazione continua a funzionare senza modifiche. Solo dopo installa docker-compose.yml, release.json, env.example e operator/ dal pacchetto, avvia la nuova versione e attende che sia in salute. Le migrazioni del database vengono eseguite automaticamente all’avvio del nuovo backend. È richiesto Docker Compose 2.17 o successivo.
Stack Portainer
upgrade.sh richiede una directory di installazione, quindi uno stack Portainer si aggiorna in Portainer:
- Verifica che l’ultimo backup pianificato sia riuscito (pannello di amministrazione), proteggi i volumi dello stack con lo strumento di snapshot o di backup della tua piattaforma e salva una copia della definizione attuale dello stack e delle sue variabili d’ambiente.
- Apri l’Editor dello stack e sostituisci la definizione con il blocco seguente: l’adattamento per Portainer del
docker-compose.ymldella release, lo stesso blocco di Installare con Portainer. Non incollare il file originale del pacchetto: legge.env, che Portainer non ha. - Mantieni il nome dello stack e le sue variabili d’ambiente (
stack.env); aggiungi le impostazioni richieste dalla nuova release (confronta con il suoenv.example). - Fai clic su Update the stack con il download delle immagini attivo. Le migrazioni del database vengono eseguite all’avvio del nuovo backend.
# 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:
Per tornare indietro, ripristina il backup dei volumi insieme alla definizione dello stack salvata. Non avviare mai la definizione precedente su un database che la nuova release ha già migrato.
Coolify
Sostituisci la definizione Compose della risorsa con il docker-compose.coolify.yml del nuovo pacchetto (Edit Compose File) e distribuisci. Coolify scarica le nuove immagini prima di arrestare i container in esecuzione. Vedi Installazione con Coolify.
Installazioni precedenti ai pacchetti di release
Se hai installato QuoteNode con queste guide prima che esistessero i pacchetti di release (un docker-compose.yml scritto a mano con immagini :latest), aggiorna nella stessa directory con gli stessi comandi: upgrade.sh crea il backup con gli strumenti della nuova release sul progetto Compose e sui volumi della tua installazione, quindi prima non servono né una directory operator/ né un servizio di backup. Prima di prepare, aggiungi queste impostazioni a .env; l’installazione in esecuzione non le usa ancora:
DOMAINora configura il nuovo punto di ingresso (caddy): un nome host pubblico ottiene automaticamente HTTPS; per HTTP semplice in LAN o su un portatile usaDOMAIN=http://<indirizzo>(ad esempiohttp://192.168.1.20ohttp://localhost).- Se le porte 80/443 sono occupate, imposta
HTTP_PORT/HTTPS_PORTinvece di modificare il file Compose.
Il pacchetto mantiene i nomi dei volumi delle guide precedenti (postgres_data, backend_uploads, backend_pdfs), quindi i tuoi dati vengono riutilizzati. Esegui upgrade.sh dalla stessa directory (o mantieni lo stesso nome dello stack in Portainer): con un altro nome di progetto partirebbe con volumi vuoti.
Se una migrazione blocca l’aggiornamento
Una migrazione può rifiutarsi di procedere quando trova dati che non può modificare da sola. Il backend allora non si avvia e quella migrazione non cambia nulla — viene annullata completamente e non lascia nulla da riparare nella cronologia delle migrazioni. Migrazioni precedenti dello stesso aggiornamento possono essere già state applicate, quindi il database si trova subito prima del passo rifiutato e non alla versione di partenza. Non c’è fretta e non c’è perdita di dati.
Leggi il motivo nel log:
docker compose logs backend | grep -i "cannot enforce"
Per le assegnazioni delle sezioni delle offerte, l’immagine include una riparazione guidata che funziona senza backend attivo — mostra le offerte interessate e ti lascia scegliere cosa farne. Vedi Risoluzione dei problemi di installazione.
Non usare flyway repair o baseline per superare una migrazione rifiutata. Non riparano i dati, e una migrazione bloccata non lascia nulla da riparare.
Poiché migrazioni precedenti possono essere già applicate, un avvio fallito da solo non consente mai di tornare alle immagini precedenti — vedi Tornare indietro.
Verifica dopo l’aggiornamento
- Apri
/health— la risposta èUP. - Apri
/actuator/info: il suocommitShacoincide con ilcommitShadel nuovorelease.json. - Accedi e verifica la nuova versione in Admin > Licenza.
- Crea un’offerta di prova e genera un PDF.
- Apri un link pubblico per verificare l’instradamento.
- Se SMTP è configurato, invia un’e-mail di notifica di prova.
Tornare indietro
Non eseguire mai docker compose down -v — elimina tutti i dati, compreso il database.
upgrade.sh rollback torna alla definizione salvata da prepare solo quando è sicuro: il checkpoint deve appartenere a questa installazione, lo schema del database deve essere esattamente quello registrato — un avvio fallito non lo dimostra, migrazioni precedenti possono essere già confermate — e ogni immagine precedente deve essere ancora quella che era in esecuzione. Altrimenti rifiuta e non cambia nulla:
../next/operator/upgrade.sh rollback
- Consentito:
docker-compose.yml,.enverelease.jsonsalvati tornano e le immagini precedenti si avviano senza download; i servizi aggiunti dalla nuova release vengono rimossi, i volumi restano. - Rifiutato: non avviare a mano le immagini precedenti. Se una migrazione ha rifiutato, correggi nella nuova versione (vedi Se una migrazione blocca l’aggiornamento) oppure ripristina il backup precedente all’aggiornamento — database e file come un’unica unità — come descritto in Backup e ripristino. Il ripristino verifica l’archivio e la sua compatibilità prima di cambiare qualcosa e conserva una copia di sicurezza finché non ha verificato il risultato.
- Le installazioni precedenti ai pacchetti di release tornano indietro allo stesso modo. Dopo un rifiuto, ripristina il backup da
../quotenode-backupsconoperator/restore.shdi questa release; la release aggiorna i dati ripristinati al suo avvio.
Formato della versione
QuoteNode usa il versionamento semantico con un suffisso di canale:
v0.9.0-alpha— sviluppo inizialev0.9.0-beta— beta pubblicav0.9.0-rc— release candidatev0.9.0— release stabile
Un’etichetta di release non viene mai riutilizzata per altre immagini. release.json e https://quotenode.dev/releases/latest.json riportano versione, canale, etichetta, commit e digest delle immagini della release.