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>
TerManager2
Applicazione web per la gestione dell'assegnazione e del rientro dei territori (cartoline) di congregazione, con statistiche di percorrenza, campagne speciali e registro storico completo.
I dati personali dei proclamatori sono cifrati nel database, e ogni operazione è tracciata in un audit log consultabile.
Indice
Per iniziare
- Deploy su Dokploy — la via consigliata per la produzione
- Deploy su altri sistemi — Coolify, Portainer, server con Docker
- Sviluppo locale
Gestione quotidiana
- Variabili d'ambiente
- Impostazioni applicative
- Aggiornamenti
- Backup e ripristino
- Comandi utili
- Troubleshooting
Riferimento
Deploy su Dokploy
Come funziona
L'immagine Docker è autosufficiente: dipendenze PHP e asset frontend sono compilati durante il build, non all'avvio. Nginx gira nello stesso container di php-fpm. Un deploy quindi è semplicemente: build → migrazioni → online.
Non serve il codice sorgente sul server, non ci sono bind mount, e non viene
eseguito né composer install né npm build all'avvio.
| Servizio | Ruolo | Esposto |
|---|---|---|
app |
nginx + php-fpm — l'applicazione | via Traefik |
queue-worker |
Job in coda (import PDF massivi) | no |
scheduler |
Task pianificati (pulizia audit log) | no |
mariadb |
Database | no |
redis |
Cache, sessioni, code | no |
Solo app è raggiungibile dall'esterno. Database e Redis restano su una rete
interna senza rotta verso Internet.
Passo 1 — Crea l'applicazione
Su Dokploy: Create Application → tipo Compose.
| Campo | Valore |
|---|---|
| Repository | il tuo repo Git |
| Branch | main |
| Compose Path | docker-compose.yml |
Passo 2 — Genera le credenziali
Sul tuo computer, esegui questi comandi e tieni da parte l'output:
echo "APP_KEY=base64:$(openssl rand -base64 32)"
echo "DB_PASSWORD=$(openssl rand -base64 24)"
echo "DB_ROOT_PASSWORD=$(openssl rand -base64 24)"
echo "REDIS_PASSWORD=$(openssl rand -base64 24)"
Conserva
APP_KEYin un password manager. I dati personali dei proclamatori sono cifrati con questa chiave: se la perdi, non sono più leggibili nemmeno avendo il backup del database.
Passo 3 — Imposta le variabili d'ambiente
Nella sezione Environment di Dokploy, incolla questo blocco e sostituisci i valori:
# --- Applicazione ---
APP_NAME=TerManager2
APP_ENV=production
APP_DEBUG=false
APP_URL=https://territori.tuodominio.it
APP_TIMEZONE=Europe/Rome
# --- Chiavi e password (dal Passo 2) ---
APP_KEY=base64:...
DB_PASSWORD=...
DB_ROOT_PASSWORD=...
REDIS_PASSWORD=...
# --- Database ---
DB_DATABASE=termanager2
DB_USERNAME=termanager2
# --- Primo amministratore (creato al primo avvio) ---
INITIAL_ADMIN_NAME=Nome Cognome
INITIAL_ADMIN_EMAIL=admin@tuodominio.it
INITIAL_ADMIN_PASSWORD=UnaPasswordSolida
# --- Sessioni ---
SESSION_SECURE_COOKIE=true
# --- Posta (SMTP esterno) ---
MAIL_MAILER=smtp
MAIL_HOST=smtp.tuoprovider.it
MAIL_PORT=587
MAIL_USERNAME=utente
MAIL_PASSWORD=password
MAIL_SCHEME=tls
MAIL_FROM_ADDRESS=noreply@tuodominio.it
APP_URLdeve corrispondere esattamente al dominio che servirai, schemahttps://incluso. L'applicazione rifiuta richieste con unHostdiverso: è la protezione contro l'host header injection, ed è la causa più comune di "pagina che non si apre" al primo deploy.
Passo 4 — Assegna il dominio
Domains → Add Domain:
| Campo | Valore |
|---|---|
| Service Name | app |
| Container Port | 80 |
| Host | il tuo dominio |
| HTTPS | attivo, Let's Encrypt |
Dokploy genera da sé le label Traefik: non vanno scritte nel compose.
Passo 5 — Deploy
Premi Deploy. Il primo avvio esegue migrazioni, seed di ruoli e permessi, e crea l'amministratore. Richiede in genere uno o due minuti.
Nei log dovresti vedere:
[✓] Storage directories ready.
[✓] APP_KEY provided via environment.
[✓] Database reachable.
[✓] Migrations applied.
[✓] Seed completed.
Initial admin created: admin@tuodominio.it
[✓] Caches warmed.
TerManager2 — ready
Accedi con le credenziali INITIAL_ADMIN_* e cambia subito la password.
Se qualcosa non parte
Lo stack si rifiuta di avviarsi quando manca una variabile essenziale, con un messaggio esplicito:
| Messaggio nei log | Cosa fare |
|---|---|
required variable DB_PASSWORD is missing a value |
Imposta DB_PASSWORD |
required variable REDIS_PASSWORD is missing a value |
Imposta REDIS_PASSWORD |
[✗] APP_URL is not set |
Imposta APP_URL |
[✗] APP_URL must include the scheme |
Aggiungi https:// davanti |
È voluto: meglio un errore immediato che un database in produzione con la password di default.
Deploy su altri sistemi
Lo stack è Docker Compose standard e funziona su Coolify, Portainer, CapRover o un semplice server con Docker.
Le uniche due particolarità sono:
-
La rete
dokploy-network. In fondo adocker-compose.ymlc'è una rete esterna che serve al Traefik di Dokploy per raggiungere il container. Su altri sistemi rimuovila e pubblica una porta:services: app: ports: - "8080:80" networks: - internal # rimuovi "- dokploy-network" networks: internal: driver: bridge # rimuovi il blocco dokploy-networkPoi metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta
8080, con certificato TLS. -
Il file
.env. Dokploy lo genera dalle variabili della sua interfaccia. Altrove crealo a mano nella cartella del progetto, con lo stesso contenuto del Passo 3.
Dopodiché:
docker compose up -d --build
docker compose logs -f app
Sviluppo locale
cp .env.example .env
docker compose -f docker-compose.dev.yml up -d --build
| Cosa | Dove |
|---|---|
| Applicazione | http://localhost:8080 |
| Mailpit (posta test) | http://localhost:8025 |
| Login | admin@termanager2.local / password |
A differenza della produzione, l'ambiente di sviluppo monta il codice sorgente in bind mount — le modifiche PHP sono immediate — e pubblica le porte sull'host.
Se la porta 8080 è occupata:
APP_PORT=8081 docker compose -f docker-compose.dev.yml up -d
Per lavorare sugli asset frontend con hot reload:
npm install
npm run dev
Variabili d'ambiente
Il container legge la configurazione dall'ambiente di processo (su Dokploy: le
variabili della UI, salvate in .env). Il file docker-compose.yml non
ridichiara le variabili applicative: le prende dal .env così com'è.
Obbligatorie
| Variabile | Esempio | Note |
|---|---|---|
APP_URL |
https://territori.esempio.it |
Deve combaciare col dominio servito |
APP_KEY |
base64:... |
Cifra i dati personali — da custodire |
DB_PASSWORD |
— | Nessun default |
DB_ROOT_PASSWORD |
— | Nessun default |
REDIS_PASSWORD |
— | Nessun default |
Principali facoltative
| Variabile | Default | Descrizione |
|---|---|---|
APP_NAME |
TerManager2 |
Nome mostrato nell'interfaccia |
APP_ENV |
production |
local in sviluppo |
APP_DEBUG |
false |
Mai true in produzione |
APP_TIMEZONE |
Europe/Rome |
Fuso orario |
LOG_LEVEL |
warning |
debug per diagnosticare |
SESSION_SECURE_COOKIE |
true |
false solo su HTTP locale |
INITIAL_ADMIN_EMAIL |
— | Admin creato al primo avvio |
INITIAL_ADMIN_PASSWORD |
— | Idem |
INITIAL_ADMIN_NAME |
Amministratore |
Idem |
MAIL_MAILER |
log |
smtp per inviare davvero |
RUN_MIGRATIONS |
true |
Migrazioni automatiche all'avvio |
RUN_DB_SEED_ON_FIRST_START |
true |
Seed al primo avvio |
Fissate dal compose
Questi valori sono imposti da docker-compose.yml e ignorano quanto scritto
nel .env, perché descrivono la topologia dello stack: DB_HOST, DB_PORT,
REDIS_HOST, REDIS_PORT, CACHE_STORE, SESSION_DRIVER,
QUEUE_CONNECTION, LOG_CHANNEL.
Serve a evitare che un .env copiato da una macchina di sviluppo (con, per
dire, DB_HOST=127.0.0.1) rompa il container.
Impostazioni applicative
Configurabili dall'interfaccia (Impostazioni, permesso settings.manage),
non da variabili d'ambiente:
| Impostazione | Descrizione |
|---|---|
| Nome congregazione | Mostrato nell'intestazione e nei PDF |
| Logo | Immagine nell'intestazione |
| URL pubblico base | Base dei link PDF condivisibili |
| Giorni giacenza "da assegnare" | Soglia oltre cui un territorio è da assegnare |
| Mesi giacenza "prioritari" | Soglia oltre cui un territorio diventa prioritario |
| Giorni per "smarrito" | Soglia oltre cui un'assegnazione è da rientrare |
| Elementi nelle liste della home | Quanti record mostrare nelle tre liste rapide |
| Validità link PDF (mesi) | Scadenza dei link pubblici alle cartoline |
| Download PDF visibile | Mostra o nasconde il pulsante di download |
| Retention audit log (giorni) | Dopo quanti giorni i log vengono cancellati |
Aggiornamenti
Push su main → Deploy su Dokploy. Ad ogni avvio il container:
- ricrea le directory di storage e risolve
APP_KEY - attende il database
- applica le migrazioni pendenti
- salta il seed (già eseguito — il marker è nel volume storage)
- rigenera le cache di configurazione, rotte e viste
Se le migrazioni falliscono il container non parte: preferibile a servire traffico su uno schema applicato a metà.
Un aggiornamento ordinario non richiede alcun intervento manuale.
Backup e ripristino
Cosa salvare
| Volume | Contenuto | Nel backup |
|---|---|---|
db_data |
Database MariaDB | ✅ |
storage_data |
PDF territori, miniature, log, APP_KEY di fallback |
✅ |
redis_data |
Cache e sessioni — rigenerabili | — |
Più il valore di APP_KEY. Il database da solo non basta: senza la chiave
i dati personali cifrati non sono leggibili.
Backup
# Database
docker exec <container-db> mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \
termanager2 | gzip > termanager2_$(date +%Y%m%d).sql.gz
# File caricati (PDF e miniature)
docker run --rm -v <progetto>_storage_data:/data -v "$PWD":/backup \
alpine tar czf /backup/storage_$(date +%Y%m%d).tar.gz -C /data .
Automatizzabile con cron:
0 2 * * * docker exec <container-db> mariadb-dump -u root -p"$DB_ROOT_PASSWORD" termanager2 | gzip > /backups/termanager2_$(date +\%Y\%m\%d).sql.gz
Ripristino
gunzip -c termanager2_20260718.sql.gz | \
docker exec -i <container-db> mariadb -u root -p"$DB_ROOT_PASSWORD" termanager2
docker exec <container-app> php artisan migrate --force
docker exec <container-app> php artisan cache:clear
Assicurati che APP_KEY sia la stessa di quando il backup è stato creato.
Comandi utili
Sostituisci <app> con il nome del container (lo trovi con docker ps).
# Log in tempo reale
docker logs -f <app>
# Stato delle migrazioni
docker exec -it <app> php artisan migrate:status
# Shell interattiva Laravel
docker exec -it <app> php artisan tinker
# Reimpostare la password di un utente
docker exec -it <app> php artisan tinker --execute="
\$u = App\Models\User::where('email','admin@tuodominio.it')->first();
\$u->password = Hash::make('NuovaPassword');
\$u->save();
"
# Creare un altro amministratore
docker exec -it <app> php artisan app:create-initial-admin \
--name="Nome" --email="mail@dominio.it" --password="Password"
# Pulizia manuale dell'audit log
docker exec -it <app> php artisan audit:cleanup
# Svuotare le cache
docker exec -it <app> php artisan optimize:clear
Troubleshooting
Il container app resta unhealthy
L'health check interroga /up. Guarda i log (docker logs <app>): quasi
sempre è il database irraggiungibile o una migrazione fallita.
Errore 400 o pagina bianca sul dominio
APP_URL non corrisponde al dominio servito. Deve combaciare esattamente,
schema https:// incluso. È la protezione contro l'host header injection.
La pagina appare senza stile (CSS mancante)
Gli asset sono compilati dentro l'immagine. Se mancano, è fallito lo stage di
build frontend: cerca npm run build nel log di build del deploy.
I job restano in coda
Verifica che queue-worker sia healthy e che REDIS_PASSWORD sia identica per
tutti i servizi.
L'import PDF non termina
L'import ha un timeout di 15 minuti; oltre, il job viene marcato failed e i file temporanei rimossi. Controlla i log del worker per individuare il PDF responsabile.
I dati cifrati non sono più leggibili
APP_KEY è cambiata. Ripristina il valore originale: non esiste altro modo di
recuperare i dati. Se la chiave è persa definitivamente, i proclamatori vanno
reinseriti (territori, assegnazioni e storico restano intatti — non sono
cifrati).
Reset completo (solo sviluppo)
docker compose -f docker-compose.dev.yml down -v
docker compose -f docker-compose.dev.yml up -d --build
-v cancella i volumi: database, Redis e file caricati vengono persi.
Funzionalità
Dashboard
- Anno teocratico corrente (creato automaticamente, settembre → agosto)
- Media mensile di percorrenza e durata media delle assegnazioni
- Campagna attiva con barra di progresso
- Tre liste rapide con azione diretta: da assegnare, prioritari, da rientrare
Territori
- CRUD completo con upload PDF e miniatura generata automaticamente
- Filtri per zona, tipologia e stato
- Badge di stato colorati e indicatore di priorità (manuale o automatica)
- Dettaglio con visualizzatore PDF e storico assegnazioni per anno teocratico
- Link PDF pubblico condivisibile, a scadenza configurabile
- Soft delete con cestino (ripristino o eliminazione definitiva)
- Import massivo di PDF da cartella o archivio ZIP, elaborato in background
Proclamatori
- CRUD con nome e cognome cifrati nel database
- Conteggio dei territori attualmente assegnati
- Anonimizzazione GDPR irreversibile
- Soft delete con cestino
Assegnazione e rientro
- Selezione territorio disponibile e proclamatore attivo
- Anno teocratico rilevato automaticamente dalla data
- Validazione: territorio non già assegnato, entrambi attivi
- Rientro con calcolo automatico dei giorni
- Prompt campagna: se il rientro ricade nel periodo di una campagna, viene chiesto se conteggiarlo
Campagne
- CRUD con date di inizio e fine
- Stato automatico: attiva, futura o conclusa
- Percentuale di percorrenza in tempo reale
- Dettaglio territorio per territorio
Registro assegnazioni
- Filtri per anno teocratico, zona, tipologia, stato e ricerca testuale
- Colonne: territorio, proclamatore, date, giorni, campagna
Audit log
- Tracciamento automatico su tutti i modelli
- Eventi di flusso espliciti: assegnazione, rientro, login, logout
- Filtri per utente e tipo evento, con diff prima/dopo
- Pulizia automatica schedulata secondo la retention configurata
XML Exchange
- Conversione di dump SQL legacy in XML compatibile
- Import ed export XML dell'intero set di dati gestionali
Zone e tipologie
- Gestione inline: aggiungi, rinomina, attiva/disattiva, elimina
- Non eliminabili se hanno territori associati
Ruoli e permessi
Tre ruoli con permessi granulari (spatie/laravel-permission):
| Permesso | Amministratore | Assistente | Operatore |
|---|---|---|---|
settings.manage |
✅ | ||
territori.manage |
✅ | ||
proclamatori.manage |
✅ | ✅ | |
campagne.manage |
✅ | ✅ | |
registro.view |
✅ | ✅ | |
registro.export |
✅ | ||
territori.assign |
✅ | ✅ | ✅ |
territori.return |
✅ | ✅ | ✅ |
audit.view |
✅ | ||
audit.export |
✅ |
L'amministratore ha un bypass completo (Gate::before in AppServiceProvider).
Il menu laterale mostra solo le voci per cui l'utente ha i permessi.
I permessi sono verificati sia sulle rotte sia sulle richieste Livewire, quindi una revoca ha effetto immediato senza bisogno di ricaricare la pagina.
Sicurezza e GDPR
Sicurezza
- Cifratura a riposo: nome e cognome dei proclamatori cifrati con AES-256-CBC
tramite
APP_KEY - Password: hash bcrypt
- RBAC: middleware
permission:sulle rotte, riapplicato anche alle richieste Livewire - CSRF: token su tutti i form
- Rate limiting: sul login
- Host header: accettate solo richieste il cui
Hostcorrisponde aAPP_URL - Scheme forwarding: lo schema è deciso dal server, non dall'header del client, per non falsare la validazione dei link firmati
- Security headers:
X-Frame-Options,X-Content-Type-Options,Referrer-Policy - Sessioni: in Redis, cookie httpOnly e
Securein produzione - Esecuzione PHP: solo
index.phpè un entrypoint valido; ogni altro.phprestituisce 404
GDPR
- Minimizzazione: dei proclamatori si conservano solo nome, cognome e stato
- Cifratura a riposo: i dati sono illeggibili anche con accesso diretto al DB
- Audit trail: registro completo delle operazioni
- Diritto all'oblio: funzione di anonimizzazione irreversibile
- Retention: cancellazione automatica dei log oltre il periodo configurato
- No dati sensibili nei log: l'audit registra gli identificativi, non i nomi in chiaro
Modello dati
| Tabella | Descrizione |
|---|---|
users |
Utenti dell'applicazione |
settings |
Configurazione singleton |
zone |
Zone territoriali |
tipologie |
Tipologie di territorio |
proclamatori |
Assegnatari, con nome/cognome cifrati e soft delete |
territori |
Cartoline con zona, tipologia, PDF, miniatura, soft delete |
anni_teocratici |
Periodi settembre → agosto |
campagne |
Periodi speciali con date e descrizione |
assegnazioni |
Territorio ↔ proclamatore, con date, anno e campagna |
activity_log |
Audit trail |
Vincoli e indici principali
territori.numeroeanni_teocratici.label— univociassegnazioni(territorio_id, returned_at)— indice composito, è la ricerca più frequente dell'applicazioneassegnazioni(proclamatore_id, returned_at),assegnazioni(assigned_at)territori(attivo, deleted_at),proclamatori(deleted_at)- Foreign key con
RESTRICTsu territorio e proclamatore, a protezione dell'integrità dello storico
Regole di business
Anno teocratico
Va da settembre ad agosto: da settembre in poi l'anno è corrente/corrente+1,
prima è corrente-1/corrente. Viene creato automaticamente quando serve.
L'assegnazione conta per l'anno determinato dalla data di assegnazione.
Stato del territorio
Calcolato dinamicamente, non memorizzato:
| Stato | Condizione |
|---|---|
in_reparto |
Attivo, nessuna assegnazione aperta |
assegnato |
Ha un'assegnazione con returned_at nullo |
da_rientrare |
Assegnato da più giorni della soglia configurata |
inattivo |
Contrassegnato come non attivo |
Priorità
Un territorio è prioritario se lo è per flag manuale o perché non viene assegnato da più mesi della soglia configurata. La soglia automatica vince sempre sul flag: l'amministratore può aggiungere priorità, non toglierla quando la soglia è scattata.
Percentuale di percorrenza delle campagne
- Denominatore: tutti i territori assegnati nel periodo della campagna
- Numeratore: le assegnazioni contrassegnate come conteggiate
Architettura
Stack
| Componente | Tecnologia |
|---|---|
| Framework | Laravel 11 |
| PHP | 8.3 (FPM) |
| Interfaccia | Livewire 3.5 + TailwindCSS 4 + Alpine.js |
| Database | MariaDB 11 |
| Cache/Sessioni/Code | Redis 7 |
| Web server | Nginx, nello stesso container di php-fpm |
| RBAC | spatie/laravel-permission |
| Audit trail | spatie/laravel-activitylog |
| barryvdh/laravel-dompdf + poppler-utils | |
| Build asset | Vite 6 (in fase di build immagine) |
Struttura del progetto
TerManager2/
├── app/
│ ├── Console/Commands/ # audit:cleanup, app:create-initial-admin
│ ├── Http/Controllers/ # PDF pubblici, import, login
│ ├── Jobs/ # Import PDF massivo (in coda)
│ ├── Livewire/ # Tutti i componenti di interfaccia
│ ├── Models/ # 9 modelli Eloquent
│ ├── Providers/ # AppServiceProvider
│ └── Services/ # Miniature PDF, stato import
├── bootstrap/app.php # Middleware, proxy e host fidati
├── config/ # Configurazione Laravel
├── database/
│ ├── migrations/
│ └── seeders/ # Ruoli, permessi, dati di sviluppo
├── docker/
│ ├── nginx/default.conf # Vhost
│ ├── php/
│ │ ├── Dockerfile # Build multi-stage
│ │ ├── entrypoint.sh # Avvio: chiave, migrazioni, cache
│ │ ├── queue-entrypoint.sh # Avvio worker e scheduler
│ │ ├── php.ini
│ │ └── www.conf
│ └── supervisor/ # nginx + php-fpm nello stesso container
├── resources/ # CSS, JS, viste Blade
├── routes/web.php # Rotte con i relativi permessi
├── docker-compose.yml # Produzione
├── docker-compose.dev.yml # Sviluppo locale
├── HANDOFF.md # Stato del progetto per chi subentra
└── README.md
Perché nginx e php-fpm nello stesso container
Con container separati, nginx avrebbe bisogno di una propria copia di public/
e del volume storage solo per servire i file statici. Per un'applicazione di
questa scala è complessità senza beneficio: un solo container significa un solo
health check, un solo target per il proxy e nessun volume condiviso da tenere
allineato.
Licenza
Proprietaria. Tutti i diritti riservati.