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,2 +0,0 @@
FROM nginx:1.25-alpine
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf

View File

@@ -1,11 +1,21 @@
server {
listen 80;
server_name localhost;
listen 80 default_server;
server_name _;
root /var/www/html/public;
index index.php index.html;
index index.php;
charset utf-8;
client_max_body_size 64M;
# Territory PDF/ZIP imports are the largest uploads; keep this in step with
# upload_max_filesize / post_max_size in docker/php/php.ini.
client_max_body_size 256M;
# Behind Dokploy's Traefik, the real client IP arrives in X-Forwarded-For.
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_recursive on;
location / {
try_files $uri $uri/ /index.php?$query_string;
@@ -14,34 +24,70 @@ server {
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# Vite emits content-hashed filenames, so these are safe to cache hard.
location /build/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
# Uploaded territory PDFs and thumbnails (public/storage -> storage/app/public).
# Served directly by nginx rather than round-tripping through PHP.
location /storage/ {
expires 7d;
add_header Cache-Control "public";
try_files $uri =404;
}
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
location ~ ^/index\.php(/|$) {
fastcgi_pass 127.0.0.1:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_hide_header X-Powered-By;
# Forward proxy headers so Laravel can validate signed URLs behind reverse proxy
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_HOST $http_host;
fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port;
# PDF generation and ZIP imports can outrun the default 60s.
fastcgi_read_timeout 300;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
# Trust external proxy proto, or default to https for signed URL validation
set $fwd_proto $http_x_forwarded_proto;
if ($fwd_proto = "") {
set $fwd_proto "https";
}
fastcgi_param HTTP_X_FORWARDED_PROTO $fwd_proto;
# Set server-side, NOT taken from the client's header: honouring a
# client-supplied X-Forwarded-Proto would let an attacker declare the
# connection plaintext and skew signed-URL validation and secure-cookie
# decisions. The value is written by the entrypoint from APP_URL's
# scheme (see /etc/nginx/conf.d/forwarded-proto.conf) — https in
# production behind Traefik, http for local plain-HTTP development.
fastcgi_param HTTP_X_FORWARDED_PROTO $app_forwarded_proto;
# X-Forwarded-Port is deliberately NOT set here. nginx already passes
# the incoming header through as HTTP_X_FORWARDED_PORT, and Traefik
# sends the real public port; overriding it with $server_port (always
# 80 inside the container) would corrupt generated URLs.
internal;
}
# Any other .php file is not a valid entrypoint — Laravel routes everything
# through index.php. This blocks execution of uploaded/stray scripts.
location ~ \.php$ {
return 404;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
}

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"]

View File

@@ -1,246 +1,205 @@
#!/bin/bash
set -Eeuo pipefail
echo "========================================="
echo " TerManager2 - Entrypoint"
echo "========================================="
# =============================================================================
# TerManager2 — application container entrypoint
#
# The image is self-contained: vendor/ and public/build are baked in at build
# time. This script only does what genuinely cannot happen until runtime —
# resolve APP_KEY, wait for the database, migrate, and warm the caches.
#
# Configuration comes from real environment variables (Dokploy injects them).
# No .env file is written; Laravel reads the process environment directly.
# =============================================================================
warn() {
echo "[!] $*"
}
log() { echo "[*] $*"; }
ok() { echo "[✓] $*"; }
warn() { echo "[!] $*" >&2; }
die() { echo "[✗] $*" >&2; exit 1; }
echo "========================================="
echo " TerManager2 — starting"
echo "========================================="
retry() {
local attempts="$1"
local delay="$2"
shift 2
local attempts="$1" delay="$2"; shift 2
local n=1
until "$@"; do
if [ "$n" -ge "$attempts" ]; then
return 1
fi
warn "Command failed (attempt ${n}/${attempts}). Retrying in ${delay}s..."
warn "Attempt ${n}/${attempts} failed. Retrying in ${delay}s..."
sleep "$delay"
n=$((n + 1))
done
}
upsert_env() {
local key="$1"
local value="$2"
local env_file="$3"
# -----------------------------------------------------------------------------
# 0. Required configuration
# APP_URL anchors every generated link (assignment PDFs, e-mails) and is
# matched against the incoming Host header, so a wrong or missing value
# surfaces as blank pages or 400s much later. Fail here instead, where the
# message is unambiguous.
# -----------------------------------------------------------------------------
if [ -z "${APP_URL:-}" ]; then
die "APP_URL is not set. Set it to the public URL of the application, e.g. https://territori.example.org"
fi
if grep -q "^${key}=" "$env_file"; then
sed -i "s|^${key}=.*|${key}=${value}|" "$env_file"
else
echo "${key}=${value}" >> "$env_file"
fi
}
case "$APP_URL" in
http://*|https://*) ;;
*) die "APP_URL must include the scheme (https://...), got: ${APP_URL}" ;;
esac
read_env_value() {
local key="$1"
local env_file="$2"
# -----------------------------------------------------------------------------
# 1. Writable directories
# The storage volume mounts empty on a first deploy, which hides the
# skeleton baked into the image — recreate it every boot.
# -----------------------------------------------------------------------------
mkdir -p \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
storage/app/public \
bootstrap/cache
if [ ! -f "$env_file" ]; then
return 0
fi
local line
line=$(grep -E "^${key}=" "$env_file" | tail -n 1 || true)
if [ -z "$line" ]; then
return 0
fi
local value="${line#*=}"
value="${value%\"}"
value="${value#\"}"
printf '%s' "$value"
}
# -----------------------------------------------
# 0. Application code is bind-mounted from host
# -----------------------------------------------
echo "[✓] Using bind-mounted application code from host."
# -----------------------------------------------
# 0b. Create required directories & fix permissions
# -----------------------------------------------
mkdir -p storage/framework/{cache,sessions,views}
mkdir -p storage/logs
mkdir -p storage/app
mkdir -p bootstrap/cache
if [ "$(id -u)" = "0" ]; then
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
fi
ok "Storage directories ready."
# -----------------------------------------------
# 1. .env file (must exist before composer/artisan)
# -----------------------------------------------
if [ ! -f .env ]; then
echo "[*] Creating .env from .env.example..."
cp .env.example .env
fi
# Override .env values with Docker environment variables (if set)
# This ensures Dokploy env vars take precedence
if [ -n "${APP_KEY:-}" ]; then
upsert_env "APP_KEY" "${APP_KEY}" .env
fi
if [ -n "${APP_URL:-}" ]; then
upsert_env "APP_URL" "${APP_URL}" .env
fi
if [ -n "${DB_HOST:-}" ]; then
upsert_env "DB_HOST" "${DB_HOST}" .env
fi
if [ -n "${DB_DATABASE:-}" ]; then
upsert_env "DB_DATABASE" "${DB_DATABASE}" .env
fi
if [ -n "${DB_USERNAME:-}" ]; then
upsert_env "DB_USERNAME" "${DB_USERNAME}" .env
fi
if [ -n "${DB_PASSWORD:-}" ]; then
upsert_env "DB_PASSWORD" "${DB_PASSWORD}" .env
fi
if [ -n "${REDIS_HOST:-}" ]; then
upsert_env "REDIS_HOST" "${REDIS_HOST}" .env
fi
if [ -n "${REDIS_PASSWORD:-}" ]; then
upsert_env "REDIS_PASSWORD" "${REDIS_PASSWORD}" .env
fi
echo "[✓] .env file ready."
# -----------------------------------------------
# 2. Composer install (must run before artisan commands)
# -----------------------------------------------
if [ ! -f vendor/autoload.php ]; then
echo "[*] Installing Composer dependencies..."
retry 3 5 composer install --no-interaction --prefer-dist --optimize-autoloader --no-progress
else
echo "[✓] Composer dependencies already installed."
fi
# -----------------------------------------------
# 3. Application key (persisted in storage volume)
# -----------------------------------------------
# -----------------------------------------------------------------------------
# 2. APP_KEY
# Personal data of proclamatori is encrypted at column level with this key.
# Losing it means losing that data, so a generated key is persisted into the
# storage volume and reused across redeploys.
# -----------------------------------------------------------------------------
KEY_FILE="/var/www/html/storage/app/.app_key"
if [ -f "$KEY_FILE" ]; then
# Restore key from persistent volume
STORED_KEY=$(cat "$KEY_FILE")
upsert_env "APP_KEY" "$STORED_KEY" .env
echo "[✓] Application key restored from storage."
elif grep -q "^APP_KEY=$" .env; then
# No stored key and .env has empty key: generate new one
echo "[*] Generating application key..."
php artisan key:generate --ansi
# Save to persistent volume
grep '^APP_KEY=' .env | cut -d= -f2- > "$KEY_FILE"
echo "[✓] Application key saved to persistent storage."
if [ -n "${APP_KEY:-}" ]; then
ok "APP_KEY provided via environment."
printf '%s' "$APP_KEY" > "$KEY_FILE"
elif [ -s "$KEY_FILE" ]; then
APP_KEY="$(cat "$KEY_FILE")"
export APP_KEY
ok "APP_KEY restored from persistent storage."
else
# .env already has a key (from env var override or .env.example), persist it
grep '^APP_KEY=' .env | cut -d= -f2- > "$KEY_FILE"
echo "[✓] Application key already set, saved to persistent storage."
APP_KEY="base64:$(head -c 32 /dev/urandom | base64)"
export APP_KEY
printf '%s' "$APP_KEY" > "$KEY_FILE"
warn "No APP_KEY was provided — generated one and persisted it to the"
warn "storage volume. Copy it into the Dokploy environment now:"
warn " APP_KEY=${APP_KEY}"
warn "If that volume is ever lost, encrypted personal data becomes"
warn "unrecoverable."
fi
# -----------------------------------------------
# 4. NPM install & build assets
# -----------------------------------------------
if [ ! -d node_modules ]; then
echo "[*] Installing NPM dependencies..."
if [ -f package-lock.json ]; then
retry 3 5 npm ci --no-audit --no-fund
chmod 600 "$KEY_FILE" 2>/dev/null || true
if [ "$(id -u)" = "0" ]; then
chown www-data:www-data "$KEY_FILE" 2>/dev/null || true
fi
# -----------------------------------------------------------------------------
# 2b. Forwarded scheme for nginx.
# Derived from APP_URL rather than from the client's X-Forwarded-Proto,
# which is attacker-controlled. https in production behind Traefik; http
# when developing locally over plain HTTP, so redirects don't jump scheme.
# -----------------------------------------------------------------------------
case "${APP_URL:-}" in
http://*) FORWARDED_PROTO="http" ;;
*) FORWARDED_PROTO="https" ;;
esac
cat > /etc/nginx/conf.d/forwarded-proto.conf <<EOF
map \$host \$app_forwarded_proto {
default ${FORWARDED_PROTO};
}
EOF
ok "Forwarded scheme set to ${FORWARDED_PROTO}."
# -----------------------------------------------------------------------------
# 3. Storage symlink (public/storage -> storage/app/public)
# -----------------------------------------------------------------------------
if [ ! -e public/storage ]; then
php artisan storage:link --quiet && ok "Storage symlink created."
else
ok "Storage symlink present."
fi
# -----------------------------------------------------------------------------
# 4. Drop caches from the previous image before touching the database.
# A stale bootstrap/cache/config.php would otherwise shadow the new env.
# -----------------------------------------------------------------------------
php artisan config:clear --quiet || warn "config:clear failed; continuing."
php artisan route:clear --quiet || warn "route:clear failed; continuing."
php artisan view:clear --quiet || warn "view:clear failed; continuing."
# -----------------------------------------------------------------------------
# 5. Wait for the database, then migrate.
# -----------------------------------------------------------------------------
log "Waiting for database at ${DB_HOST:-mariadb}:${DB_PORT:-3306}..."
if ! retry 30 2 nc -z "${DB_HOST:-mariadb}" "${DB_PORT:-3306}"; then
die "Database never became reachable. Check DB_HOST/DB_PORT and that the mariadb service is healthy."
fi
ok "Database reachable."
if [ "${RUN_MIGRATIONS:-true}" = "true" ]; then
log "Running migrations..."
retry 5 3 php artisan migrate --force --no-interaction \
|| die "Migrations failed. Refusing to start with a half-migrated schema."
ok "Migrations applied."
else
echo "[i] RUN_MIGRATIONS=false — skipping migrations."
fi
# -----------------------------------------------------------------------------
# 6. Seed reference data (roles, permissions, settings) once.
# The marker lives in the storage volume so redeploys don't reseed.
# -----------------------------------------------------------------------------
SEED_MARKER="/var/www/html/storage/app/.db_seeded"
if [ "${RUN_DB_SEED_ON_FIRST_START:-true}" = "true" ] && [ ! -f "$SEED_MARKER" ]; then
log "First start — seeding reference data..."
if retry 3 3 php artisan db:seed --force --no-interaction; then
touch "$SEED_MARKER"
ok "Seed completed."
else
retry 3 5 npm install --no-audit --no-fund
warn "Seed failed. The app will start, but roles/permissions may be missing."
fi
else
echo "[✓] NPM dependencies already installed."
ok "Seed already applied (or disabled)."
fi
if [ ! -f public/build/manifest.json ]; then
echo "[*] Building frontend assets..."
npm run build
else
echo "[✓] Frontend assets already built."
fi
# -----------------------------------------------
# 5. Storage link
# -----------------------------------------------
if [ ! -L public/storage ]; then
echo "[*] Creating storage symlink..."
php artisan storage:link
else
echo "[✓] Storage symlink already exists."
fi
# -----------------------------------------------
# 6b. Clear any stale cached config from a previous deploy
# (a cached bootstrap/cache/config.php would override the .env above)
# -----------------------------------------------
echo "[*] Clearing stale cached config..."
php artisan config:clear || warn "config:clear failed; continuing startup."
# -----------------------------------------------
# 7. Run migrations
# -----------------------------------------------
echo "[*] Running database migrations..."
retry 10 3 php artisan migrate --force
# -----------------------------------------------
# 7b. Seed database on first container startup only
# -----------------------------------------------
SEED_MARKER_FILE="/var/www/html/storage/framework/.runtime_db_seeded"
RUN_DB_SEED_ON_FIRST_START="${RUN_DB_SEED_ON_FIRST_START:-true}"
if [ "$RUN_DB_SEED_ON_FIRST_START" = "true" ]; then
if [ ! -f "$SEED_MARKER_FILE" ]; then
echo "[*] First startup detected. Running database seed..."
retry 5 3 php artisan db:seed --force
touch "$SEED_MARKER_FILE"
echo "[✓] Database seed completed and startup marker saved."
# -----------------------------------------------------------------------------
# 7. Initial administrator
# -----------------------------------------------------------------------------
if [ "${ENSURE_INITIAL_ADMIN_ON_EMPTY_DB:-true}" = "true" ]; then
if [ -n "${INITIAL_ADMIN_EMAIL:-}" ] && [ -n "${INITIAL_ADMIN_PASSWORD:-}" ]; then
log "Ensuring initial administrator..."
php artisan app:create-initial-admin \
--name="${INITIAL_ADMIN_NAME:-Amministratore}" \
--email="${INITIAL_ADMIN_EMAIL}" \
--password="${INITIAL_ADMIN_PASSWORD}" \
--no-interaction \
|| warn "Initial admin creation failed (it may already exist)."
else
echo "[✓] Seed already executed on a previous startup."
warn "INITIAL_ADMIN_EMAIL / INITIAL_ADMIN_PASSWORD not set — no admin created."
warn "Set them in the Dokploy environment to get a login on first deploy."
fi
else
echo "[i] RUN_DB_SEED_ON_FIRST_START=false, skipping automatic seed."
fi
# -----------------------------------------------
# 7c. Ensure initial admin exists (first startup)
# -----------------------------------------------
ENSURE_INITIAL_ADMIN_ON_EMPTY_DB="${ENSURE_INITIAL_ADMIN_ON_EMPTY_DB:-true}"
INITIAL_ADMIN_NAME_VALUE="${INITIAL_ADMIN_NAME:-$(read_env_value INITIAL_ADMIN_NAME .env)}"
INITIAL_ADMIN_EMAIL_VALUE="${INITIAL_ADMIN_EMAIL:-$(read_env_value INITIAL_ADMIN_EMAIL .env)}"
INITIAL_ADMIN_PASSWORD_VALUE="${INITIAL_ADMIN_PASSWORD:-$(read_env_value INITIAL_ADMIN_PASSWORD .env)}"
if [ "$ENSURE_INITIAL_ADMIN_ON_EMPTY_DB" = "true" ]; then
echo "[*] Ensuring initial administrator account..."
if ! php artisan app:create-initial-admin \
--name="$INITIAL_ADMIN_NAME_VALUE" \
--email="$INITIAL_ADMIN_EMAIL_VALUE" \
--password="$INITIAL_ADMIN_PASSWORD_VALUE" \
--no-interaction; then
warn "Initial admin creation failed. Set INITIAL_ADMIN_NAME, INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD."
fi
else
echo "[i] ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=false, skipping initial admin creation check."
fi
# -----------------------------------------------
# 8. Cache config/routes/views
# -----------------------------------------------
echo "[*] Caching configuration..."
php artisan config:cache || warn "config:cache failed; continuing startup."
php artisan route:cache || warn "route:cache failed; continuing startup."
php artisan view:cache || warn "view:cache failed; continuing startup."
# -----------------------------------------------------------------------------
# 8. Warm caches. Done last, so the config cache captures the final env.
# -----------------------------------------------------------------------------
log "Warming caches..."
php artisan config:cache --quiet || warn "config:cache failed; continuing."
php artisan route:cache --quiet || warn "route:cache failed; continuing."
php artisan view:cache --quiet || warn "view:cache failed; continuing."
ok "Caches warmed."
echo "========================================="
echo " TerManager2 - Ready!"
echo " TerManager2 — ready"
echo "========================================="
# -----------------------------------------------
# Execute CMD (default: php-fpm)
# -----------------------------------------------
exec "$@"

View File

@@ -0,0 +1,40 @@
#!/bin/bash
set -Eeuo pipefail
# =============================================================================
# TerManager2 — queue worker entrypoint
#
# Deliberately does NOT migrate or seed: the app container owns schema changes.
# Two containers racing on `artisan migrate` is how you get a half-applied
# schema. This one only waits for its dependencies and starts working.
# =============================================================================
warn() { echo "[!] $*" >&2; }
# APP_KEY must match the app container or encrypted payloads won't decrypt.
KEY_FILE="/var/www/html/storage/app/.app_key"
if [ -z "${APP_KEY:-}" ] && [ -s "$KEY_FILE" ]; then
APP_KEY="$(cat "$KEY_FILE")"
export APP_KEY
fi
echo "[*] Queue worker waiting for database and redis..."
for _ in $(seq 1 60); do
if nc -z "${DB_HOST:-mariadb}" "${DB_PORT:-3306}" \
&& nc -z "${REDIS_HOST:-redis}" "${REDIS_PORT:-6379}"; then
echo "[✓] Dependencies reachable."
break
fi
sleep 2
done
# Wait for the app container to finish migrating before consuming jobs, so a
# job doesn't hit a table that doesn't exist yet.
for _ in $(seq 1 60); do
if php artisan migrate:status --no-interaction >/dev/null 2>&1; then
break
fi
sleep 2
done
exec "$@"

View File

@@ -1,7 +1,10 @@
[www]
user = www-data
group = www-data
listen = 0.0.0.0:9000
; nginx now runs in the same container, so php-fpm no longer needs to be
; reachable from the network. Binding to loopback removes that exposure.
listen = 127.0.0.1:9000
listen.owner = www-data
listen.group = www-data
@@ -11,3 +14,15 @@ pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
; Surface worker output in `docker logs` instead of swallowing it.
catch_workers_output = yes
decorate_workers_output = no
; php-fpm sanitizes the environment by default, which would hide every Dokploy
; env var from Laravel. clear_env=no is what makes config-from-env work.
clear_env = no
; Log slow requests (PDF import/generation) to help diagnose timeouts.
slowlog = /proc/self/fd/2
request_slowlog_timeout = 30s

View File

@@ -0,0 +1,38 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid
# Both programs log to the container's stdout/stderr so `docker logs` and the
# Dokploy log viewer show everything without shelling into the container.
[program:php-fpm]
command=php-fpm --nodaemonize --force-stderr
autostart=true
autorestart=true
startretries=3
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
startretries=3
priority=20
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
# If either process dies for good, take the whole container down so Docker's
# restart policy (and Dokploy's health check) can react. Without this the
# container stays "up" while serving nothing.
[eventlistener:process_exit]
command=bash -c "printf 'READY\\n'; while read -r line; do echo \"Process exited: $line\" >&2; kill -SIGTERM $PPID; done < /dev/stdin"
events=PROCESS_STATE_FATAL