Semplifica il deploy: un solo docker-compose senza anchor YAML

I due file compose andavano tenuti allineati a mano, e gli anchor
(x-app-env, <<:) sono sintassi che va conosciuta per riuscire a leggere il
file. Per un deploy che deve restare semplice era complessita' non
giustificata.

Ora c'e' un solo docker-compose.yml, esplicito, che copre produzione e
sviluppo:

- ogni servizio dichiara quello che usa, senza merge key
- mailpit e' dietro il profilo "dev", quindi non parte mai in produzione
- la porta e' legata a 127.0.0.1 per default: in produzione il traffico
  arriva da Traefik sulla rete interna, in locale basta localhost:8080.
  Per esporla davvero: APP_PORT=0.0.0.0:8080
- .env diventa l'unica fonte di configurazione, senza valori ridichiarati

Compromesso accettato: sparendo i valori fissati nel compose, DB_HOST e
REDIS_HOST devono contenere i nomi dei servizi (mariadb, redis) e non un
indirizzo. E' segnalato in .env.example, nel README e in HANDOFF.md.

Per le modifiche al codice senza ricostruire l'immagine il README documenta
un docker-compose.override.yml, che Compose applica da solo ed e' gia'
ignorato da git.

Verificato: sei servizi healthy, login e pagine a 200, redeploy idempotente,
porta esposta solo su localhost, mailpit assente senza il profilo dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 17:41:48 +02:00
parent 0502fad996
commit afaaeb5927
5 changed files with 232 additions and 318 deletions

View File

@@ -1,74 +1,91 @@
# ============================================================================= # =============================================================================
# TerManager2 — environment template # TerManager2 — configurazione
# #
# LOCAL DEVELOPMENT: copy to .env and adjust. # Questo file e' l'UNICA fonte di configurazione: docker-compose.yml lo legge
# cp .env.example .env && docker compose -f docker-compose.dev.yml up -d # cosi' com'e' e non ridichiara nulla.
# #
# PRODUCTION (Dokploy): do NOT ship a .env file. Set these as environment # In locale: cp .env.example .env e modifica quello che serve
# variables in the Dokploy UI instead — the container reads them directly. # Su Dokploy: incolla queste variabili in Environment, il .env lo
# Variables marked [REQUIRED] have no safe default and the stack will refuse # genera lui
# to start without them. #
# Le variabili marcate [OBBLIGATORIA] non hanno default: lo stack si rifiuta
# di partire senza.
# ============================================================================= # =============================================================================
# -----------------------------------------------------------------------------
# Applicazione
# -----------------------------------------------------------------------------
APP_NAME=TerManager2 APP_NAME=TerManager2
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=false
APP_TIMEZONE=Europe/Rome APP_TIMEZONE=Europe/Rome
# [REQUIRED] Generate with: php artisan key:generate --show # [OBBLIGATORIA] Deve combaciare ESATTAMENTE col dominio servito, schema
# Proclamatori personal data is encrypted with this key. If it changes or is # incluso. L'app rifiuta richieste con un Host diverso (protezione contro
# lost, that data becomes permanently unreadable. Back it up. # l'host header injection): un valore sbagliato da' 400 su tutto.
APP_KEY= # In locale: http://localhost:8080
# [REQUIRED] Public URL, including scheme. Used for links in PDFs and emails,
# and to validate the incoming Host header.
APP_URL=https://territori.example.org APP_URL=https://territori.example.org
# [OBBLIGATORIA] Genera con: echo "base64:$(openssl rand -base64 32)"
# I dati personali dei proclamatori sono cifrati con questa chiave: se la
# perdi non sono piu' leggibili nemmeno avendo il backup del database.
# Conservala in un password manager.
APP_KEY=
LOG_CHANNEL=stderr LOG_CHANNEL=stderr
LOG_LEVEL=warning LOG_LEVEL=warning
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# First-run bootstrap # Primo avvio
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# The admin account created on first deploy. Change the password after login. # L'amministratore creato al primo deploy. Cambia la password dopo il login.
INITIAL_ADMIN_NAME=Amministratore INITIAL_ADMIN_NAME=Amministratore
INITIAL_ADMIN_EMAIL= INITIAL_ADMIN_EMAIL=
INITIAL_ADMIN_PASSWORD= INITIAL_ADMIN_PASSWORD=
# Leave these at their defaults; they exist as escape hatches. # Lasciale cosi': esistono come via di fuga se qualcosa va storto.
RUN_MIGRATIONS=true RUN_MIGRATIONS=true
RUN_DB_SEED_ON_FIRST_START=true RUN_DB_SEED_ON_FIRST_START=true
ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=true ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=true
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Database — hostnames are fixed by docker-compose.yml # Database
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# DB_HOST deve restare "mariadb": e' il nome del servizio in
# docker-compose.yml, non un indirizzo di rete.
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=mariadb DB_HOST=mariadb
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=termanager2 DB_DATABASE=termanager2
DB_USERNAME=termanager2 DB_USERNAME=termanager2
# [REQUIRED] No defaults on purpose — generate with: openssl rand -base64 24 # [OBBLIGATORIE] Genera con: openssl rand -base64 24
DB_PASSWORD= DB_PASSWORD=
DB_ROOT_PASSWORD= DB_ROOT_PASSWORD=
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Redis (cache, sessions, queue) # Redis — cache, sessioni, code
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Come sopra: "redis" e' il nome del servizio, non va cambiato.
REDIS_HOST=redis REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
# [REQUIRED] openssl rand -base64 24
# [OBBLIGATORIA] openssl rand -base64 24
REDIS_PASSWORD= REDIS_PASSWORD=
CACHE_STORE=redis CACHE_STORE=redis
SESSION_DRIVER=redis SESSION_DRIVER=redis
SESSION_SECURE_COOKIE=true
QUEUE_CONNECTION=redis QUEUE_CONNECTION=redis
# true in produzione (HTTPS), false se accedi in HTTP su localhost.
SESSION_SECURE_COOKIE=true
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Mail — external SMTP. Use MAIL_MAILER=log to disable delivery entirely. # Posta
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# In produzione: SMTP esterno.
# In locale con --profile dev: MAIL_HOST=mailpit, MAIL_PORT=1025
# Per disattivare del tutto l'invio: MAIL_MAILER=log
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST= MAIL_HOST=
MAIL_PORT=587 MAIL_PORT=587
@@ -79,10 +96,10 @@ MAIL_FROM_ADDRESS=noreply@example.org
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Local development only — ignored in production # Porte
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Host port for docker-compose.dev.yml. In production Traefik handles routing # Di default l'app ascolta solo su localhost. In produzione va bene cosi': il
# and no port is published. # traffico arriva da Traefik sulla rete interna, non da questa porta.
APP_PORT=8080 # Per esporla davvero (server senza reverse proxy): APP_PORT=0.0.0.0:8080
MAILPIT_UI_PORT=8025 APP_PORT=127.0.0.1:8080
SEED_DEV_DATA=false MAILPIT_UI_PORT=127.0.0.1:8025

