Installazione
BlueGem CRM è pensato per girare su un singolo server Linux con PostgreSQL locale: un'API in Go più un worker in background per workflow, email e manutenzione. Sul server arriva un artefatto binario, non il codice sorgente: non serve nessun runtime. Questa pagina copre tutto il ciclo di vita: prima installazione, aggiornamenti, backup e ripristino.
01Requisiti
- Server Linux con systemd (VPS o macchina dedicata). 2 GB di RAM sono sufficienti per partire. Le procedure sono provate su Ubuntu 24.04.
- PostgreSQL ≥ 14. Se il database sta sulla stessa macchina non devi installarlo tu: lo fa l'installer.
- Nessun runtime applicativo: il backend è un binario Go compilato staticamente. Sul server non servono Node, npm né toolchain di build. Il CSS del frontend arriva già compilato dentro l'artefatto.
- Reverse proxy con TLS davanti all'app: nginx o Caddy. Certificato e header HSTS li gestisce il proxy: vedi la sezione 05.
02Il database PostgreSQL
Con il database sulla stessa macchina non c'è niente da preparare a mano:
setup.sh installa PostgreSQL, crea il ruolo
(crm_user) e il database di sua proprietà
(crm_db) con la password che gli passi in
--db-pass, e allinea il ruolo a quella password a ogni
rilancio.
- Privilegio minimo: il ruolo dell'app non è superuser e non ha
CREATE DATABASE: gli basta possedere il suo database. (I permessi da superuser servono solo al ripristino, che gira comepostgres: vedi Backup e ripristino.) - Lo schema lo creano le migrazioni: le tabelle vivono nello schema indicato da
DB_SCHEMA(defaultpublic), creato alla prima migrazione; essendo il ruolo proprietario del database non serve fare altro. - Rotazione della password: la fonte di verità è il
.env. Se ce n'è già una, vince su--db-passe il ruolo viene riallineato a quella. Per ruotarla davvero: modifichiDB_PASSWORDnel.enve rilanci l'installer.
Con il database su una macchina separata, due comandi, uno per macchina:
# sulla macchina del database: abilita l'accesso dal SOLO indirizzo dell'app sudo bash scripts/ops/setup-db.sh --db-name crm_db --db-user crm_user \ --db-pass '<password>' --allow-host <ip-app> # sulla macchina dell'applicazione sudo /opt/crm/scripts/ops/setup.sh --skip-db --db-host <ip-db> --db-pass '<password>'
03Prima installazione
L'artefatto di rilascio si produce sulla macchina di build (dove servono Go e, solo per il CSS, Node):
scripts/ops/build-release.sh # -> dist/crm-<versione>.tar.gz + .sha256
Contiene i binari (crmgo-server,
crmgo-worker, crmgo-migrate,
crmgo-bootstrap), il frontend già compilato, le unit systemd,
gli script di esercizio e un file VERSION con il commit da cui
è stato prodotto. Le migrazioni non sono file da copiare: viaggiano dentro il binario.
Accanto all'archivio la build scrive l'impronta .sha256:
copiala insieme all'archivio.
Sul server, l'app gira come utente di sistema dedicato
(crm), senza shell, con l'installazione in
/opt/crm:
sudo mkdir -p /opt/crm sha256sum -c crm-<versione>.tar.gz.sha256 # deve dire OK sudo tar xzf crm-<versione>.tar.gz -C /opt/crm sudo /opt/crm/scripts/ops/setup.sh --db-pass '<password-db>' --admin-pass '<password-admin>'
Un comando solo. setup.sh fa, in ordine: PostgreSQL con ruolo e
database, utente di sistema e permessi, .env con i segreti
mancanti generati, migrazioni (le applica
crmgo-migrate), primo amministratore
(crmgo-bootstrap), backup pianificati con il primo giro
eseguito subito, sentinella locale, irrobustimento SSH, unit systemd
crm-go-app e crm-go-worker, avvio e
health check su /ready. Se l'archivio non corrisponde alla sua
impronta, si ferma prima di installare qualsiasi cosa.
| Opzione | Default | A cosa serve |
|---|---|---|
| --db-pass | obbligatoria | Password del ruolo applicativo, alla prima installazione. |
| --admin-pass | generata | Password del primo amministratore. Se non la passi, viene generata e stampata una volta sola: annotala. |
| --admin-user / --admin-email | admin | Identità del primo amministratore. |
| --db-host / --db-port | localhost / 5432 | Database su un'altra macchina (con --skip-db). |
| --addr | :5000 | Indirizzo di ascolto dell'app. |
| --backup-tier | standard | basic (giornaliero), standard (+ settimanale), premium (+ orario e mensile). |
| --no-restart | riavvio attivo | Non riavviare l'app a fine messa in servizio: serve a scegliere tu il momento del fermo. |
Non esiste un utente admin predefinito: il primo amministratore lo crea
crmgo-bootstrap, chiamato da
setup.sh, e deve cambiare la password al primo accesso.
Nessuna credenziale di default da ricordarsi di cambiare.
Lo script è idempotente ed è pensato per essere rilanciato: non rigenera i segreti già presenti, non ricrea l'amministratore se esiste, non sovrascrive una configurazione dei backup scritta a mano.
Finita la messa in servizio, il servizio ascolta solo su 127.0.0.1, in chiaro. Non è ancora consegnabile: servono nome a dominio, reverse proxy con certificato e HSTS (sezione 05).
04Configurazione (.env)
Tutta la configurazione vive in un unico file .env, che
lo scrive l'installer: genera i valori mancanti e conserva quelli già
presenti. Le chiavi che contano:
| Variabile | Valore | Note |
|---|---|---|
| APP_ENV | production | Attiva i controlli di sicurezza di produzione: senza segreti validi l'app rifiuta di partire. |
| GO_HTTP_ADDR | :5000 | Indirizzo e porta su cui ascolta l'app (dietro il proxy). |
| DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME | … | Credenziali del ruolo e del database (sezione 02). Il ruolo è a privilegio minimo: non serve CREATE DATABASE. |
| DB_SCHEMA | public | Lo schema in cui vivono le tabelle; viene creato alla prima migrazione. |
| AUTH_JWT_SECRET | ≥ 32 byte | L'unico segreto di firma dei token. Lo genera l'installer; in produzione l'app non parte se manca, è troppo corto o è il default di sviluppo. |
| CONNECTIONS_ENC_KEY | ≥ 32 caratteri | Chiave di cifratura dei segreti a riposo (credenziali SMTP, S3, provider di fatturazione, secret TOTP). Obbligatoria in produzione. |
| CLIENT_DIR / ATTACHMENTS_DIR | … | Frontend da servire e cartella degli allegati su disco locale. |
| TRUST_PROXY | true | Da impostare solo dopo aver messo il proxy davanti: vedi sotto. |
TRUST_PROXY si scrive true o
false, mai un numero: non è il numero di
hop. Un valore numerico è un errore di configurazione e il server non
parte, con un messaggio che dice cosa scrivere.
true è corretto soltanto con un proxy che
riscrive X-Forwarded-For come negli esempi
della sezione 05. Con un proxy che lo appende, un client può dichiarare l'indirizzo che
vuole: il limite per IP sul login smette di funzionare e l'indirizzo registrato
nell'audit è quello scelto da lui.
AUTH_JWT_SECRET e CONNECTIONS_ENC_KEY
si generano una volta sola. Rigenerarli invalida tutte le sessioni attive e rende
illeggibili i segreti già cifrati a riposo.
05Reverse proxy e TLS
Davanti all'app va un proxy che termina il TLS. Esempio minimo con nginx:
server { listen 443 ssl; server_name crm.tuodominio.it; # ssl_certificate / ssl_certificate_key gestiti da certbot o simili add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; client_max_body_size 30m; # deve superare il limite sugli allegati location / { proxy_pass http://127.0.0.1:5000; # RISCRIVE, non appende: è il presupposto di TRUST_PROXY=true proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; } }
L'header Strict-Transport-Security non lo
imposta l'applicazione: parla in chiaro con il proxy e non può sapere se davanti c'è
TLS. Va messo nella configurazione del proxy, come nell'esempio, oppure non lo mette
nessuno. Con Caddy il certificato e il rinnovo sono automatici, l'header va comunque
dichiarato.
Solo dopo aver messo il proxy davanti, imposta
TRUST_PROXY=true nel .env e
riavvia. La CSP dell'app è strict e non ammette host esterni; il CSS arriva già
compilato nell'artefatto, quindi sul server non c'è niente da costruire.
06Aggiornare l'istanza
Stesso comando dell'installazione, sull'artefatto nuovo. Il backup non è automatico: è un passo esplicito, ed è il primo.
cd /opt/crm sudo -u crm scripts/ops/backup-db.sh # PRIMA di toccare lo schema sudo tar xzf crm-<versione-nuova>.tar.gz -C /opt/crm sudo /opt/crm/scripts/ops/setup.sh --db-pass '<password-db>'
setup.sh riapplica le migrazioni, riavvia i
servizi (altrimenti resterebbe in esecuzione il binario precedente, e la macchina
risulterebbe aggiornata senza esserlo) e si ferma con errore se
/ready non risponde: un aggiornamento rotto si vede subito.
Con --no-restart scegli tu il momento del fermo.
Le migrazioni vanno solo in avanti, per scelta: non esiste un down. Ognuna gira dentro una transazione, e la rete di sicurezza è il backup del primo passo, non un percorso a ritroso: un down ricostruirebbe la forma delle tabelle, non i dati.
Ne segue che rimettere il binario precedente non basta se lo schema è
già cambiato. Se l'installer è fallito prima del passo delle migrazioni (il
log in /opt/crm/logs/setup/ stampa i passi come
==> [4/9] Migrazioni) basta riscompattare l'artefatto
precedente; se le migrazioni sono già passate, il rollback è
ripristino del dump + artefatto precedente, e costa i dati scritti dopo il
dump.
07Backup e ripristino
I backup pianificati li installa l'installer: un timer systemd
(crm-backup.timer) che chiama
scripts/ops/backup-run.sh con il livello scelto, più un primo
giro eseguito subito, così la macchina nasce con almeno un punto di ripristino. Non serve
scrivere righe di cron a mano, e non conviene farlo.
- Livelli:
basicgiornaliero,standardgiornaliero e settimanale,premiumorario, giornaliero, settimanale e mensile, ognuno con la sua ritenzione. - Destinazione: si dichiara in
/etc/crm/backup.env, scritto alla messa in servizio e mai sovrascritto dopo. Di partenza èBACKUP_MODE=local; per la copia fuori dal server si passa aBACKUP_MODE=s3con le credenziali del bucket. - Un dump singolo, a mano, prima di un intervento:
cd /opt/crm && scripts/ops/backup-db.sh.
Porta i backup fuori dal server (object storage o altro host): un backup sullo stesso disco non sopravvive alla perdita del disco. La modalità locale è un punto di partenza, non un arrivo.
Per il ripristino, la regola è: prima in un database di prova, verifichi, poi tagli sul database vivo:
# con un ruolo che può creare database (es. postgres) scripts/ops/restore-db.sh backups/crm-<db>-<data>.dump crm_restore_check # ispezioni crm_restore_check; se ok, ripristini sul DB vivo (serve --force): scripts/ops/restore-db.sh backups/crm-<db>-<data>.dump <db-vivo> --force # in emergenza, per riaprire prima il servizio (indici di ricerca ricostruiti dopo): scripts/ops/restore-db.sh backups/crm-<db>-<data>.dump <db-vivo> --force --due-fasi
restore-db.sh rifiuta di sovrascrivere il
database vivo senza --force, carica in un database di appoggio
e scambia i nomi solo a caricamento riuscito: se il dump è illeggibile, quello vivo non
viene toccato. Il database precedente resta col suffisso
_precedente_<data> finché non lo cancelli a mano. Il ciclo
dump e restore è scriptato e viene rieseguito dalla verifica automatica a ogni giro.
08Worker e salute operativa
Il worker esegue la coda dei workflow, lo scheduler, il recupero dei job bloccati e il poller delle email in ingresso. Ogni ciclo è protetto da un advisory lock PostgreSQL: un solo worker è sicuro anche attraverso i riavvii.
- Stato e log:
sudo systemctl status crm-go-worker·sudo journalctl -u crm-go-worker -f - Endpoint di salute (admin):
GET /api/ops/health: metriche HTTP + stato della coda workflow. - Coda che si accumula: se
oldestPendingAgeMscresce, il worker è fermo o lento. - Job orfani:
staleRunningCount > 0indica job il cui worker è morto a metà: il reaper li recupera da solo oltre la soglia. - Dead letter: dai log workflow un admin può rilanciare un job: Riprendi (salta le azioni già completate) o Da capo (ricomincia tutta la catena).
09Checklist go-live
Prima di esporre l'istanza a utenti reali:
| # | Controllo |
|---|---|
| 1 | APP_ENV=production nel .env. |
| 2 | Impronta .sha256 dell'archivio verificata prima di scompattarlo. |
| 3 | AUTH_JWT_SECRET e CONNECTIONS_ENC_KEY presenti e generati dall'installer (non i valori di esempio). |
| 4 | TRUST_PROXY=true solo dietro un proxy che riscrive X-Forwarded-For; mai un numero. |
| 5 | Credenziali DB reali; ruolo a privilegio minimo (niente CREATE DATABASE). |
| 6 | TLS attivo davanti (nginx/Caddy) e header HSTS dichiarato nel proxy: l'app non lo imposta. |
| 7 | crm-backup.timer attivo e copie fuori dal server (BACKUP_MODE=s3). |
| 8 | Prova di ripristino fatta almeno una volta su un database di scorta. |
| 9 | crm-go-app e crm-go-worker entrambi enabled e running. |
| 10 | Primo admin creato dall'installer, password cambiata al primo accesso; nessun utente di seed presente. |
| 11 | Smoke test: login, una lista, un dettaglio, GET /api/ops/health → status: ok. |