Il Dockerfile compilava l'applicazione in /app-src mentre il runtime girava da /var/www/html, popolato solo dal bind mount del sorgente: l'immagine costruita non veniva mai usata. L'entrypoint rieseguiva composer install e npm run build a ogni avvio, dentro il container. Su Dokploy, senza sorgente sull'host, questo significava build a runtime a ogni deploy. Ora vendor/ e public/build sono compilati in stage separati e copiati nell'immagine finale. I container di produzione non montano il sorgente e non eseguono build all'avvio: un redeploy e' build, migrazioni, online. - Dockerfile multi-stage (composer, node, runtime) - nginx e php-fpm nello stesso container via supervisor: con container separati nginx avrebbe bisogno di una copia di public/ e del volume storage solo per servire file statici - clear_env = no in www.conf, senza cui php-fpm nasconde a Laravel tutte le variabili d'ambiente - configurazione da ambiente di processo invece di .env riscritto con sed; il compose fissa solo i valori dettati dalla topologia - fail-fast su APP_URL, DB_PASSWORD, DB_ROOT_PASSWORD, REDIS_PASSWORD - servizio scheduler dedicato: schedule:work non veniva mai eseguito, quindi la pulizia dell'audit log non e' mai partita - solo il container app esegue le migrazioni, per evitare che due container in gara lascino uno schema applicato a meta' - healthcheck separati per worker e scheduler, che non espongono HTTP - docker-compose.dev.yml per lo sviluppo locale - documentazione unificata in README.md e HANDOFF.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
187 lines
5.9 KiB
YAML
187 lines
5.9 KiB
YAML
# =============================================================================
|
|
# TerManager2 — production stack (Dokploy)
|
|
#
|
|
# 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.
|
|
#
|
|
# 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.
|
|
#
|
|
# For local development use docker-compose.dev.yml instead.
|
|
# =============================================================================
|
|
|
|
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:
|
|
app:
|
|
<<: *app
|
|
environment:
|
|
<<: *app-env
|
|
# Sources never change in a built image, so skip the stat() on every hit.
|
|
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
|
|
networks:
|
|
- internal
|
|
- dokploy-network
|
|
depends_on:
|
|
mariadb:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
queue-worker:
|
|
<<: *app
|
|
user: www-data
|
|
environment:
|
|
<<: *app-env
|
|
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
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.
|
|
command:
|
|
- php
|
|
- artisan
|
|
- queue:work
|
|
- redis
|
|
- --queue=default
|
|
- --sleep=3
|
|
- --tries=3
|
|
- --timeout=600
|
|
- --max-time=3600
|
|
# Overrides the image's nginx-based HEALTHCHECK (no web server here).
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pgrep -f 'queue:work' > /dev/null || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 60s
|
|
|
|
scheduler:
|
|
<<: *app
|
|
user: www-data
|
|
environment:
|
|
<<: *app-env
|
|
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).
|
|
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
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 60s
|
|
|
|
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.
|
|
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_AUTO_UPGRADE: "1"
|
|
command:
|
|
- --character-set-server=utf8mb4
|
|
- --collation-server=utf8mb4_unicode_ci
|
|
- --innodb-buffer-pool-size=256M
|
|
volumes:
|
|
- db_data:/var/lib/mysql
|
|
networks:
|
|
- internal
|
|
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
|
|
environment:
|
|
REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
|
command:
|
|
- redis-server
|
|
- --appendonly
|
|
- "yes"
|
|
- --requirepass
|
|
- ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
|
- --maxmemory
|
|
- 256mb
|
|
# Sessions and cache live here; evict volatile keys rather than OOM.
|
|
- --maxmemory-policy
|
|
- allkeys-lru
|
|
volumes:
|
|
- redis_data:/data
|
|
networks:
|
|
- internal
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 10s
|
|
|
|
volumes:
|
|
db_data:
|
|
redis_data:
|
|
storage_data:
|
|
|
|
networks:
|
|
# Datastores stay on an isolated network with no route to the outside.
|
|
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.
|
|
dokploy-network:
|
|
external: true
|