Installation
Risoluzione problemi installazione
Problemi comuni di deployment di QuoteNode relativi a link, CORS, SMTP, DNS, health check Docker e segreti persi.
Risoluzione problemi installazione
La maggior parte dei problemi di installazione derivano da mismatch di URL, SMTP mancante, porte bloccate o segreti persi. Inizia dall’URL visibile nel tuo browser e procedi verso l’interno.
I link pubblici si aprono sul mio computer ma non per i clienti
Controlla quale URL è stato copiato o inviato.
http://localhost/...funziona solo sulla stessa macchina.http://192.168.x.x/...funziona solo nella LAN o VPN. La VPN può essere un ponte valido per i team interni, ma non è una strategia di link cliente pubblico.https://crm.example.com/...funziona pubblicamente solo quando DNS, HTTPS, proxy e firewall sono corretti.
Per i link cliente pubblici, effettua il deploy con un dominio pubblico usando Server pubblico Ubuntu o Coolify.
Il browser mostra errori CORS
CORS_ALLOWED_ORIGINS deve corrispondere esattamente all’origine del browser. Queste sono origini diverse:
https://crm.example.com
https://www.crm.example.com
http://crm.example.com
https://crm.example.com:8443
Correggi la variabile, ridistribuisci e riprova in una nuova sessione browser.
L’email non viene consegnata
Controlla prima questi elementi:
SMTP_HOSTeSMTP_PORTsono corretti.SMTP_AUTH=truequando il tuo provider richiede autenticazione.SMTP_STARTTLS=trueper la porta 587.SMTP_USERNAMEeSMTP_PASSWORDsono validi.- Il provider consente l’accesso SMTP/app-password.
NOTIFICATIONS_EMAIL_ENABLED=truequando le email di notifica devono essere inviate.
La condivisione manuale del link pubblico e il download PDF possono comunque funzionare senza SMTP.
I link delle preferenze di notifica sono errati
Imposta:
NOTIFICATIONS_PUBLIC_PREFERENCES_URL=https://<frontend-domain>/notifications/preferences
Non lasciarlo che punti a localhost in un deployment pubblico.
I container Docker sono unhealthy
Esegui:
docker compose ps
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f gotenberg
Cause comuni:
- PostgreSQL si sta ancora avviando.
DB_PASSWORDdifferisce tra PostgreSQL e backend.DB_ENCRYPTION_KEYè mancante o malformato.- Gotenberg non è healthy, quindi la generazione PDF non può avviarsi.
- Le porte host 80/443 sono già usate da un altro servizio.
Dopo aver corretto la configurazione, riavvia il servizio interessato o lo stack:
docker compose restart backend
docker compose up -d
Il mio IP è bloccato — non riesco ad accedere
Ci sono due cause comuni: la lista IP consentiti ha bloccato l’accesso oppure la protezione anti-brute-force ha bloccato il tuo IP dopo troppi tentativi di accesso falliti.
Blocco dalla lista IP consentiti
QuoteNode ha una lista di IP consentiti configurabile in Impostazioni > Sicurezza > Lista IP consentiti (o tramite la variabile SECURITY_IP_WHITELIST). Quando è attiva, protegge l’intera superficie di amministrazione — comprese le schermate di accesso e 2FA — quindi un indirizzo escluso riceve 403 IP_NOT_ALLOWED e non può nemmeno accedere. (Gli health check e i link pubblici alle offerte per i clienti restano raggiungibili.)
Prima attendi due minuti se l’hai appena modificata nell’app. Un’attivazione o un restringimento nell’app è protetto dal blocco: deve essere riconfermato dalla tua sessione entro una breve finestra (120 s predefiniti) e, se il tuo indirizzo attuale non è incluso, la lista torna automaticamente a quella precedente. Un autoblocco accidentale dalla schermata Sicurezza si risolve quindi da solo — attendi e riprova. (Questa rete di sicurezza non si applica alla variabile SECURITY_IP_WHITELIST, che viene applicata all’avvio.)
Recupero (override dell’operatore): se sei ancora bloccato — es. una SECURITY_IP_WHITELIST dell’operatore, una lista memorizzata non valida o non puoi attendere — aggiungi questa variabile all’ambiente del container backend (.env o sezione environment: nel docker-compose) e riavvia:
SECURITY_IP_WHITELIST_FORCE_DISABLE=true
docker compose restart backend
Questo disabilita completamente la lista consentiti e permette l’accesso. Una volta dentro, correggi la lista in Impostazioni > Sicurezza, poi rimuovi SECURITY_IP_WHITELIST_FORCE_DISABLE e riavvia di nuovo.
Non lasciare
SECURITY_IP_WHITELIST_FORCE_DISABLE=truein produzione. Disabilita l’intero controllo accessi per IP.
Blocco dalla protezione anti-brute-force
QuoteNode blocca gli indirizzi IP dopo troppi tentativi di accesso falliti in un’ora:
| Soglia | Fallimenti in 1 h | Effetto |
|---|---|---|
| Soft | 10 | Ritardo progressivo (1 s per tentativo aggiuntivo, max. 30 s) |
| Blocco temporaneo | 20 | Bloccato per 5 minuti |
| Blocco permanente | 50 | Bloccato fino allo sblocco manuale |
Come sbloccare:
- Pannello admin — un altro amministratore apre Impostazioni > Dashboard Sicurezza > IP bloccati e clicca Sblocca.
- Attendere — i blocchi temporanei scadono dopo
SECURITY_TEMP_LOCK_MINUTES(5 minuti predefiniti). I blocchi permanenti non scadono da soli. - Alzare temporaneamente le soglie se sei bloccato e nessun altro admin può aiutare. Aggiungi al
.env, riavvia, accedi, poi rimuovi:
SECURITY_MAX_FAILED_LOGINS_SOFT=9999
SECURITY_MAX_FAILED_LOGINS_TEMP_LOCK=9999
SECURITY_MAX_FAILED_LOGINS_HARD_LOCK=9999
Ripristina i valori predefiniti dopo aver recuperato l’accesso.
Blocchi IP dei link pubblici
Il rate limiter dei link pubblici blocca gli indirizzi IP che superano i limiti di tentativi. Questi blocchi sono in memoria e scadono dopo PUBLIC_RATE_LIMIT_IP_BLOCK_MINUTES (60 minuti predefiniti) o dopo un riavvio del backend.
L’aggiornamento si ferma con “cannot enforce offer-section ownership”
Dopo un aggiornamento il backend non si avvia e docker compose logs backend mostra:
Migration V145 cannot enforce offer-section ownership: 3 offer_items row(s) point at a
section of another offer or tenant.
Che cosa significa
Un’offerta è composta da sezioni (“Hardware”, “Servizi”) e dalle righe collocate al loro interno. Questa versione rende vincolante quel collegamento: una riga può stare solo in una sezione della propria offerta. Alcune righe vecchie nel vostro database violano la regola, quindi l’aggiornamento si ferma e chiede a voi di decidere, invece di riscrivere le offerte da solo.
Nulla è rotto e il rifiuto stesso non ha modificato nulla. Questa migrazione è stata annullata completamente: nessuna modifica applicata a metà e nulla da riparare nella cronologia delle migrazioni. Tenete però presente che migrazioni precedenti dello stesso aggiornamento potrebbero essere già state applicate: il database si trova appena prima di questo passo, non necessariamente alla versione di partenza. Ciò significa anche che i container precedenti potrebbero funzionarci o no, a seconda di che cosa quelle migrazioni hanno cambiato: non contate sul tornare alle vecchie immagini come via d’uscita.
Scegliete che cosa deve accadere
| Opzione | Che cosa fa | Che cosa cambia per voi |
|---|---|---|
| Controlla il problema (predefinito) | Elenca le offerte e le righe interessate. | Nulla. Sola lettura. |
| Scollega le assegnazioni errate | Cancella la sezione solo di quelle righe. | Le righe restano nella loro offerta con lo stesso prezzo, quantità e descrizione. Perdono l’intestazione di sezione e appaiono come righe non raggruppate. |
| Scegli una sezione per ogni riga | Chiede riga per riga e propone solo sezioni della sua stessa offerta. | Mantenete il raggruppamento desiderato. Le righe saltate restano irrisolte e l’aggiornamento si fermerà di nuovo. |
Le offerte già inviate ai clienti continuano a mostrare esattamente ciò che il cliente ha visto: il contenuto di un’offerta inviata viene congelato al momento dell’invio, quindi né il link pubblico né il PDF cambiano.
Individuate prima l’installazione giusta (Coolify o più stack su un server)
Ogni comando qui sotto deve colpire la vostra installazione. Un server può ospitarne diverse, e più di una può avere un servizio chiamato backend: elencate quindi prima le candidate — ogni riga è un’installazione, con il nome del progetto e il file Compose da cui è stata avviata:
docker inspect $(docker ps -aq --filter "label=com.docker.compose.service=backend" \
--filter "label=com.docker.compose.oneoff=False") \
--format '{{index .Config.Labels "com.docker.compose.project"}} | {{index .Config.Labels "com.docker.compose.project.config_files"}}'
Se ottenete più di una riga, scegliete la vostra; non tirate a indovinare. Poi eseguite ogni comando nominando esplicitamente quel progetto e quel file, invece di un docker compose secco, così da non agire sull’installazione sbagliata:
docker compose -p <project> -f <compose-file> run --rm backend --repair-offer-sections
Sostituite <project> e <compose-file> con i valori della vostra riga e teneteli in ogni comando che segue: è questo che fa agire ciascuno sulla vostra installazione e non su un’altra. Se la riga mostra più file separati da virgole, ripetete -f per ciascuno, in quell’ordine. Se l’elenco è vuoto, i container non sono stati avviati con Docker Compose: usate la directory indicata dal vostro provider o dalla guida di installazione, oppure chiedete a chi ha configurato il server. Su Coolify fatelo in un terminale sul server, non sul container: il terminale del container richiede un backend in esecuzione, e il vostro non lo è. (I messaggi dello strumento mostrano un docker compose … breve senza queste opzioni, per restare leggibili: quando li copiate, aggiungete i vostri -p e -f.)
Prima il backup
PostgreSQL resta in funzione anche quando il backend non parte, quindi potete fare il backup senza l’applicazione. Il comando legge nome del database e utente dal container stesso, quindi funziona comunque li abbiate chiamati, e nessuna password compare a schermo:
docker compose -p <project> -f <compose-file> exec -T postgres sh -c \
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom -f /tmp/before-repair.dump \
&& pg_restore --list /tmp/before-repair.dump | head -3'
docker compose -p <project> -f <compose-file> cp postgres:/tmp/before-repair.dump ./before-repair.dump
ls -lh before-repair.dump
La riga pg_restore --list stampa le prime voci dell’archivio: se le vedete e ls -lh mostra un file non vuoto, il backup è leggibile. Conservate before-repair.dump accanto al vostro docker-compose.yml: si trova sul disco del server, non dentro un container, quindi sopravvive a tutto ciò che segue.
Controllare il problema
docker compose -p <project> -f <compose-file> run --rm backend --repair-offer-sections
Questo comando si limita a leggere. Mostra le offerte interessate, che cosa non va in ogni riga e le opzioni:
Found 3 offer item(s) in 2 offer(s) assigned to a section that does not belong to that offer.
Offer OF-2026-014 [DRAFT]
item: Mounting frame
section: section "Installation" — section belongs to another offer
Anteprima, poi applicazione
Senza --apply=true ogni comando è un’anteprima: mostra esattamente che cosa farebbe e termina senza aver cambiato nulla, in qualsiasi terminale.
docker compose -p <project> -f <compose-file> run --rm backend --repair-offer-sections --plan=unlink
Aggiungete --apply=true --acknowledge-backup=true per eseguirlo. È l’unica forma che scrive:
docker compose -p <project> -f <compose-file> run --rm backend --repair-offer-sections --plan=unlink --apply=true --acknowledge-backup=true
Per decidere voi stessi ogni riga usate --plan=choose. Lo strumento pone una domanda per riga ed elenca le sezioni di quell’offerta, così non digitate mai un identificativo del database. Senza --apply=true continua solo a mostrarvi il piano:
docker compose -p <project> -f <compose-file> run --rm backend --repair-offer-sections --plan=choose --apply=true --acknowledge-backup=true
Completare l’aggiornamento
Quando lo strumento segnala che nessuna riga punta più a una sezione sbagliata:
docker compose -p <project> -f <compose-file> up -d
La migrazione bloccata non ha lasciato nulla in sospeso, quindi al prossimo avvio viene semplicemente eseguita. Non serve alcun comando flyway repair o baseline — e non dovete usarlo, perché nasconderebbe il problema dei dati invece di risolverlo.
Verificate come sempre: aprite /health, poi controllate la nuova versione in Admin > Licenza.
Se dice che i dati sono cambiati dopo la vostra anteprima
Lo strumento applica il piano che avete approvato, non ciò che trova nel momento in cui scrive. Se durante la conferma è comparsa un’assegnazione errata, qualcosa si è spostato o una sezione che avevate scelto è stata eliminata, rifiuta l’intera riparazione e non cambia nulla. Rieseguite il controllo per vedere lo stato attuale, poi rifate la vostra scelta.
Se dice che la riparazione è stata salvata — o che non ha potuto confermarlo
La riparazione avviene in un’unica transazione, quindi o viene salvata per intero o non viene salvata affatto. Due finali richiedono attenzione. Se fallisce un passo successivo — la scrittura del resoconto o la rilettura che conferma il risultato — lo strumento mantiene la parola SAVED nel messaggio: la riparazione ha funzionato, quindi non ripristinate il backup. Se invece compare UNCONFIRMED, il database non ha mai risposto e nessuno può dire come sia andata; non ripristinate il backup e non ripetete la riparazione. In entrambi i casi rieseguite il controllo in sola lettura: se segnala che non c’è nulla da riparare, avete finito e potete avviare lo stack; se elenca ancora le stesse righe, non è stato salvato nulla e potete rifare la vostra scelta.
docker compose -p <project> -f <compose-file> run --rm backend --repair-offer-sections
Una cosa da leggere con attenzione: se avete usato Scegli una sezione per ogni riga e ne avete saltate alcune, quelle righe saltate continueranno a comparire nel controllo. È previsto e non significa che la riparazione sia fallita. Confrontate l’elenco con le righe che avevate davvero scelto di sistemare: se restano solo quelle saltate, la riparazione ha funzionato e ciò che resta è la decisione che avete rimandato — rieseguite lo strumento per esse quando volete. Se compaiono ancora righe che avevate scelto di sistemare, non è stato salvato nulla.
Che cosa è stato cambiato e cosa fare se continua a fallire
Ogni esecuzione che modifica qualcosa scrive gli identificativi di riga, offerta e sezione interessate in /app/data/backups/ del volume del backend. Non contiene dati dei clienti e può essere allegato a una richiesta di assistenza. Se il file non può essere scritto, lo strumento stampa lo stesso resoconto a schermo: copiatelo prima di chiudere il terminale:
docker compose -p <project> -f <compose-file> run --rm --entrypoint sh backend \
-c 'ls -1 /app/data/backups/offer-section-repair-*.json'
Se l’aggiornamento si ferma ancora:
- eseguite di nuovo il controllo: le righe saltate compaiono ancora;
- se il messaggio nomina un’altra migrazione, questo strumento non la riguarda. Ripara solo le assegnazioni di sezione delle righe d’offerta;
- se il messaggio è invariato e il controllo non segnala nulla da riparare, segnalate il problema allegando il log del backend e il resoconto qui sopra.
Il certificato HTTPS non viene emesso
Per i deployment pubblici:
- Il DNS deve puntare al server di deployment.
- Le porte 80 e 443 devono essere raggiungibili da internet.
- Solo un proxy dovrebbe terminare HTTPS per lo stesso dominio.
- In Coolify, assegna il dominio al servizio
frontend.
Ho perso .env
Se hai perso .env, fermati e valuta prima di ruotare i valori.
DB_PASSWORDpuò solitamente essere reimpostato con accesso amministrativo PostgreSQL.TIMING_TOKEN_SECRETePUBLIC_LINK_PASSWORD_SESSION_SECRETpossono essere rigenerati, ma le sessioni/token esistenti potrebbero scadere.DB_ENCRYPTION_KEYè critico. Senza la chiave originale, i valori cifrati non possono essere decifrati.- Se la cifratura PII e abilitata, i campi cliente/contatto cifrati dipendono da
DB_ENCRYPTION_KEY.
Ripristina .env dal backup ogniqualvolta possibile.
Ripristino se .env è perso ma esiste un backup del database
- Crea un nuovo
.envcon nuovi segreti. - Ripristina il database dal tuo backup (
pg_dump/pg_restore). - Tutti i dati di business (clienti, offerte, prodotti, utenti) saranno completamente accessibili.
- Solo pochi campi vengono persi: codici MFA (gli utenti si re-iscrivono), password SMTP (reinserire nelle impostazioni), chiave API FX (reinserire). Se
ENCRYPT_PII=trueera abilitato, anche i dati cliente cifrati vengono persi.
Se i tuoi backup sono cifrati con age (impostazione predefinita), hai bisogno del file di identità age dal tuo kit di recupero per decifrarli. Consulta la guida Backup e Ripristino.
Password admin dimenticata
Usa un percorso di recupero non distruttivo:
- Se SMTP è configurato, usa la pagina di accesso: Password dimenticata?
- Se un altro amministratore può ancora accedere, apri Utenti nel pannello admin e reimposta la password.
- Se nessun amministratore riesce ad accedere e il flusso di reset via email non è disponibile, ripristina l’accesso da un backup funzionante o usa la tua procedura di manutenzione interna.
docker compose down -vnon e una procedura di reset della password. Elimina tutti i dati — utenti, offerte, clienti, impostazioni e backup. Usalo solo per ambienti di test usa-e-getta.
La verifica della licenza fallisce
Per i deployment reali, mantieni:
LICENSE_SKIP_SIGNATURE_CHECK=false
Se una licenza valida fallisce comunque, controlla che il deployment abbia la versione app attesa e un orologio di sistema sincronizzato con NTP. Conferma che la licenza corrisponda all’edizione/versione distribuita, quindi contatta il supporto o sostituisci il file di licenza tramite il percorso operatore documentato. Non impostare il bypass della firma a true come workaround di produzione.