Files
PupaMLupa/Server/install-service.sh
T

290 lines
13 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 # удалить сервис"