Ristruttura il deploy: immagine autosufficiente per Dokploy
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>
This commit is contained in:
@@ -1,132 +1,186 @@
|
||||
# =============================================================================
|
||||
# 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:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
container_name: termanager2_app
|
||||
restart: unless-stopped
|
||||
# Runs as root so the entrypoint can mkdir/chown the bind-mounted storage;
|
||||
# php-fpm workers drop to www-data via docker/php/www.conf.
|
||||
working_dir: /var/www/html
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
<<: *app
|
||||
environment:
|
||||
<<: *app-env
|
||||
# Sources never change in a built image, so skip the stat() on every hit.
|
||||
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
|
||||
networks:
|
||||
- termanager2
|
||||
- internal
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "nc -z 127.0.0.1 9000"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 45s
|
||||
environment:
|
||||
- PHP_OPCACHE_VALIDATE_TIMESTAMPS=1
|
||||
# DB/Redis creds must be passed into the container so the entrypoint can
|
||||
# upsert them into .env. Defaults match the mariadb/redis services below.
|
||||
- DB_HOST=${DB_HOST:-mariadb}
|
||||
- DB_DATABASE=${DB_DATABASE:-termanager2}
|
||||
- DB_USERNAME=${DB_USERNAME:-termanager2}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-secret}
|
||||
- REDIS_HOST=${REDIS_HOST:-redis}
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-redissecret}
|
||||
- SEED_DEV_DATA=${SEED_DEV_DATA:-false}
|
||||
- RUN_DB_SEED_ON_FIRST_START=${RUN_DB_SEED_ON_FIRST_START:-true}
|
||||
- ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=${ENSURE_INITIAL_ADMIN_ON_EMPTY_DB:-true}
|
||||
- INITIAL_ADMIN_NAME=${INITIAL_ADMIN_NAME:-}
|
||||
- INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-}
|
||||
- INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:-}
|
||||
|
||||
nginx:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/nginx/Dockerfile
|
||||
container_name: termanager2_nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${APP_PORT:-8080}:80"
|
||||
volumes:
|
||||
- ./:/var/www/html:ro
|
||||
networks:
|
||||
- termanager2
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
|
||||
queue-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/php/Dockerfile
|
||||
container_name: termanager2_queue
|
||||
restart: unless-stopped
|
||||
# Same UID as php-fpm workers, so storage files stay consistent.
|
||||
user: "www-data"
|
||||
working_dir: /var/www/html
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
networks:
|
||||
- termanager2
|
||||
<<: *app
|
||||
user: www-data
|
||||
environment:
|
||||
<<: *app-env
|
||||
PHP_OPCACHE_VALIDATE_TIMESTAMPS: 0
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
entrypoint: ["php"]
|
||||
command: ["artisan", "queue:work", "redis", "--queue=default", "--sleep=1", "--tries=1", "--timeout=0"]
|
||||
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:
|
||||
- PHP_OPCACHE_VALIDATE_TIMESTAMPS=1
|
||||
<<: *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
|
||||
container_name: termanager2_db
|
||||
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:-secret}
|
||||
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootsecret}
|
||||
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:
|
||||
- termanager2
|
||||
- internal
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: termanager2_redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redissecret}
|
||||
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:
|
||||
- termanager2
|
||||
- internal
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redissecret}", "ping"]
|
||||
test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
mailpit:
|
||||
image: axllent/mailpit:latest
|
||||
container_name: termanager2_mail
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${MAIL_PORT:-1025}:1025"
|
||||
- "${MAILPIT_UI_PORT:-8025}:8025"
|
||||
networks:
|
||||
- termanager2
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
storage_data:
|
||||
|
||||
networks:
|
||||
termanager2:
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user