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