From afaaeb59279623ab783fcfe1fca517f54345d5ff Mon Sep 17 00:00:00 2001 From: francesco Date: Sun, 19 Jul 2026 17:41:48 +0200 Subject: [PATCH] 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 --- .env.example | 75 +++++++++++++-------- HANDOFF.md | 51 ++++++++------ README.md | 126 ++++++++++++++++++++++------------ docker-compose.dev.yml | 149 ----------------------------------------- docker-compose.yml | 149 +++++++++++++++++++++-------------------- 5 files changed, 232 insertions(+), 318 deletions(-) delete mode 100644 docker-compose.dev.yml diff --git a/.env.example b/.env.example index 60deb9d..dbd96b3 100644 --- a/.env.example +++ b/.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 diff --git a/HANDOFF.md b/HANDOFF.md index 3cad02f..12cd74c 100644 --- a/HANDOFF.md +++ b/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`. --- diff --git a/README.md b/README.md index 5219bf9..1ff5af3 100644 --- a/README.md +++ b/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 ``` diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 88c72ef..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index ba8f405..2b7c415 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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