francesco cf78550516 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>
2026-07-19 16:49:10 +02:00
2026-04-16 10:05:15 +00:00
2026-04-05 19:26:04 +02:00
2026-04-05 19:26:04 +02:00
2026-04-05 19:26:04 +02:00
2026-04-05 19:26:04 +02:00

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

Gestione quotidiana

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_KEY in 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_URL deve corrispondere esattamente al dominio che servirai, schema https:// incluso. L'applicazione rifiuta richieste con un Host diverso: è 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:

  1. La rete dokploy-network. In fondo a docker-compose.yml c'è 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-network
    

    Poi metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta 8080, con certificato TLS.

  2. 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:

  1. ricrea le directory di storage e risolve APP_KEY
  2. attende il database
  3. applica le migrazioni pendenti
  4. salta il seed (già eseguito — il marker è nel volume storage)
  5. 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 Host corrisponde a APP_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 Secure in produzione
  • Esecuzione PHP: solo index.php è un entrypoint valido; ogni altro .php restituisce 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.numero e anni_teocratici.label — univoci
  • assegnazioni(territorio_id, returned_at) — indice composito, è la ricerca più frequente dell'applicazione
  • assegnazioni(proclamatore_id, returned_at), assegnazioni(assigned_at)
  • territori(attivo, deleted_at), proclamatori(deleted_at)
  • Foreign key con RESTRICT su 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
PDF 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.

Description
No description provided
Readme 4.2 MiB
Languages
PHP 64%
Blade 26.4%
CSS 4.2%
JavaScript 3.7%
Shell 0.9%
Other 0.8%