Files
2026-03-22 17:28:14 +04:00

241 lines
7.0 KiB
Bash
Executable File

#!/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
log "Настраиваю Passport"
if [[ ! -f "${ROOT_DIR}/storage/oauth-private.key" ]]; then
app_exec php artisan passport:keys --force
log "Сгенерированы ключи Passport"
fi
chmod 600 "${ROOT_DIR}/storage/oauth-private.key" "${ROOT_DIR}/storage/oauth-public.key"
log "Установлены права 600 на ключи Passport"
if ! app_exec php artisan passport:client --personal --no-interaction 2>/dev/null | grep -q 'already exists'; then
app_exec php artisan passport:client --personal --name="Launcher" --no-interaction
log "Создан Personal Access Client «Launcher»"
else
log "Personal Access Client уже существует"
fi
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 "$@"