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:
2026-07-19 16:49:10 +02:00
parent abd6e5c21b
commit cf78550516
14 changed files with 1657 additions and 1698 deletions

View File

@@ -1,23 +1,74 @@
FROM php:8.3-fpm
# syntax=docker/dockerfile:1
# System dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
netcat-openbsd \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
libzip-dev \
libicu-dev \
zip \
unzip \
supervisor \
poppler-utils \
# =============================================================================
# Stage 1 — PHP dependencies (no dev, production autoloader)
# =============================================================================
FROM composer:2 AS vendor
WORKDIR /app
# --no-scripts: artisan isn't available yet, package discovery would fail.
# Autoload is optimized in the runtime stage once the full source is present.
COPY composer.json composer.lock ./
RUN composer install \
--no-interaction \
--no-dev \
--prefer-dist \
--no-progress \
--no-scripts \
--ignore-platform-reqs
# =============================================================================
# Stage 2 — Frontend assets (Vite + Tailwind)
# =============================================================================
FROM node:20-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json* ./
RUN if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
else \
npm install --no-audit --no-fund; \
fi
# Tailwind 4 scans blade/php sources for class names, so they must be present.
COPY vite.config.js ./
COPY resources ./resources
COPY app ./app
COPY config ./config
RUN npm run build
# =============================================================================
# Stage 3 — Runtime
# =============================================================================
FROM php:8.3-fpm AS runtime
# nginx + supervisor run in-image alongside php-fpm: a separate nginx container
# would need its own copy of public/ plus the storage volume just to serve
# static files, which is more moving parts than this app warrants.
# poppler-utils provides pdftoppm/pdfinfo used by TerritorioThumbnailService.
# Node is deliberately absent here: assets are built in stage 2.
# The -dev packages are kept rather than purged. Their runtime counterparts
# are named per Debian release (libicu72 vs libicu76, libzip4 vs libzip5), so
# purging means pinning package names that break whenever the php base image
# moves to a new Debian. A few tens of MB is worth not having that breakage.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
netcat-openbsd \
procps \
nginx \
supervisor \
poppler-utils \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
libzip-dev \
libicu-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install \
&& docker-php-ext-install -j"$(nproc)" \
pdo_mysql \
mbstring \
exif \
@@ -32,52 +83,47 @@ RUN apt-get update && apt-get install -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Composer
# Kept only so `composer dump-autoload` can run below; harmless at runtime.
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Install Node.js 20 LTS
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini
COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY docker/nginx/default.conf /etc/nginx/sites-available/default
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Build application in staging area
WORKDIR /app-src
# Install PHP dependencies first for better Docker layer caching
COPY composer.json composer.lock ./
RUN composer install --no-interaction --prefer-dist --no-progress --no-scripts
# Install Node dependencies first for better Docker layer caching
COPY package.json package-lock.json* ./
RUN if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
else \
npm install --no-audit --no-fund; \
fi
# Copy full source and build frontend assets
COPY . .
RUN mkdir -p bootstrap/cache storage/framework/cache storage/framework/sessions storage/framework/views storage/logs storage/app
RUN if [ ! -f .env ] && [ -f .env.example ]; then cp .env.example .env; fi
RUN date -u +%Y%m%d%H%M%S > .image-build-id
RUN composer dump-autoload --optimize --no-interaction
RUN npm run build
# Runtime working directory
WORKDIR /var/www/html
# Copy PHP configuration
COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini
# Everything the app needs is baked into the image. Runtime containers do NOT
# bind-mount the source, so there is no composer/npm work at boot.
COPY --chown=www-data:www-data . .
COPY --from=vendor --chown=www-data:www-data /app/vendor ./vendor
COPY --from=assets --chown=www-data:www-data /app/public/build ./public/build
# Copy PHP-FPM pool config
COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
# Writable paths. storage/ is replaced by a named volume at runtime; this is
# the skeleton the entrypoint repopulates when that volume starts out empty.
RUN mkdir -p \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
storage/app/public \
bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R 775 storage bootstrap/cache
RUN composer dump-autoload --optimize --no-dev --no-interaction \
&& php artisan package:discover --ansi
# Copy entrypoint
COPY docker/php/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
COPY docker/php/queue-entrypoint.sh /usr/local/bin/queue-entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/queue-entrypoint.sh
EXPOSE 9000
EXPOSE 80
# /up is Laravel's built-in health endpoint (registered in bootstrap/app.php),
# so this exercises the full nginx -> php-fpm -> framework path, not just a port.
HEALTHCHECK --interval=10s --timeout=5s --start-period=120s --retries=12 \
CMD curl -fsS http://127.0.0.1/up >/dev/null || exit 1
ENTRYPOINT ["entrypoint.sh"]
CMD ["php-fpm"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]