#!/bin/bash set -Eeuo pipefail # ============================================================================= # 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. # ============================================================================= log() { echo "[*] $*"; } ok() { echo "[✓] $*"; } warn() { echo "[!] $*" >&2; } die() { echo "[✗] $*" >&2; exit 1; } echo "=========================================" echo " TerManager2 — starting" echo "=========================================" retry() { local attempts="$1" delay="$2"; shift 2 local n=1 until "$@"; do if [ "$n" -ge "$attempts" ]; then return 1 fi warn "Attempt ${n}/${attempts} failed. Retrying in ${delay}s..." sleep "$delay" n=$((n + 1)) done } # ----------------------------------------------------------------------------- # 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 case "$APP_URL" in http://*|https://*) ;; *) die "APP_URL must include the scheme (https://...), got: ${APP_URL}" ;; esac # ----------------------------------------------------------------------------- # 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 [ "$(id -u)" = "0" ]; then chown -R www-data:www-data storage bootstrap/cache chmod -R 775 storage bootstrap/cache fi ok "Storage directories ready." # ----------------------------------------------------------------------------- # 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 [ -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 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 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 < /etc/nginx/conf.d/upload-limit.conf < /usr/local/etc/php/conf.d/zz-upload-limit.ini < 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 warn "Seed failed. The app will start, but roles/permissions may be missing." fi else ok "Seed already applied (or disabled)." fi # ----------------------------------------------------------------------------- # 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 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 fi # ----------------------------------------------------------------------------- # 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 "=========================================" exec "$@"