View File

@@ -19,7 +19,7 @@ redeploy, login, tutte le pagine, code e scheduler.
| Deploy Dokploy (Compose) | ✅ verificato | | Deploy Dokploy (Compose) | ✅ verificato |
| Primo avvio a zero-config | ✅ migrazioni + seed + admin automatici | | Primo avvio a zero-config | ✅ migrazioni + seed + admin automatici |
| Redeploy senza interventi | ✅ ~25 secondi | | Redeploy senza interventi | ✅ ~25 secondi |
| Sviluppo locale | ✅ `docker-compose.dev.yml` | | Sviluppo locale | ✅ stesso compose, profilo `dev` |
| Test automatici | ❌ **assenti** — vedi [Cosa resta aperto](#cosa-resta-aperto) | | Test automatici | ❌ **assenti** — vedi [Cosa resta aperto](#cosa-resta-aperto) |
--- ---
@@ -62,16 +62,22 @@ muore definitivamente — altrimenti resterebbe "up" senza servire nulla.
### La configurazione arriva dall'ambiente, non da `.env` riscritto ### La configurazione arriva dall'ambiente, non da `.env` riscritto
Il vecchio entrypoint riscriveva `.env` con `sed` a ogni avvio. Ora Laravel Il vecchio entrypoint riscriveva `.env` con `sed` a ogni avvio. Ora Laravel
legge direttamente l'ambiente di processo, e `docker-compose.yml` usa legge direttamente l'ambiente di processo, e `docker-compose.yml` si limita a
`env_file` senza ridichiarare le variabili. `env_file: .env` senza ridichiarare nulla.
Fissati nel compose solo i valori che sono una proprietà *della topologia* Il `.env` è quindi l'**unica** fonte di configurazione. Una versione precedente
(`DB_HOST: mariadb`, `CACHE_STORE: redis`, `LOG_CHANNEL: stderr`, …). Questi fissava nel compose i valori dettati dalla topologia (`DB_HOST: mariadb` e
**vincono** su quanto scritto nel `.env`, così un file copiato da una macchina simili) perché vincessero su un `.env` sbagliato, ma richiedeva gli anchor YAML
di sviluppo non rompe il container. (`x-app-env`, `<<:`) ed è stata scartata a favore della leggibilità.
Nota: perché questo funzioni, `www.conf` ha `clear_env = no`. Senza, php-fpm Il rovescio della medaglia: `DB_HOST` e `REDIS_HOST` devono valere `mariadb` e
sanifica l'ambiente e Laravel non vedrebbe nessuna variabile. `redis`, cioè i nomi dei servizi. Se qualcuno vi mette un indirizzo IP il
container non trova più il database. È documentato in `.env.example` e nel
README, ma è il punto fragile di questa scelta.
Nota: perché la configurazione da ambiente funzioni, `www.conf` ha
`clear_env = no`. Senza, php-fpm sanifica l'ambiente e Laravel non vedrebbe
nessuna variabile.
### Fail-fast sulle variabili mancanti ### Fail-fast sulle variabili mancanti
@@ -191,41 +197,42 @@ riavviare il container: `vendor/` e `public/build` vivono dentro l'immagine.
Dopo una modifica sostanziale, questo giro copre i percorsi critici: Dopo una modifica sostanziale, questo giro copre i percorsi critici:
Si usa il compose di **sviluppo**: quello di produzione richiede la rete C'è un solo `docker-compose.yml`, usato sia in produzione sia in locale. In
esterna `dokploy-network` e non pubblica porte sull'host, quindi non è locale serve creare una volta la rete esterna attesa da Dokploy.
verificabile in locale così com'è.
```bash ```bash
cp .env.example .env # se non c'è già cp .env.example .env
# nel .env: APP_URL=http://localhost:8080, APP_ENV=local,
# SESSION_SECURE_COOKIE=false, e le tre password
docker network create dokploy-network
# 1. L'immagine builda # 1. L'immagine builda
docker build -f docker/php/Dockerfile -t termanager2-check . docker build -f docker/php/Dockerfile -t termanager2-check .
# 2. Stack pulito da zero # 2. Stack pulito da zero
docker compose -f docker-compose.dev.yml down -v docker compose --profile dev down -v
docker compose -f docker-compose.dev.yml up -d --build docker compose --profile dev up -d --build
docker compose -f docker-compose.dev.yml ps # tutti i servizi healthy docker compose --profile dev ps # tutti i servizi healthy
# 3. L'applicazione risponde # 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/up # 200
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/login # 200 curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/login # 200
# 4. Il redeploy è idempotente # 4. Il redeploy è idempotente
docker compose -f docker-compose.dev.yml up -d --force-recreate app docker compose up -d --force-recreate app
docker compose -f docker-compose.dev.yml logs app | grep -E "Migrations|Seed" docker compose logs app | grep -E "Migrations|Seed"
# atteso: "Migrations applied" + "Seed already applied" # atteso: "Migrations applied" + "Seed already applied"
# 5. La cifratura funziona # 5. La cifratura funziona
docker compose -f docker-compose.dev.yml exec app php artisan tinker --execute=" docker compose exec app php artisan tinker --execute="
\$p = App\Models\Proclamatore::create(['nome'=>'Test','cognome'=>'Verifica','attivo'=>true]); \$p = App\Models\Proclamatore::create(['nome'=>'Test','cognome'=>'Verifica','attivo'=>true]);
\$raw = DB::table('proclamatori')->where('id',\$p->id)->value('nome'); \$raw = DB::table('proclamatori')->where('id',\$p->id)->value('nome');
echo \$raw === 'Test' ? 'ERRORE: dati in chiaro' : 'OK: cifrato'; echo \$raw === 'Test' ? 'ERRORE: dati in chiaro' : 'OK: cifrato';
" "
``` ```
Per provare davvero il compose di produzione serve creare la rete È lo stesso file che gira in produzione: l'unica differenza è il profilo `dev`
(`docker network create dokploy-network`) e aggiungere un override che pubblichi (che aggiunge Mailpit) e i valori nel `.env`.
la porta 80 del servizio `app`.
--- ---

126
README.md
View File

@@ -102,17 +102,29 @@ DB_PASSWORD=...
DB_ROOT_PASSWORD=... DB_ROOT_PASSWORD=...
REDIS_PASSWORD=... REDIS_PASSWORD=...
# --- Database --- # --- Database (mariadb e' il nome del servizio, non cambiarlo) ---
DB_CONNECTION=mysql
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=termanager2 DB_DATABASE=termanager2
DB_USERNAME=termanager2 DB_USERNAME=termanager2
# --- Redis (idem: redis e' il nome del servizio) ---
REDIS_HOST=redis
REDIS_PORT=6379
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# --- Primo amministratore (creato al primo avvio) --- # --- Primo amministratore (creato al primo avvio) ---
INITIAL_ADMIN_NAME=Nome Cognome INITIAL_ADMIN_NAME=Nome Cognome
INITIAL_ADMIN_EMAIL=admin@tuodominio.it INITIAL_ADMIN_EMAIL=admin@tuodominio.it
INITIAL_ADMIN_PASSWORD=UnaPasswordSolida INITIAL_ADMIN_PASSWORD=UnaPasswordSolida
# --- Sessioni --- # --- Sessioni e log ---
SESSION_SECURE_COOKIE=true SESSION_SECURE_COOKIE=true
LOG_CHANNEL=stderr
LOG_LEVEL=warning
# --- Posta (SMTP esterno) --- # --- Posta (SMTP esterno) ---
MAIL_MAILER=smtp MAIL_MAILER=smtp
@@ -186,63 +198,84 @@ Lo stack è Docker Compose standard e funziona su **Coolify**, **Portainer**,
Le uniche due particolarità sono: Le uniche due particolarità sono:
1. **La rete `dokploy-network`.** In fondo a `docker-compose.yml` c'è una rete 1. **Il file `.env`.** Dokploy lo genera dalle variabili della sua interfaccia.
esterna che serve al Traefik di Dokploy per raggiungere il container. Su Altrove crealo a mano: `cp .env.example .env` e compila i valori.
altri sistemi rimuovila e pubblica una porta:
```yaml 2. **La rete `dokploy-network`.** In fondo a `docker-compose.yml` c'è una rete
services: esterna che serve al Traefik di Dokploy per raggiungere il container. Hai
app: due possibilità:
ports:
- "8080:80"
networks:
- internal # rimuovi "- dokploy-network"
networks: - **Tenerla**, creandola una volta sola:
internal: `docker network create dokploy-network`
driver: bridge - **Rimuoverla**: elimina `- dokploy-network` dalle reti del servizio `app`
# rimuovi il blocco dokploy-network e il blocco `dokploy-network:` in fondo al file.
```
Poi metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta Poi imposta nel `.env` la porta su cui esporre l'applicazione e avvia:
`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](#passo-3--imposta-le-variabili-dambiente).
Dopodiché:
```bash ```bash
# ascolta su tutte le interfacce, per metterci davanti un reverse proxy
echo "APP_PORT=0.0.0.0:8080" >> .env
docker compose up -d --build docker compose up -d --build
docker compose logs -f app docker compose logs -f app
``` ```
Infine metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta
`8080`, con certificato TLS.
--- ---
## Sviluppo locale ## Sviluppo locale
Si usa lo stesso `docker-compose.yml` della produzione, con il profilo `dev`
che aggiunge Mailpit:
```bash ```bash
cp .env.example .env cp .env.example .env
docker compose -f docker-compose.dev.yml up -d --build docker network create dokploy-network # una volta sola
# nel .env, per l'uso locale:
# APP_URL=http://localhost:8080
# APP_ENV=local
# SESSION_SECURE_COOKIE=false
# MAIL_MAILER=smtp MAIL_HOST=mailpit MAIL_PORT=1025
# e le tre password a piacere
docker compose --profile dev up -d --build
``` ```
| Cosa | Dove | | Cosa | Dove |
|---------------------|-------------------------------| |---------------------|-------------------------------|
| Applicazione | http://localhost:8080 | | Applicazione | http://localhost:8080 |
| Mailpit (posta test)| http://localhost:8025 | | Mailpit (posta test)| http://localhost:8025 |
| Login | `admin@termanager2.local` / `password` | | Login | quello che hai messo in `INITIAL_ADMIN_*` |
A differenza della produzione, l'ambiente di sviluppo monta il codice sorgente Se la porta 8080 è occupata, cambia `APP_PORT` nel `.env`
in bind mount — le modifiche PHP sono immediate — e pubblica le porte (es. `APP_PORT=127.0.0.1:8081`).
sull'host.
Se la porta 8080 è occupata: ### Modifiche al codice senza ricostruire l'immagine
```bash L'immagine è autosufficiente: `vendor/` e `public/build` stanno dentro, quindi
APP_PORT=8081 docker compose -f docker-compose.dev.yml up -d di norma una modifica al codice richiede `docker compose up -d --build`.
Per avere invece le modifiche PHP immediate, crea un
`docker-compose.override.yml` — Docker Compose lo applica da solo, senza
bisogno di alcun flag:
```yaml
services:
app:
environment:
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1
volumes:
- ./:/var/www/html
# vendor e build restano quelli dell'immagine, non quelli dell'host
- /var/www/html/vendor
- /var/www/html/public/build
``` ```
Il file è già in `.gitignore`, quindi resta solo tuo.
Per lavorare sugli asset frontend con hot reload: Per lavorare sugli asset frontend con hot reload:
```bash ```bash
@@ -285,15 +318,20 @@ ridichiara le variabili applicative: le prende dal `.env` così com'è.
| `RUN_MIGRATIONS` | `true` | Migrazioni automatiche all'avvio | | `RUN_MIGRATIONS` | `true` | Migrazioni automatiche all'avvio |
| `RUN_DB_SEED_ON_FIRST_START` | `true` | Seed al primo avvio | | `RUN_DB_SEED_ON_FIRST_START` | `true` | Seed al primo avvio |
### Fissate dal compose ### Da non cambiare
Questi valori sono imposti da `docker-compose.yml` e **ignorano** quanto scritto Questi valori sono **nomi di servizi** definiti in `docker-compose.yml`, non
nel `.env`, perché descrivono la topologia dello stack: `DB_HOST`, `DB_PORT`, indirizzi di rete. Se li modifichi il container non trova più il database o
`REDIS_HOST`, `REDIS_PORT`, `CACHE_STORE`, `SESSION_DRIVER`, Redis:
`QUEUE_CONNECTION`, `LOG_CHANNEL`.
Serve a evitare che un `.env` copiato da una macchina di sviluppo (con, per ```dotenv
dire, `DB_HOST=127.0.0.1`) rompa il container. DB_HOST=mariadb
REDIS_HOST=redis
```
Nemmeno `CACHE_STORE`, `SESSION_DRIVER` e `QUEUE_CONNECTION` andrebbero
spostati altrove: lo stack fornisce Redis e il queue worker consuma la coda
Redis, quindi puntarli su un driver diverso interrompe l'elaborazione dei job.
--- ---
@@ -451,8 +489,8 @@ cifrati).
### Reset completo (solo sviluppo) ### Reset completo (solo sviluppo)
```bash ```bash
docker compose -f docker-compose.dev.yml down -v docker compose --profile dev down -v
docker compose -f docker-compose.dev.yml up -d --build docker compose --profile dev up -d --build
``` ```
`-v` cancella i volumi: database, Redis e file caricati vengono persi. `-v` cancella i volumi: database, Redis e file caricati vengono persi.
@@ -673,8 +711,8 @@ TerManager2/
│ └── supervisor/ # nginx + php-fpm nello stesso container │ └── supervisor/ # nginx + php-fpm nello stesso container
├── resources/ # CSS, JS, viste Blade ├── resources/ # CSS, JS, viste Blade
├── routes/web.php # Rotte con i relativi permessi ├── routes/web.php # Rotte con i relativi permessi
├── docker-compose.yml # Produzione ├── docker-compose.yml # Unico, produzione e sviluppo
├── docker-compose.dev.yml # Sviluppo locale ├── .env.example # Tutta la configurazione
├── HANDOFF.md # Stato del progetto per chi subentra ├── HANDOFF.md # Stato del progetto per chi subentra
└── README.md └── README.md
``` ```

View File

@@ -1,149 +0,0 @@
# =============================================================================
# TerManager2 — local development
#
# cp .env.example .env
# docker compose -f docker-compose.dev.yml up -d --build
# open http://localhost:8080 (Mailpit UI on http://localhost:8025)
#
# This is a standalone file, not an overlay: it bind-mounts the source so edits
# apply without a rebuild, publishes ports to the host, and swaps the external
# Traefik network for a local one. Production uses docker-compose.yml.
# =============================================================================
x-dev-env: &dev-env
APP_NAME: ${APP_NAME:-TerManager2}
APP_ENV: local
APP_DEBUG: "true"
APP_KEY: ${APP_KEY:-}
APP_URL: http://localhost:${APP_PORT:-8080}
APP_TIMEZONE: ${APP_TIMEZONE:-Europe/Rome}
DB_CONNECTION: mysql
DB_HOST: mariadb
DB_PORT: 3306
DB_DATABASE: ${DB_DATABASE:-termanager2}
DB_USERNAME: ${DB_USERNAME:-termanager2}
DB_PASSWORD: ${DB_PASSWORD:-secret}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD:-redissecret}
CACHE_STORE: redis
SESSION_DRIVER: redis
SESSION_SECURE_COOKIE: "false"
QUEUE_CONNECTION: redis
LOG_CHANNEL: stderr
LOG_LEVEL: debug
MAIL_MAILER: smtp
MAIL_HOST: mailpit
MAIL_PORT: 1025
MAIL_FROM_ADDRESS: noreply@termanager2.local
MAIL_FROM_NAME: TerManager2
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
ports:
- "${APP_PORT:-8080}:80"
environment:
<<: *dev-env
# Pick up source edits without restarting php-fpm.
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1
RUN_DB_SEED_ON_FIRST_START: "true"
SEED_DEV_DATA: ${SEED_DEV_DATA:-false}
INITIAL_ADMIN_NAME: ${INITIAL_ADMIN_NAME:-Amministratore}
INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-admin@termanager2.local}
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-password}
volumes:
# Source is bind-mounted for live edits. vendor/ and public/build stay as
# anonymous volumes so the host (which has neither) doesn't mask what the
# image built.
- ./:/var/www/html
- /var/www/html/vendor
- /var/www/html/public/build
networks:
- termanager2
depends_on:
mariadb:
condition: service_healthy
redis:
condition: service_healthy
queue-worker:
build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
environment:
<<: *dev-env
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1
volumes:
- ./:/var/www/html
- /var/www/html/vendor
- /var/www/html/public/build
networks:
- termanager2
depends_on:
app:
condition: service_healthy
entrypoint: ["queue-entrypoint.sh"]
command: ["php", "artisan", "queue:work", "redis", "--queue=default", "--sleep=3", "--tries=1", "--timeout=600"]
mariadb:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_DATABASE:-termanager2}
MARIADB_USER: ${DB_USERNAME:-termanager2}
MARIADB_PASSWORD: ${DB_PASSWORD:-secret}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootsecret}
ports:
# Exposed locally so you can attach a GUI client. Never in production.
- "${DB_PORT_HOST:-3306}:3306"
volumes:
- db_data_dev:/var/lib/mysql
networks:
- termanager2
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-redissecret}"]
volumes:
- redis_data_dev:/data
networks:
- termanager2
healthcheck:
test: ["CMD-SHELL", "redis-cli -a '${REDIS_PASSWORD:-redissecret}' ping | grep -q PONG"]
interval: 10s
timeout: 5s
retries: 10
mailpit:
image: axllent/mailpit:latest
restart: unless-stopped
ports:
- "${MAIL_PORT:-1025}:1025"
- "${MAILPIT_UI_PORT:-8025}:8025"
networks:
- termanager2
volumes:
db_data_dev:
redis_data_dev:
networks:
termanager2:
driver: bridge

View File

@@ -1,60 +1,37 @@
# ============================================================================= # =============================================================================
# TerManager2 — production stack (Dokploy) # TerManager2
# #
# The application image is self-contained: PHP dependencies and compiled # Un solo file per produzione e sviluppo. Tutta la configurazione sta nel file
# frontend assets are baked in at build time, and nginx runs alongside php-fpm # .env (su Dokploy lo genera l'interfaccia, in locale lo copi da .env.example).
# inside the app container. There are no bind mounts and no build steps at
# boot, so a redeploy is: build image, migrate, serve.
# #
# Configuration comes from the .env file, which Dokploy writes from the # Produzione (Dokploy): deploy dall'interfaccia, niente da fare qui
# variables you set in its UI. This file does not re-declare them — it only # Locale: docker compose --profile dev up -d --build
# pins the handful of values dictated by the topology below (service names,
# which datastores exist), because those must match this file, not .env.
# #
# For local development use docker-compose.dev.yml instead. # L'immagine e' autosufficiente: dipendenze PHP e asset frontend sono compilati
# durante il build, non all'avvio.
# ============================================================================= # =============================================================================
x-app: &app services:
# --- Applicazione (nginx + php-fpm) ---------------------------------------
app:
build: build:
context: . context: .
dockerfile: docker/php/Dockerfile dockerfile: docker/php/Dockerfile
restart: unless-stopped restart: unless-stopped
# required:false so `docker compose config` and a bare checkout still work; env_file: .env
# a missing .env then surfaces as the explicit DB_PASSWORD error below.
env_file:
- path: .env
required: false
volumes:
# Uploaded territory PDFs, thumbnails, logs and the persisted APP_KEY.
- storage_data:/var/www/html/storage
networks:
- internal
# Values that are a property of THIS compose file rather than of the
# deployment, so they override whatever .env happens to say. A DB_HOST of
# 127.0.0.1 left over from a local .env would otherwise break the container.
x-app-env: &app-env
DB_CONNECTION: mysql
DB_HOST: mariadb
DB_PORT: 3306
REDIS_HOST: redis
REDIS_PORT: 6379
# The stack ships Redis and the worker below consumes the redis queue;
# pointing these elsewhere would silently break job processing.
CACHE_STORE: redis
SESSION_DRIVER: redis
QUEUE_CONNECTION: redis
# Logs go to the container's stderr, where `docker logs` and the Dokploy
# log viewer can see them, rather than into a file inside the volume.
LOG_CHANNEL: stderr
services:
app:
<<: *app
environment: environment:
<<: *app-env # I sorgenti non cambiano dentro un'immagine gia' costruita.
# Sources never change in a built image, so skip the stat() on every hit.
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0 PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
ports:
# Di default ascolta solo su localhost: in produzione il traffico passa
# da Traefik sulla rete interna, non da questa porta. In locale invece
# apri http://localhost:8080.
# Per esporlo davvero: APP_PORT=0.0.0.0:8080 nel .env
- "${APP_PORT:-127.0.0.1:8080}:80"
volumes:
# PDF territori, miniature, log e chiave di fallback.
- storage_data:/var/www/html/storage
networks: networks:
- internal - internal
- dokploy-network - dokploy-network
@@ -64,18 +41,26 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
# --- Job in coda (import PDF massivi) -------------------------------------
queue-worker: queue-worker:
<<: *app build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
user: www-data user: www-data
environment: env_file: .env
<<: *app-env volumes:
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0 - storage_data:/var/www/html/storage
networks:
- internal
depends_on: depends_on:
app: app:
condition: service_healthy condition: service_healthy
# Non esegue migrazioni: le fa solo il container app, per evitare che due
# container in gara lascino uno schema applicato a meta'.
entrypoint: ["queue-entrypoint.sh"] entrypoint: ["queue-entrypoint.sh"]
# --max-time recycles the worker hourly so a leaked handle or grown heap # --max-time ricicla il worker ogni ora, cosi' un handle non rilasciato o
# can't accumulate indefinitely; restarting it is Docker's job. # la memoria cresciuta non si accumulano indefinitamente.
command: command:
- php - php
- artisan - artisan
@@ -86,7 +71,7 @@ services:
- --tries=3 - --tries=3
- --timeout=600 - --timeout=600
- --max-time=3600 - --max-time=3600
# Overrides the image's nginx-based HEALTHCHECK (no web server here). # Sostituisce l'healthcheck dell'immagine, che interroga nginx: qui non c'e'.
healthcheck: healthcheck:
test: ["CMD-SHELL", "pgrep -f 'queue:work' > /dev/null || exit 1"] test: ["CMD-SHELL", "pgrep -f 'queue:work' > /dev/null || exit 1"]
interval: 30s interval: 30s
@@ -94,21 +79,25 @@ services:
retries: 3 retries: 3
start_period: 60s start_period: 60s
# --- Task pianificati (pulizia audit log) ---------------------------------
scheduler: scheduler:
<<: *app build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
user: www-data user: www-data
environment: env_file: .env
<<: *app-env volumes:
- storage_data:/var/www/html/storage
networks:
- internal
depends_on: depends_on:
app: app:
condition: service_healthy condition: service_healthy
entrypoint: ["queue-entrypoint.sh"] entrypoint: ["queue-entrypoint.sh"]
# `schedule:work` is the container-native replacement for a host crontab — # Sostituisce il crontab di sistema: si sveglia ogni minuto ed esegue
# it wakes every minute and dispatches whatever routes/console.php defines # quanto definito in routes/console.php.
# (currently the audit-log cleanup).
command: ["php", "artisan", "schedule:work"] command: ["php", "artisan", "schedule:work"]
# Same reason as above: this container runs neither nginx nor php-fpm, so
# the image's HTTP health check would mark it unhealthy forever.
healthcheck: healthcheck:
test: ["CMD-SHELL", "pgrep -f 'schedule:work' > /dev/null || exit 1"] test: ["CMD-SHELL", "pgrep -f 'schedule:work' > /dev/null || exit 1"]
interval: 30s interval: 30s
@@ -116,18 +105,17 @@ services:
retries: 3 retries: 3
start_period: 60s start_period: 60s
# --- Database -------------------------------------------------------------
mariadb: mariadb:
image: mariadb:11 image: mariadb:11
restart: unless-stopped restart: unless-stopped
# Interpolated (not env_file) because these are the *image's* own bootstrap # Le ":?" fanno fallire subito il deploy se manca una password, invece di
# variables under different names. The `:?` guards are what make a deploy # avviare un database di produzione con credenziali di default.
# with no configured passwords fail loudly instead of coming up with
# whatever default happened to be lying around.
environment: environment:
MARIADB_DATABASE: ${DB_DATABASE:-termanager2} MARIADB_DATABASE: ${DB_DATABASE:-termanager2}
MARIADB_USER: ${DB_USERNAME:-termanager2} MARIADB_USER: ${DB_USERNAME:-termanager2}
MARIADB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} MARIADB_PASSWORD: ${DB_PASSWORD:?manca DB_PASSWORD nel .env}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required} MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?manca DB_ROOT_PASSWORD nel .env}
MARIADB_AUTO_UPGRADE: "1" MARIADB_AUTO_UPGRADE: "1"
command: command:
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
@@ -144,20 +132,21 @@ services:
retries: 10 retries: 10
start_period: 30s start_period: 30s
# --- Cache, sessioni, code ------------------------------------------------
redis: redis:
image: redis:7-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
environment: environment:
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required} REDIS_PASSWORD: ${REDIS_PASSWORD:?manca REDIS_PASSWORD nel .env}
command: command:
- redis-server - redis-server
- --appendonly - --appendonly
- "yes" - "yes"
- --requirepass - --requirepass
- ${REDIS_PASSWORD:?REDIS_PASSWORD is required} - ${REDIS_PASSWORD:?manca REDIS_PASSWORD nel .env}
- --maxmemory - --maxmemory
- 256mb - 256mb
# Sessions and cache live here; evict volatile keys rather than OOM. # Qui vivono sessioni e cache: meglio sfrattare chiavi che esaurire memoria.
- --maxmemory-policy - --maxmemory-policy
- allkeys-lru - allkeys-lru
volumes: volumes:
@@ -171,16 +160,28 @@ services:
retries: 10 retries: 10
start_period: 10s start_period: 10s
# --- Posta di test (solo sviluppo) ----------------------------------------
# Parte solo con --profile dev, mai in produzione.
mailpit:
image: axllent/mailpit:latest
profiles: ["dev"]
restart: unless-stopped
ports:
- "${MAILPIT_UI_PORT:-127.0.0.1:8025}:8025"
networks:
- internal
volumes: volumes:
db_data: db_data:
redis_data: redis_data:
storage_data: storage_data:
networks: networks:
# Datastores stay on an isolated network with no route to the outside. # Database e Redis restano qui, senza rotta verso l'esterno.
internal: internal:
driver: bridge driver: bridge
# Dokploy's shared Traefik network. Only `app` joins it, and Dokploy attaches # Rete condivisa del Traefik di Dokploy: e' cosi' che il dominio raggiunge
# the routing labels when you assign a domain to that service in the UI. # il container. In locale creala una volta con:
# docker network create dokploy-network
dokploy-network: dokploy-network:
external: true external: true