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.
# 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

View File

@@ -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
View File

@@ -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
```

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
# 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