#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)" ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" COMPOSE_FILE="${COMPOSE_FILE:-${ROOT_DIR}/compose.yaml}" APP_SERVICE="${APP_SERVICE:-laravel.test}" DB_SERVICE="${DB_SERVICE:-mysql}" COMPOSER_IMAGE="${COMPOSER_IMAGE:-composer:2}" BUILD_IMAGES=1 WARM_CACHE=1 PROJECT_UID="" PROJECT_GID="" usage() { cat <<'EOF' Использование: ./scripts/deploy-ubuntu.sh [--skip-build] [--skip-cache] Что делает скрипт: 1. Создаёт .env из .env.example, если файла ещё нет 2. Подготавливает storage/bootstrap cache директории 3. Собирает docker-образ приложения 4. Устанавливает production-зависимости Composer внутри контейнера 5. Генерирует APP_KEY, если он пустой 6. Поднимает контейнеры 7. Ждёт готовности MySQL 8. Запускает миграции и прогревает Laravel-кэш Требования: - Ubuntu с установленными Docker Engine и Docker Compose Plugin - настроенный .env с корректными значениями APP_URL и AZEROTHCORE_AUTH_DB_* EOF } log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" } fail() { printf '[error] %s\n' "$*" >&2 exit 1 } require_command() { command -v "$1" >/dev/null 2>&1 || fail "Не найдена команда: $1" } env_value() { local key="$1" [[ -f "$ENV_FILE" ]] || return 0 grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- | sed -e 's/^"//' -e 's/"$//' } set_env_value() { local key="$1" local value="$2" if grep -qE "^${key}=" "$ENV_FILE"; then sed -i "s#^${key}=.*#${key}=${value}#" "$ENV_FILE" else printf '%s=%s\n' "$key" "$value" >> "$ENV_FILE" fi } compose() { docker compose -f "$COMPOSE_FILE" "$@" } composer_install() { docker run --rm \ -u "${PROJECT_UID}:${PROJECT_GID}" \ -e HOME=/tmp/composer-home \ -e COMPOSER_HOME=/tmp/composer-home \ -v "${ROOT_DIR}:/app" \ -w /app \ "$COMPOSER_IMAGE" \ sh -lc 'mkdir -p "$COMPOSER_HOME" /app/vendor && git config --global --add safe.directory /app && composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction' } app_exec() { compose exec -T --user "${WWWUSER}:${WWWGROUP}" "$APP_SERVICE" "$@" } wait_for_mysql() { local container_id local status local attempts=0 local max_attempts=60 container_id="$(compose ps -q "$DB_SERVICE")" [[ -n "$container_id" ]] || fail "Не удалось определить контейнер MySQL" while (( attempts < max_attempts )); do status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || true)" if [[ "$status" == "healthy" || "$status" == "running" ]]; then log "MySQL готов" return 0 fi attempts=$((attempts + 1)) sleep 2 done fail "MySQL не перешёл в готовое состояние" } prepare_env() { if [[ -f "$ENV_FILE" ]]; then return 0 fi [[ -f "${ROOT_DIR}/.env.example" ]] || fail "Не найден .env.example" cp "${ROOT_DIR}/.env.example" "$ENV_FILE" set_env_value "APP_ENV" "production" set_env_value "APP_DEBUG" "false" log "Создан ${ENV_FILE} из .env.example" log "Проверь APP_URL и AZEROTHCORE_AUTH_DB_* перед публичным запуском" } prepare_directories() { mkdir -p \ "${ROOT_DIR}/bootstrap/cache" \ "${ROOT_DIR}/storage/app" \ "${ROOT_DIR}/storage/framework/cache/data" \ "${ROOT_DIR}/storage/framework/sessions" \ "${ROOT_DIR}/storage/framework/testing" \ "${ROOT_DIR}/storage/framework/views" \ "${ROOT_DIR}/storage/logs" \ "${ROOT_DIR}/vendor" chmod -R ug+rwx "${ROOT_DIR}/bootstrap/cache" "${ROOT_DIR}/storage" if [[ "$(id -u)" -eq 0 ]]; then chown -R "${WWWUSER}:${WWWGROUP}" "${ROOT_DIR}/bootstrap/cache" "${ROOT_DIR}/storage" fi } resolve_ids() { PROJECT_UID="$(stat -c '%u' "$ROOT_DIR")" PROJECT_GID="$(stat -c '%g' "$ROOT_DIR")" } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --skip-build) BUILD_IMAGES=0 ;; --skip-cache) WARM_CACHE=0 ;; -h|--help) usage exit 0 ;; *) fail "Неизвестный аргумент: $1" ;; esac shift done } main() { parse_args "$@" require_command docker docker compose version >/dev/null 2>&1 || fail "Не найден Docker Compose Plugin" cd "$ROOT_DIR" prepare_env WWWUSER="$(env_value WWWUSER)" WWWGROUP="$(env_value WWWGROUP)" WWWUSER="${WWWUSER:-1000}" WWWGROUP="${WWWGROUP:-1000}" resolve_ids prepare_directories log "Устанавливаю production-зависимости Composer" composer_install if (( BUILD_IMAGES )); then log "Собираю образ приложения" compose build "$APP_SERVICE" fi if [[ -z "$(env_value APP_KEY)" ]]; then log "Генерирую APP_KEY" compose run --rm --no-deps --user "${WWWUSER}:${WWWGROUP}" --entrypoint php "$APP_SERVICE" artisan key:generate --force fi log "Поднимаю контейнеры" compose up -d wait_for_mysql log "Выполняю миграции" app_exec php artisan migrate --force if (( WARM_CACHE )); then log "Обновляю Laravel-кэш" app_exec php artisan optimize:clear app_exec php artisan config:cache app_exec php artisan route:cache app_exec php artisan view:cache fi log "Деплой завершён" log "Сайт доступен по адресу: $(env_value APP_URL)" log "Проверка контейнеров: docker compose -f ${COMPOSE_FILE} ps" } main "$@"