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,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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user