Ristruttura il deploy: immagine autosufficiente per Dokploy
Il Dockerfile compilava l'applicazione in /app-src mentre il runtime girava da /var/www/html, popolato solo dal bind mount del sorgente: l'immagine costruita non veniva mai usata. L'entrypoint rieseguiva composer install e npm run build a ogni avvio, dentro il container. Su Dokploy, senza sorgente sull'host, questo significava build a runtime a ogni deploy. Ora vendor/ e public/build sono compilati in stage separati e copiati nell'immagine finale. I container di produzione non montano il sorgente e non eseguono build all'avvio: un redeploy e' build, migrazioni, online. - Dockerfile multi-stage (composer, node, runtime) - nginx e php-fpm nello stesso container via supervisor: con container separati nginx avrebbe bisogno di una copia di public/ e del volume storage solo per servire file statici - clear_env = no in www.conf, senza cui php-fpm nasconde a Laravel tutte le variabili d'ambiente - configurazione da ambiente di processo invece di .env riscritto con sed; il compose fissa solo i valori dettati dalla topologia - fail-fast su APP_URL, DB_PASSWORD, DB_ROOT_PASSWORD, REDIS_PASSWORD - servizio scheduler dedicato: schedule:work non veniva mai eseguito, quindi la pulizia dell'audit log non e' mai partita - solo il container app esegue le migrazioni, per evitare che due container in gara lascino uno schema applicato a meta' - healthcheck separati per worker e scheduler, che non espongono HTTP - docker-compose.dev.yml per lo sviluppo locale - documentazione unificata in README.md e HANDOFF.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
249
HANDOFF.md
Normal file
249
HANDOFF.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Handoff — TerManager2
|
||||
|
||||
Stato del progetto per chi ci mette mano dopo. Per installazione, deploy e uso
|
||||
quotidiano vedi [README.md](README.md); qui c'è il contesto che il README non
|
||||
può dare: perché le cose sono come sono, e cosa resta aperto.
|
||||
|
||||
Ultimo aggiornamento: **18 luglio 2026**.
|
||||
|
||||
---
|
||||
|
||||
## Stato attuale
|
||||
|
||||
L'applicazione è **pronta per il deploy in produzione su Dokploy**. Lo stack è
|
||||
stato verificato end-to-end: build dell'immagine, primo deploy su volumi vuoti,
|
||||
redeploy, login, tutte le pagine, code e scheduler.
|
||||
|
||||
| Aspetto | Stato |
|
||||
|-----------------------------|-------|
|
||||
| Deploy Dokploy (Compose) | ✅ verificato |
|
||||
| Primo avvio a zero-config | ✅ migrazioni + seed + admin automatici |
|
||||
| Redeploy senza interventi | ✅ ~25 secondi |
|
||||
| Sviluppo locale | ✅ `docker-compose.dev.yml` |
|
||||
| Test automatici | ❌ **assenti** — vedi [Cosa resta aperto](#cosa-resta-aperto) |
|
||||
|
||||
---
|
||||
|
||||
## Da dove veniamo
|
||||
|
||||
Il progetto aveva un difetto strutturale nel deploy: il `Dockerfile` compilava
|
||||
l'applicazione in `/app-src`, ma il runtime girava da `/var/www/html`, popolato
|
||||
solo dal bind mount del sorgente. **L'immagine costruita non veniva mai usata.**
|
||||
L'entrypoint rieseguiva `composer install` e `npm run build` a ogni avvio dentro
|
||||
il container. Su Dokploy, senza sorgente sull'host, significava build a runtime
|
||||
a ogni deploy: lento, dipendente dalla rete, e capace di fallire in produzione.
|
||||
|
||||
Da qui la ristrutturazione descritta sotto.
|
||||
|
||||
---
|
||||
|
||||
## Decisioni architetturali
|
||||
|
||||
Le scelte non ovvie, con il motivo. Se una di queste ti sembra sbagliata, il
|
||||
contesto per ridiscuterla è qui.
|
||||
|
||||
### L'immagine è autosufficiente
|
||||
|
||||
`vendor/` e `public/build` sono compilati in stage separati del `Dockerfile` e
|
||||
copiati nell'immagine finale. I container di produzione **non montano il
|
||||
sorgente** e non eseguono build all'avvio.
|
||||
|
||||
Conseguenza pratica: dopo aver modificato dipendenze PHP o asset frontend,
|
||||
serve un rebuild dell'immagine, non un semplice restart.
|
||||
|
||||
### nginx e php-fpm nello stesso container
|
||||
|
||||
Con container separati, nginx avrebbe bisogno di una copia di `public/` e del
|
||||
volume storage solo per servire file statici. Per questa scala è complessità
|
||||
senza beneficio. Sono coordinati da supervisor
|
||||
(`docker/supervisor/supervisord.conf`), che abbatte il container se uno dei due
|
||||
muore definitivamente — altrimenti resterebbe "up" senza servire nulla.
|
||||
|
||||
### La configurazione arriva dall'ambiente, non da `.env` riscritto
|
||||
|
||||
Il vecchio entrypoint riscriveva `.env` con `sed` a ogni avvio. Ora Laravel
|
||||
legge direttamente l'ambiente di processo, e `docker-compose.yml` usa
|
||||
`env_file` senza ridichiarare le variabili.
|
||||
|
||||
Fissati nel compose solo i valori che sono una proprietà *della topologia*
|
||||
(`DB_HOST: mariadb`, `CACHE_STORE: redis`, `LOG_CHANNEL: stderr`, …). Questi
|
||||
**vincono** su quanto scritto nel `.env`, così un file copiato da una macchina
|
||||
di sviluppo non rompe il container.
|
||||
|
||||
Nota: perché questo funzioni, `www.conf` ha `clear_env = no`. Senza, php-fpm
|
||||
sanifica l'ambiente e Laravel non vedrebbe nessuna variabile.
|
||||
|
||||
### Fail-fast sulle variabili mancanti
|
||||
|
||||
Lo stack si rifiuta di partire senza `APP_URL`, `DB_PASSWORD`,
|
||||
`DB_ROOT_PASSWORD`, `REDIS_PASSWORD`. Le password sono validate
|
||||
dall'interpolazione compose (`${VAR:?}`), `APP_URL` dall'entrypoint (dove il
|
||||
messaggio d'errore è più chiaro).
|
||||
|
||||
Deliberato: un errore immediato è meglio di un database in produzione con
|
||||
password `secret`.
|
||||
|
||||
### Un solo container esegue le migrazioni
|
||||
|
||||
Solo `app` migra. `queue-worker` e `scheduler` usano `queue-entrypoint.sh`, che
|
||||
attende e non tocca lo schema. Due container in gara su `artisan migrate` è
|
||||
il modo tipico di ritrovarsi con uno schema applicato a metà.
|
||||
|
||||
### `APP_KEY` risolta una volta sola all'avvio
|
||||
|
||||
Prima veniva rigenerata da `AppServiceProvider::boot()` a ogni richiesta se
|
||||
mancante — con il rischio di chiavi diverse a ogni richiesta e dati cifrati
|
||||
irrecuperabili. Ora è risolta dall'entrypoint (env → volume → generata) e il
|
||||
provider **solleva un'eccezione** se manca, invece di rigenerarla.
|
||||
|
||||
---
|
||||
|
||||
## Bug corretti — i più significativi
|
||||
|
||||
Contesto utile se ti imbatti in comportamenti che sembrano regressioni.
|
||||
|
||||
| Bug | Effetto | Dove |
|
||||
|-----|---------|------|
|
||||
| `key:generate` a runtime | Rotazione silenziosa della chiave → **perdita irreversibile** dei dati cifrati | `AppServiceProvider` |
|
||||
| Import PDF da cartella mai accodato | Upload completato, nessun territorio aggiornato, file orfani a crescita illimitata | `Livewire/Settings/XmlExchange` |
|
||||
| Permessi non riapplicati su Livewire | Un permesso revocato restava attivo fino al reload della pagina | `AppServiceProvider` (`addPersistentMiddleware`) |
|
||||
| Host header injection | Link generati (inclusi i PDF pubblici con codice d'accesso) verso domini arbitrari | `bootstrap/app.php`, `nginx/default.conf` |
|
||||
| Default TTL link mai convertito | Link PDF validi 2 anni invece di 1 mese | migrazione `2026_07_18_000001` |
|
||||
| `syncPermissions()` nel seeder | Un re-seed azzerava i permessi personalizzati dei ruoli | `RolesAndPermissionsSeeder` |
|
||||
| `campagna_id` manipolabile | Rientro conteggiato su una campagna arbitraria | `Livewire/Assegnazioni/Rientra` (`#[Locked]`) |
|
||||
| `pdftoppm` senza timeout | Un PDF malformato bloccava il worker **per sempre** | `TerritorioThumbnailService` |
|
||||
| Collisione nomi file PDF | `1 A` e `1-A` scrivevano sullo stesso file → dati sovrascritti | `Jobs/ImportTerritoryPdfFolder` |
|
||||
| Zip senza limiti | Zip bomb → esaurimento memoria | `TerritorioPdfImportDispatcher` |
|
||||
| Migrazioni thumbnail duplicate | `migrate:rollback` falliva a metà | rimosse 2 migrazioni ridondanti |
|
||||
|
||||
---
|
||||
|
||||
## Cosa resta aperto
|
||||
|
||||
In ordine di priorità.
|
||||
|
||||
### 1. Nessun test automatico
|
||||
|
||||
`composer.json` richiede PHPUnit ma **la cartella `tests/` non esiste**. Ogni
|
||||
verifica è manuale. È il debito più rilevante: le regole di business (anno
|
||||
teocratico, stato territorio, priorità automatica, percentuale campagne) sono
|
||||
esattamente il tipo di logica che va protetta da test.
|
||||
|
||||
Nota: `config/database.php` ridefinisce `connections` e lascia solo `mysql`.
|
||||
Per usare SQLite in memoria nei test va riaggiunta la connessione `sqlite`.
|
||||
|
||||
### 2. Race su doppia assegnazione
|
||||
|
||||
`Assegna::save()` controlla che il territorio sia libero e poi crea la riga, ma
|
||||
nulla a livello di database impedisce due assegnazioni aperte sullo stesso
|
||||
territorio (doppio click, due schede). `assegnazioneCorrente` usa `latestOfMany`,
|
||||
quindi la seconda resta invisibile nell'interfaccia mentre falsa i conteggi.
|
||||
|
||||
Rimedio: transazione con `lockForUpdate()`, oppure un indice univoco su colonna
|
||||
generata che imponga una sola assegnazione aperta per territorio.
|
||||
|
||||
### 3. Scrittura durante il rendering
|
||||
|
||||
`Assegnazione::shortPdfUrl()` genera e salva il codice d'accesso al primo
|
||||
utilizzo, quindi una GET del registro può produrre decine di INSERT. Meglio
|
||||
generare il codice alla creazione dell'assegnazione (observer) e far restituire
|
||||
`null` al metodo quando manca.
|
||||
|
||||
### 4. Ricerca in memoria sui campi cifrati
|
||||
|
||||
Nome e cognome dei proclamatori sono cifrati, quindi la ricerca carica l'intera
|
||||
tabella e filtra in PHP (`ProclamatoreIndex`, `Registro`). Accettabile sotto
|
||||
qualche centinaio di record, non oltre. Se dovesse servire scalare: hash
|
||||
deterministico ricercabile affiancato al campo cifrato.
|
||||
|
||||
### 5. Endpoint PDF pubblici senza throttle
|
||||
|
||||
`/p/{code}` e le rotte del visualizzatore sono senza autenticazione né rate
|
||||
limit. Il controllo d'accesso è solido (confronto in tempo costante su codice a
|
||||
~62 bit, più controlli di scadenza), ma un tentativo di enumerazione non
|
||||
verrebbe né rallentato né registrato. Un `throttle:30,1` costerebbe una riga.
|
||||
|
||||
---
|
||||
|
||||
## Trappole da conoscere
|
||||
|
||||
**`APP_KEY` è il singolo punto di perdita dati.** I nomi dei proclamatori sono
|
||||
cifrati con questa chiave. Il backup del database senza la chiave è inutile per
|
||||
quei campi. Se non la imposti, l'entrypoint ne genera una e la salva nel volume
|
||||
`storage_data`, stampandola nei log: se quel volume viene perso, i dati sono
|
||||
irrecuperabili.
|
||||
|
||||
**`APP_URL` deve combaciare col dominio servito.** L'applicazione rifiuta
|
||||
richieste con `Host` diverso (protezione host header injection) e restituisce
|
||||
400 senza spiegazioni. È la causa più comune di "il deploy è andato ma il sito
|
||||
non si apre". `localhost` e `127.0.0.1` sono ammessi perché li usano gli health
|
||||
check interni.
|
||||
|
||||
**Il volume `storage_data` non è solo cache.** Contiene i PDF dei territori, le
|
||||
miniature e la chiave di fallback. Non va trattato come rigenerabile.
|
||||
|
||||
**Modifiche a dipendenze o asset richiedono un rebuild.** Non è più sufficiente
|
||||
riavviare il container: `vendor/` e `public/build` vivono dentro l'immagine.
|
||||
|
||||
---
|
||||
|
||||
## Come verificare che tutto funzioni
|
||||
|
||||
Dopo una modifica sostanziale, questo giro copre i percorsi critici:
|
||||
|
||||
Si usa il compose di **sviluppo**: quello di produzione richiede la rete
|
||||
esterna `dokploy-network` e non pubblica porte sull'host, quindi non è
|
||||
verificabile in locale così com'è.
|
||||
|
||||
```bash
|
||||
cp .env.example .env # se non c'è già
|
||||
|
||||
# 1. L'immagine builda
|
||||
docker build -f docker/php/Dockerfile -t termanager2-check .
|
||||
|
||||
# 2. Stack pulito da zero
|
||||
docker compose -f docker-compose.dev.yml down -v
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
docker compose -f docker-compose.dev.yml ps # tutti i servizi healthy
|
||||
|
||||
# 3. L'applicazione risponde
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/up # 200
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/login # 200
|
||||
|
||||
# 4. Il redeploy è idempotente
|
||||
docker compose -f docker-compose.dev.yml up -d --force-recreate app
|
||||
docker compose -f docker-compose.dev.yml logs app | grep -E "Migrations|Seed"
|
||||
# atteso: "Migrations applied" + "Seed already applied"
|
||||
|
||||
# 5. La cifratura funziona
|
||||
docker compose -f docker-compose.dev.yml exec app php artisan tinker --execute="
|
||||
\$p = App\Models\Proclamatore::create(['nome'=>'Test','cognome'=>'Verifica','attivo'=>true]);
|
||||
\$raw = DB::table('proclamatori')->where('id',\$p->id)->value('nome');
|
||||
echo \$raw === 'Test' ? 'ERRORE: dati in chiaro' : 'OK: cifrato';
|
||||
"
|
||||
```
|
||||
|
||||
Per provare davvero il compose di produzione serve creare la rete
|
||||
(`docker network create dokploy-network`) e aggiungere un override che pubblichi
|
||||
la porta 80 del servizio `app`.
|
||||
|
||||
---
|
||||
|
||||
## Documentazione
|
||||
|
||||
| Documento | Contenuto |
|
||||
|------------------------------------|--------------------------------------------|
|
||||
| [README.md](README.md) | Deploy, configurazione, uso, riferimento |
|
||||
| `HANDOFF.md` (questo file) | Stato, decisioni, debito tecnico |
|
||||
|
||||
La specifica funzionale originale (`TerManager2_v2.md`) è stata rimossa dal
|
||||
working tree: descriveva funzionalità poi cambiate — in particolare un wizard
|
||||
di primo avvio successivamente eliminato in favore della pagina Impostazioni —
|
||||
e conteneva checklist di sviluppo ormai esaurite. Le parti ancora valide
|
||||
(modello dati, regole di business, RBAC) sono confluite nel README.
|
||||
|
||||
Resta consultabile nella storia del repository:
|
||||
|
||||
```bash
|
||||
git show 701f479:TerManager2_v2.md
|
||||
```
|
||||
Reference in New Issue
Block a user