228 lines
9.3 KiB
Bash
Executable File
228 lines
9.3 KiB
Bash
Executable File
#!/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 <<EOF
|
|
map \$host \$app_forwarded_proto {
|
|
default ${FORWARDED_PROTO};
|
|
}
|
|
EOF
|
|
ok "Forwarded scheme set to ${FORWARDED_PROTO}."
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 2c. Upload ceiling, from a single variable.
|
|
# nginx, PHP and the Laravel validation rule must agree: when they don't,
|
|
# the smallest one wins and does so badly — nginx answers 413 with no body
|
|
# after accepting zero bytes, so the browser's upload bar simply freezes and
|
|
# the page has nothing to report. MAX_UPLOAD_MB drives all three
|
|
# (config/app.php reads the same variable for the validation rule).
|
|
# -----------------------------------------------------------------------------
|
|
MAX_UPLOAD_MB="${MAX_UPLOAD_MB:-2048}"
|
|
|
|
cat > /etc/nginx/conf.d/upload-limit.conf <<EOF
|
|
client_max_body_size ${MAX_UPLOAD_MB}m;
|
|
EOF
|
|
|
|
cat > /usr/local/etc/php/conf.d/zz-upload-limit.ini <<EOF
|
|
; Generated by entrypoint.sh from MAX_UPLOAD_MB — do not edit by hand.
|
|
; The zz- prefix makes it load after custom.ini, so these win.
|
|
upload_max_filesize = ${MAX_UPLOAD_MB}M
|
|
post_max_size = ${MAX_UPLOAD_MB}M
|
|
EOF
|
|
ok "Upload ceiling set to ${MAX_UPLOAD_MB} MB."
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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
|
|
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 "$@"
|