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:
75
.env.example
75
.env.example
@@ -1,74 +1,91 @@
|
||||
# =============================================================================
|
||||
# TerManager2 — environment template
|
||||
# TerManager2 — configurazione
|
||||
#
|
||||
# LOCAL DEVELOPMENT: copy to .env and adjust.
|
||||
# cp .env.example .env && docker compose -f docker-compose.dev.yml up -d
|
||||
# Questo file e' l'UNICA fonte di configurazione: docker-compose.yml lo legge
|
||||
# cosi' com'e' e non ridichiara nulla.
|
||||
#
|
||||
# PRODUCTION (Dokploy): do NOT ship a .env file. Set these as environment
|
||||
# variables in the Dokploy UI instead — the container reads them directly.
|
||||
# Variables marked [REQUIRED] have no safe default and the stack will refuse
|
||||
# to start without them.
|
||||
# In locale: cp .env.example .env e modifica quello che serve
|
||||
# Su Dokploy: incolla queste variabili in Environment, il .env lo
|
||||
# genera lui
|
||||
#
|
||||
# Le variabili marcate [OBBLIGATORIA] non hanno default: lo stack si rifiuta
|
||||
# di partire senza.
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Applicazione
|
||||
# -----------------------------------------------------------------------------
|
||||
APP_NAME=TerManager2
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_TIMEZONE=Europe/Rome
|
||||
|
||||
# [REQUIRED] Generate with: php artisan key:generate --show
|
||||
# Proclamatori personal data is encrypted with this key. If it changes or is
|
||||
# lost, that data becomes permanently unreadable. Back it up.
|
||||
APP_KEY=
|
||||
|
||||
# [REQUIRED] Public URL, including scheme. Used for links in PDFs and emails,
|
||||
# and to validate the incoming Host header.
|
||||
# [OBBLIGATORIA] Deve combaciare ESATTAMENTE col dominio servito, schema
|
||||
# incluso. L'app rifiuta richieste con un Host diverso (protezione contro
|
||||
# l'host header injection): un valore sbagliato da' 400 su tutto.
|
||||
# In locale: http://localhost:8080
|
||||
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_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_EMAIL=
|
||||
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_DB_SEED_ON_FIRST_START=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_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=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_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_PORT=6379
|
||||
# [REQUIRED] openssl rand -base64 24
|
||||
|
||||
# [OBBLIGATORIA] openssl rand -base64 24
|
||||
REDIS_PASSWORD=
|
||||
|
||||
CACHE_STORE=redis
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_SECURE_COOKIE=true
|
||||
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_HOST=
|
||||
MAIL_PORT=587
|
||||
@@ -79,10 +96,10 @@ MAIL_FROM_ADDRESS=noreply@example.org
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Local development only — ignored in production
|
||||
# Porte
|
||||
# -----------------------------------------------------------------------------
|
||||
# Host port for docker-compose.dev.yml. In production Traefik handles routing
|
||||
# and no port is published.
|
||||
APP_PORT=8080
|
||||
MAILPIT_UI_PORT=8025
|
||||
SEED_DEV_DATA=false
|
||||
# Di default l'app ascolta solo su localhost. In produzione va bene cosi': il
|
||||
# traffico arriva da Traefik sulla rete interna, non da questa porta.
|
||||
# Per esporla davvero (server senza reverse proxy): APP_PORT=0.0.0.0:8080
|
||||
APP_PORT=127.0.0.1:8080
|
||||
MAILPIT_UI_PORT=127.0.0.1:8025
|
||||
|
||||
51
HANDOFF.md
51
HANDOFF.md
@@ -19,7 +19,7 @@ redeploy, login, tutte le pagine, code e scheduler.
|
||||
| Deploy Dokploy (Compose) | ✅ verificato |
|
||||
| Primo avvio a zero-config | ✅ migrazioni + seed + admin automatici |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
@@ -62,16 +62,22 @@ 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.
|
||||
legge direttamente l'ambiente di processo, e `docker-compose.yml` si limita a
|
||||
`env_file: .env` senza ridichiarare nulla.
|
||||
|
||||
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.
|
||||
Il `.env` è quindi l'**unica** fonte di configurazione. Una versione precedente
|
||||
fissava nel compose i valori dettati dalla topologia (`DB_HOST: mariadb` e
|
||||
simili) perché vincessero su un `.env` sbagliato, ma richiedeva gli anchor YAML
|
||||
(`x-app-env`, `<<:`) ed è stata scartata a favore della leggibilità.
|
||||
|
||||
Nota: perché questo funzioni, `www.conf` ha `clear_env = no`. Senza, php-fpm
|
||||
sanifica l'ambiente e Laravel non vedrebbe nessuna variabile.
|
||||
Il rovescio della medaglia: `DB_HOST` e `REDIS_HOST` devono valere `mariadb` e
|
||||
`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
|
||||
|
||||
@@ -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:
|
||||
|
||||
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'è.
|
||||
C'è un solo `docker-compose.yml`, usato sia in produzione sia in locale. In
|
||||
locale serve creare una volta la rete esterna attesa da Dokploy.
|
||||
|
||||
```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
|
||||
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
|
||||
docker compose --profile dev down -v
|
||||
docker compose --profile dev up -d --build
|
||||
docker compose --profile dev 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"
|
||||
docker compose up -d --force-recreate app
|
||||
docker compose 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="
|
||||
docker compose 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`.
|
||||
È lo stesso file che gira in produzione: l'unica differenza è il profilo `dev`
|
||||
(che aggiunge Mailpit) e i valori nel `.env`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
126
README.md
126
README.md
@@ -102,17 +102,29 @@ DB_PASSWORD=...
|
||||
DB_ROOT_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_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) ---
|
||||
INITIAL_ADMIN_NAME=Nome Cognome
|
||||
INITIAL_ADMIN_EMAIL=admin@tuodominio.it
|
||||
INITIAL_ADMIN_PASSWORD=UnaPasswordSolida
|
||||
|
||||
# --- Sessioni ---
|
||||
# --- Sessioni e log ---
|
||||
SESSION_SECURE_COOKIE=true
|
||||
LOG_CHANNEL=stderr
|
||||
LOG_LEVEL=warning
|
||||
|
||||
# --- Posta (SMTP esterno) ---
|
||||
MAIL_MAILER=smtp
|
||||
@@ -186,63 +198,84 @@ Lo stack è Docker Compose standard e funziona su **Coolify**, **Portainer**,
|
||||
|
||||
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:
|
||||
1. **Il file `.env`.** Dokploy lo genera dalle variabili della sua interfaccia.
|
||||
Altrove crealo a mano: `cp .env.example .env` e compila i valori.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
ports:
|
||||
- "8080:80"
|
||||
networks:
|
||||
- internal # rimuovi "- dokploy-network"
|
||||
2. **La rete `dokploy-network`.** In fondo a `docker-compose.yml` c'è una rete
|
||||
esterna che serve al Traefik di Dokploy per raggiungere il container. Hai
|
||||
due possibilità:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: bridge
|
||||
# rimuovi il blocco dokploy-network
|
||||
```
|
||||
- **Tenerla**, creandola una volta sola:
|
||||
`docker network create dokploy-network`
|
||||
- **Rimuoverla**: elimina `- dokploy-network` dalle reti del servizio `app`
|
||||
e il blocco `dokploy-network:` in fondo al file.
|
||||
|
||||
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](#passo-3--imposta-le-variabili-dambiente).
|
||||
|
||||
Dopodiché:
|
||||
Poi imposta nel `.env` la porta su cui esporre l'applicazione e avvia:
|
||||
|
||||
```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 logs -f app
|
||||
```
|
||||
|
||||
Infine metti il tuo reverse proxy (Nginx, Caddy, Traefik) davanti alla porta
|
||||
`8080`, con certificato TLS.
|
||||
|
||||
---
|
||||
|
||||
## Sviluppo locale
|
||||
|
||||
Si usa lo stesso `docker-compose.yml` della produzione, con il profilo `dev`
|
||||
che aggiunge Mailpit:
|
||||
|
||||
```bash
|
||||
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 |
|
||||
|---------------------|-------------------------------|
|
||||
| Applicazione | http://localhost:8080 |
|
||||
| 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
|
||||
in bind mount — le modifiche PHP sono immediate — e pubblica le porte
|
||||
sull'host.
|
||||
Se la porta 8080 è occupata, cambia `APP_PORT` nel `.env`
|
||||
(es. `APP_PORT=127.0.0.1:8081`).
|
||||
|
||||
Se la porta 8080 è occupata:
|
||||
### Modifiche al codice senza ricostruire l'immagine
|
||||
|
||||
```bash
|
||||
APP_PORT=8081 docker compose -f docker-compose.dev.yml up -d
|
||||
L'immagine è autosufficiente: `vendor/` e `public/build` stanno dentro, quindi
|
||||
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:
|
||||
|
||||
```bash
|
||||
@@ -285,15 +318,20 @@ ridichiara le variabili applicative: le prende dal `.env` così com'è.
|
||||
| `RUN_MIGRATIONS` | `true` | Migrazioni automatiche all'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
|
||||
nel `.env`, perché descrivono la topologia dello stack: `DB_HOST`, `DB_PORT`,
|
||||
`REDIS_HOST`, `REDIS_PORT`, `CACHE_STORE`, `SESSION_DRIVER`,
|
||||
`QUEUE_CONNECTION`, `LOG_CHANNEL`.
|
||||
Questi valori sono **nomi di servizi** definiti in `docker-compose.yml`, non
|
||||
indirizzi di rete. Se li modifichi il container non trova più il database o
|
||||
Redis:
|
||||
|
||||
Serve a evitare che un `.env` copiato da una macchina di sviluppo (con, per
|
||||
dire, `DB_HOST=127.0.0.1`) rompa il container.
|
||||
```dotenv
|
||||
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)
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml down -v
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
docker compose --profile dev down -v
|
||||
docker compose --profile dev up -d --build
|
||||
```
|
||||
|
||||
`-v` cancella i volumi: database, Redis e file caricati vengono persi.
|
||||
@@ -673,8 +711,8 @@ TerManager2/
|
||||
│ └── 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
|
||||
├── docker-compose.yml # Unico, produzione e sviluppo
|
||||
├── .env.example # Tutta la configurazione
|
||||
├── HANDOFF.md # Stato del progetto per chi subentra
|
||||
└── README.md
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -1,60 +1,37 @@
|
||||
# =============================================================================
|
||||
# TerManager2 — production stack (Dokploy)
|
||||
# TerManager2
|
||||
#
|
||||
# The application image is self-contained: PHP dependencies and compiled
|
||||
# frontend assets are baked in at build time, and nginx runs alongside php-fpm
|
||||
# inside the app container. There are no bind mounts and no build steps at
|
||||
# boot, so a redeploy is: build image, migrate, serve.
|
||||
# Un solo file per produzione e sviluppo. Tutta la configurazione sta nel file
|
||||
# .env (su Dokploy lo genera l'interfaccia, in locale lo copi da .env.example).
|
||||
#
|
||||
# Configuration comes from the .env file, which Dokploy writes from the
|
||||
# variables you set in its UI. This file does not re-declare them — it only
|
||||
# pins the handful of values dictated by the topology below (service names,
|
||||
# which datastores exist), because those must match this file, not .env.
|
||||
# Produzione (Dokploy): deploy dall'interfaccia, niente da fare qui
|
||||
# Locale: docker compose --profile dev up -d --build
|
||||
#
|
||||
# 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
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
restart: unless-stopped
|
||||
# required:false so `docker compose config` and a bare checkout still work;
|
||||
# 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:
|
||||
|
||||
# --- Applicazione (nginx + php-fpm) ---------------------------------------
|
||||
app:
|
||||
<<: *app
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
<<: *app-env
|
||||
# Sources never change in a built image, so skip the stat() on every hit.
|
||||
# I sorgenti non cambiano dentro un'immagine gia' costruita.
|
||||
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:
|
||||
- internal
|
||||
- dokploy-network
|
||||
@@ -64,18 +41,26 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
# --- Job in coda (import PDF massivi) -------------------------------------
|
||||
queue-worker:
|
||||
<<: *app
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
restart: unless-stopped
|
||||
user: www-data
|
||||
environment:
|
||||
<<: *app-env
|
||||
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
|
||||
env_file: .env
|
||||
volumes:
|
||||
- storage_data:/var/www/html/storage
|
||||
networks:
|
||||
- internal
|
||||
depends_on:
|
||||
app:
|
||||
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"]
|
||||
# --max-time recycles the worker hourly so a leaked handle or grown heap
|
||||
# can't accumulate indefinitely; restarting it is Docker's job.
|
||||
# --max-time ricicla il worker ogni ora, cosi' un handle non rilasciato o
|
||||
# la memoria cresciuta non si accumulano indefinitamente.
|
||||
command:
|
||||
- php
|
||||
- artisan
|
||||
@@ -86,7 +71,7 @@ services:
|
||||
- --tries=3
|
||||
- --timeout=600
|
||||
- --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:
|
||||
test: ["CMD-SHELL", "pgrep -f 'queue:work' > /dev/null || exit 1"]
|
||||
interval: 30s
|
||||
@@ -94,21 +79,25 @@ services:
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# --- Task pianificati (pulizia audit log) ---------------------------------
|
||||
scheduler:
|
||||
<<: *app
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
restart: unless-stopped
|
||||
user: www-data
|
||||
environment:
|
||||
<<: *app-env
|
||||
env_file: .env
|
||||
volumes:
|
||||
- storage_data:/var/www/html/storage
|
||||
networks:
|
||||
- internal
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
entrypoint: ["queue-entrypoint.sh"]
|
||||
# `schedule:work` is the container-native replacement for a host crontab —
|
||||
# it wakes every minute and dispatches whatever routes/console.php defines
|
||||
# (currently the audit-log cleanup).
|
||||
# Sostituisce il crontab di sistema: si sveglia ogni minuto ed esegue
|
||||
# quanto definito in routes/console.php.
|
||||
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:
|
||||
test: ["CMD-SHELL", "pgrep -f 'schedule:work' > /dev/null || exit 1"]
|
||||
interval: 30s
|
||||
@@ -116,18 +105,17 @@ services:
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# --- Database -------------------------------------------------------------
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
restart: unless-stopped
|
||||
# Interpolated (not env_file) because these are the *image's* own bootstrap
|
||||
# variables under different names. The `:?` guards are what make a deploy
|
||||
# with no configured passwords fail loudly instead of coming up with
|
||||
# whatever default happened to be lying around.
|
||||
# Le ":?" fanno fallire subito il deploy se manca una password, invece di
|
||||
# avviare un database di produzione con credenziali di default.
|
||||
environment:
|
||||
MARIADB_DATABASE: ${DB_DATABASE:-termanager2}
|
||||
MARIADB_USER: ${DB_USERNAME:-termanager2}
|
||||
MARIADB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
||||
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required}
|
||||
MARIADB_PASSWORD: ${DB_PASSWORD:?manca DB_PASSWORD nel .env}
|
||||
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?manca DB_ROOT_PASSWORD nel .env}
|
||||
MARIADB_AUTO_UPGRADE: "1"
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
@@ -144,20 +132,21 @@ services:
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
# --- Cache, sessioni, code ------------------------------------------------
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?manca REDIS_PASSWORD nel .env}
|
||||
command:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- "yes"
|
||||
- --requirepass
|
||||
- ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
||||
- ${REDIS_PASSWORD:?manca REDIS_PASSWORD nel .env}
|
||||
- --maxmemory
|
||||
- 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
|
||||
- allkeys-lru
|
||||
volumes:
|
||||
@@ -171,16 +160,28 @@ services:
|
||||
retries: 10
|
||||
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:
|
||||
db_data:
|
||||
redis_data:
|
||||
storage_data:
|
||||
|
||||
networks:
|
||||
# Datastores stay on an isolated network with no route to the outside.
|
||||
# Database e Redis restano qui, senza rotta verso l'esterno.
|
||||
internal:
|
||||
driver: bridge
|
||||
# Dokploy's shared Traefik network. Only `app` joins it, and Dokploy attaches
|
||||
# the routing labels when you assign a domain to that service in the UI.
|
||||
# Rete condivisa del Traefik di Dokploy: e' cosi' che il dominio raggiunge
|
||||
# il container. In locale creala una volta con:
|
||||
# docker network create dokploy-network
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
Reference in New Issue
Block a user