290 lines
13 KiB
Bash
290 lines
13 KiB
Bash
#!/usr/bin/env bash
|
||
# install-service.sh
|
||
# Создаёт systemd-сервис для запуска Minecraft сервера (через ./start.sh)
|
||
# и сразу его поднимает. Должен быть запущен от root.
|
||
#
|
||
# Использование:
|
||
# sudo ./install-service.sh
|
||
# sudo ./install-service.sh --name minecraft --user minecraft --dir /opt/minecraft
|
||
# sudo ./install-service.sh --uninstall
|
||
#
|
||
# Флаги:
|
||
# --name <имя> Имя systemd-сервиса (по умолчанию: minecraft)
|
||
# --user <user> От какого пользователя запускать сервер
|
||
# (по умолчанию: minecraft, будет создан если нет)
|
||
# --dir <путь> Каталог сервера (по умолчанию: каталог этого скрипта)
|
||
# --java-args <строка> Дополнительные JVM-аргументы (напр. "-Xms4G -Xmx8G")
|
||
# --admin-user <user> Доп. пользователь, который тоже должен иметь
|
||
# запись в каталог (для git pull и т.п.). Будет
|
||
# добавлен в группу сервиса, на каталог встанут
|
||
# g+rwX и setgid, чтобы новые файлы наследовали
|
||
# группу. Можно указать несколько раз.
|
||
# --uninstall Остановить, выключить и удалить сервис
|
||
# -h | --help Показать справку
|
||
|
||
set -euo pipefail
|
||
|
||
SERVICE_NAME="minecraft"
|
||
SERVICE_USER="minecraft"
|
||
SERVER_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
JAVA_ARGS=""
|
||
UNINSTALL=0
|
||
ADMIN_USERS=()
|
||
|
||
print_help() {
|
||
sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'
|
||
}
|
||
|
||
err() {
|
||
echo "Ошибка: $*" >&2
|
||
exit 1
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--name) SERVICE_NAME="$2"; shift 2 ;;
|
||
--user) SERVICE_USER="$2"; shift 2 ;;
|
||
--dir) SERVER_DIR="$2"; shift 2 ;;
|
||
--java-args) JAVA_ARGS="$2"; shift 2 ;;
|
||
--admin-user) ADMIN_USERS+=("$2"); shift 2 ;;
|
||
--uninstall) UNINSTALL=1; shift ;;
|
||
-h|--help) print_help; exit 0 ;;
|
||
*) err "Неизвестный аргумент: $1 (используйте --help)" ;;
|
||
esac
|
||
done
|
||
|
||
# 1. Проверка прав root
|
||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||
err "скрипт должен быть запущен от root. Используйте: sudo $0 $*"
|
||
fi
|
||
|
||
# 2. Проверка systemd
|
||
if ! command -v systemctl >/dev/null 2>&1; then
|
||
err "systemctl не найден. Этот скрипт работает только на системах с systemd."
|
||
fi
|
||
|
||
UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||
|
||
# Удаление сервиса
|
||
if [[ "${UNINSTALL}" -eq 1 ]]; then
|
||
echo ">> Останавливаю и удаляю сервис ${SERVICE_NAME}..."
|
||
systemctl stop "${SERVICE_NAME}.service" 2>/dev/null || true
|
||
systemctl disable "${SERVICE_NAME}.service" 2>/dev/null || true
|
||
if [[ -f "${UNIT_FILE}" ]]; then
|
||
rm -f "${UNIT_FILE}"
|
||
echo " Удалён ${UNIT_FILE}"
|
||
fi
|
||
systemctl daemon-reload
|
||
systemctl reset-failed "${SERVICE_NAME}.service" 2>/dev/null || true
|
||
echo ">> Готово. Сервис ${SERVICE_NAME} удалён."
|
||
exit 0
|
||
fi
|
||
|
||
# 3. Проверка содержимого каталога сервера
|
||
[[ -d "${SERVER_DIR}" ]] || err "каталог сервера не найден: ${SERVER_DIR}"
|
||
[[ -f "${SERVER_DIR}/start.sh" ]] || err "не найден ${SERVER_DIR}/start.sh"
|
||
|
||
# 3b. Предупреждение про /home — типичная причина status=200/CHDIR
|
||
# ProtectHome=read-only в unit-файле блокирует доступ к /home,
|
||
# плюс домашний каталог обычно имеет права 750 и пользователь
|
||
# сервиса не может в него войти. Рекомендуем /opt/<name>.
|
||
if [[ "${SERVER_DIR}" == /home/* ]]; then
|
||
PARENT_USER="$(stat -c '%U' "$(dirname "${SERVER_DIR}")" 2>/dev/null || echo "$USER")"
|
||
SUGGESTED_DIR="/opt/$(basename "$(dirname "${SERVER_DIR}")")/$(basename "${SERVER_DIR}")"
|
||
cat >&2 <<EOF
|
||
|
||
============================ ВНИМАНИЕ ============================
|
||
Каталог сервера находится в /home/:
|
||
${SERVER_DIR}
|
||
|
||
Это плохое место для systemd-сервиса:
|
||
- ProtectHome=read-only в unit-файле блокирует /home;
|
||
- дефолтные права на ~ (обычно 750) не дают зайти юзеру minecraft;
|
||
- типовая ошибка systemd: status=200/CHDIR.
|
||
|
||
Рекомендуется перенести сервер в /opt (или /srv) и работать оттуда.
|
||
Пример переноса (если вы уже клонировали репозиторий в /home):
|
||
|
||
sudo systemctl stop ${SERVICE_NAME} 2>/dev/null || true
|
||
sudo mkdir -p $(dirname "${SUGGESTED_DIR}")
|
||
sudo mv "$(dirname "${SERVER_DIR}")" "$(dirname "${SUGGESTED_DIR}")/"
|
||
cd "${SUGGESTED_DIR}"
|
||
sudo ./install-service.sh --admin-user ${PARENT_USER}
|
||
|
||
После этого git pull/push продолжит работать из нового пути,
|
||
а сервис уже не будет упираться в ProtectHome.
|
||
====================================================================
|
||
|
||
EOF
|
||
read -r -p "Продолжить установку с текущим путём в /home? [y/N] " ans
|
||
case "${ans,,}" in
|
||
y|yes) echo "Продолжаю..." ;;
|
||
*) err "установка отменена. Перенесите каталог в /opt и запустите скрипт снова." ;;
|
||
esac
|
||
fi
|
||
|
||
chmod +x "${SERVER_DIR}/start.sh" "${SERVER_DIR}/run.sh" 2>/dev/null || true
|
||
|
||
# 3c. Проверка Java. start.sh иначе попытается интерактивно поставить
|
||
# Jabba и упадёт в systemd с "User did not agree to Jabba installation".
|
||
REQUIRED_JAVA_VERSION="$(grep -E '^RECOMMENDED_JAVA_VERSION=' "${SERVER_DIR}/variables.txt" 2>/dev/null \
|
||
| cut -d= -f2 | tr -d '"' | head -n1)"
|
||
REQUIRED_JAVA_VERSION="${REQUIRED_JAVA_VERSION:-21}"
|
||
|
||
if ! command -v java >/dev/null 2>&1; then
|
||
cat >&2 <<EOF
|
||
|
||
============================ ВНИМАНИЕ ============================
|
||
В системе не найдена команда 'java'. start.sh попытается поставить
|
||
её через Jabba, но Jabba требует интерактивного 'I agree', которого
|
||
в systemd нет. Сервис будет бесконечно перезапускаться.
|
||
|
||
Установите Java ${REQUIRED_JAVA_VERSION} системно перед запуском:
|
||
|
||
# Debian/Ubuntu (родной пакет, если доступен):
|
||
sudo apt update && sudo apt install -y openjdk-${REQUIRED_JAVA_VERSION}-jre-headless
|
||
|
||
# Альтернатива — Temurin от Adoptium:
|
||
sudo apt install -y wget gnupg ca-certificates
|
||
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public \\
|
||
| sudo gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
|
||
echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb \$(. /etc/os-release && echo \$VERSION_CODENAME) main" \\
|
||
| sudo tee /etc/apt/sources.list.d/adoptium.list
|
||
sudo apt update && sudo apt install -y temurin-${REQUIRED_JAVA_VERSION}-jre
|
||
|
||
После установки запустите этот скрипт повторно.
|
||
====================================================================
|
||
|
||
EOF
|
||
err "java не найдена в PATH. Установите Java ${REQUIRED_JAVA_VERSION} и повторите."
|
||
fi
|
||
|
||
JAVA_MAJOR="$(java -version 2>&1 | awk -F\" '/version/ {print $2}' | cut -d. -f1)"
|
||
if [[ "${JAVA_MAJOR}" == "1" ]]; then
|
||
JAVA_MAJOR="$(java -version 2>&1 | awk -F\" '/version/ {print $2}' | cut -d. -f2)"
|
||
fi
|
||
if [[ "${JAVA_MAJOR}" != "${REQUIRED_JAVA_VERSION}" ]]; then
|
||
echo ">> ВНИМАНИЕ: установлена Java ${JAVA_MAJOR}, ожидалась ${REQUIRED_JAVA_VERSION}." >&2
|
||
echo " Сервер может не стартовать. Рекомендуется поставить openjdk-${REQUIRED_JAVA_VERSION}-jre-headless." >&2
|
||
fi
|
||
|
||
# 4. Создание пользователя при необходимости
|
||
if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then
|
||
echo ">> Создаю системного пользователя ${SERVICE_USER}..."
|
||
useradd --system \
|
||
--home-dir "${SERVER_DIR}" \
|
||
--shell /usr/sbin/nologin \
|
||
"${SERVICE_USER}"
|
||
else
|
||
echo ">> Пользователь ${SERVICE_USER} уже существует — пропускаю создание."
|
||
fi
|
||
|
||
# 5. Передача прав на каталог сервера
|
||
echo ">> Назначаю владельца каталога ${SERVER_DIR} -> ${SERVICE_USER}..."
|
||
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${SERVER_DIR}"
|
||
|
||
# 5b. Доп. пользователи (для git pull / редактирования конфигов без sudo).
|
||
# Включаем их в группу сервиса, на каталог ставим g+rwX и setgid,
|
||
# чтобы новые файлы автоматически создавались с этой группой.
|
||
if [[ ${#ADMIN_USERS[@]} -gt 0 ]]; then
|
||
echo ">> Настраиваю групповой доступ для: ${ADMIN_USERS[*]}"
|
||
for au in "${ADMIN_USERS[@]}"; do
|
||
if ! id -u "${au}" >/dev/null 2>&1; then
|
||
echo " ! пользователь ${au} не существует — пропускаю"
|
||
continue
|
||
fi
|
||
usermod -aG "${SERVICE_USER}" "${au}"
|
||
echo " + ${au} добавлен в группу ${SERVICE_USER}"
|
||
|
||
# Чтобы git не ругался на 'dubious ownership' под этим юзером
|
||
if command -v sudo >/dev/null 2>&1; then
|
||
sudo -u "${au}" git config --global --add safe.directory "${SERVER_DIR}" \
|
||
2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# g+rwX на всё содержимое + setgid на каталоги
|
||
chmod -R g+rwX "${SERVER_DIR}"
|
||
find "${SERVER_DIR}" -type d -exec chmod g+s {} +
|
||
echo " Установлены g+rwX и setgid на ${SERVER_DIR}"
|
||
echo " ВАЖНО: после добавления в группу нужно перелогиниться,"
|
||
echo " либо выполнить 'newgrp ${SERVICE_USER}' в текущей сессии."
|
||
fi
|
||
|
||
# 6. Проверка EULA — без неё сервер встанет ожидая ввод (а в systemd ввода нет)
|
||
EULA_FILE="${SERVER_DIR}/eula.txt"
|
||
if [[ ! -f "${EULA_FILE}" ]] || ! grep -qE '^\s*eula\s*=\s*true' "${EULA_FILE}"; then
|
||
cat >&2 <<EOF
|
||
Внимание: ${EULA_FILE} отсутствует или eula=false.
|
||
В systemd нет интерактивного stdin, поэтому сервер не сможет спросить согласие.
|
||
Подтвердите EULA вручную перед стартом сервиса:
|
||
echo 'eula=true' | sudo tee ${EULA_FILE}
|
||
Скрипт продолжит и создаст unit, но сервис стартанёт только после принятия EULA.
|
||
EOF
|
||
fi
|
||
|
||
# 7. Генерация systemd unit-файла
|
||
# Если каталог в /home, ProtectHome нужно ослабить, иначе systemd
|
||
# не сможет даже сделать chdir в WorkingDirectory.
|
||
if [[ "${SERVER_DIR}" == /home/* ]]; then
|
||
PROTECT_HOME_LINE="ProtectHome=false # ослаблено, т.к. сервер в /home"
|
||
# Дополнительно открываем доступ юзеру minecraft на проход через ~
|
||
chmod o+x "$(dirname "${SERVER_DIR}")" 2>/dev/null || true
|
||
else
|
||
PROTECT_HOME_LINE="ProtectHome=read-only"
|
||
fi
|
||
|
||
echo ">> Создаю ${UNIT_FILE}..."
|
||
cat > "${UNIT_FILE}" <<EOF
|
||
[Unit]
|
||
Description=Minecraft Server (${SERVICE_NAME})
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=${SERVICE_USER}
|
||
Group=${SERVICE_USER}
|
||
WorkingDirectory=${SERVER_DIR}
|
||
Environment=WAIT_FOR_USER_INPUT=false
|
||
$( [[ -n "${JAVA_ARGS}" ]] && echo "Environment=JAVA_ARGS=${JAVA_ARGS}" )
|
||
ExecStart=/usr/bin/env bash ${SERVER_DIR}/start.sh
|
||
Restart=on-failure
|
||
RestartSec=10s
|
||
TimeoutStopSec=120
|
||
KillSignal=SIGINT
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
# Безопасность
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=full
|
||
${PROTECT_HOME_LINE}
|
||
ReadWritePaths=${SERVER_DIR}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
chmod 644 "${UNIT_FILE}"
|
||
|
||
# 8. Активация сервиса
|
||
echo ">> Перечитываю systemd, включаю и запускаю сервис..."
|
||
systemctl daemon-reload
|
||
systemctl enable "${SERVICE_NAME}.service"
|
||
systemctl restart "${SERVICE_NAME}.service"
|
||
|
||
sleep 2
|
||
echo ""
|
||
echo "===================================================================="
|
||
systemctl --no-pager --full status "${SERVICE_NAME}.service" || true
|
||
echo "===================================================================="
|
||
echo ""
|
||
echo "Готово. Полезные команды:"
|
||
echo " sudo systemctl status ${SERVICE_NAME}"
|
||
echo " sudo systemctl stop ${SERVICE_NAME}"
|
||
echo " sudo systemctl restart ${SERVICE_NAME}"
|
||
echo " sudo journalctl -u ${SERVICE_NAME} -f # живые логи"
|
||
echo " sudo $0 --uninstall # удалить сервис"
|