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>
11 KiB
Handoff — TerManager2
Stato del progetto per chi ci mette mano dopo. Per installazione, deploy e uso quotidiano vedi 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 |
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'è.
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 | 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:
git show 701f479:TerManager2_v2.md