Last active
April 11, 2025 11:44
-
-
Save nodir-malikov/b8f61d9667f27519cc88ebcf594a4e09 to your computer and use it in GitHub Desktop.
Автоматическая настройка VPS под Python проекты
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# vps_setup.sh — автоматическая настройка VPS под Python проекты | |
# ИНСТРУКЦИЯ: | |
# Скачиваем свежую версию | |
# curl -fsSL <RAW URL этого скрипта в GitHub Gist> -o vps_setup.sh | |
# chmod +x vps_setup.sh | |
# Интерактивный режим | |
# sudo ./vps_setup.sh | |
# «Тихий» режим (пример cfg.yml — см. ниже) | |
# sudo ./vps_setup.sh -c cfg.yml | |
# Пример cfg.yml для тихого запуска | |
# tasks: | |
# - 1 # update_system | |
# - 2 # oh-my-zsh | |
# - 3 # python | |
# - 4 # postgres | |
# - 5 # pgbouncer | |
# - 6 # nginx | |
# - 11 # ssl | |
# - 16 # nodejs | |
# - 17 # swap | |
# - 18 # poetry | |
set -euo pipefail | |
################ Цветной вывод + разделитель ################ | |
C_RESET="\e[0m"; C_GREEN="\e[32m"; C_RED="\e[31m"; C_BLUE="\e[34m"; C_YELLOW="\e[33m" | |
msg() { echo -e "\n${C_BLUE}=== $* ===${C_RESET}"; } | |
ok() { echo -e "${C_GREEN}✔ $*${C_RESET}"; } | |
warn() { echo -e "${C_YELLOW}! $*${C_RESET}"; } | |
err() { echo -e "${C_RED}✖ $*${C_RESET}"; } | |
################ Предварительные проверки ################ | |
[[ $EUID -ne 0 ]] && { err "Запустите скрипт через sudo или под root"; exit 1; } | |
command -v apt >/dev/null || { err "Скрипт рассчитан на Debian/Ubuntu (apt не найден)"; exit 1; } | |
################ Создание / выбор рабочего пользователя ################ | |
read -rp "Имя пользователя для окружения (Oh‑My‑Zsh, Poetry, Node.js и т.д.) [www]: " APP_USER | |
APP_USER=${APP_USER:-www} | |
# ── функция безопасной установки пароля ──────────────────────────────── | |
set_user_password() { | |
# разблокируем учётку, если она была создана с --disabled-password | |
passwd -u "${APP_USER}" 2>/dev/null || true # не страшно, если уже разблокирована | |
while true; do | |
read -rsp "Введите пароль для ${APP_USER}: " PW1; echo | |
read -rsp "Повторите пароль: " PW2; echo | |
[[ "$PW1" != "$PW2" ]] && { warn "Пароли не совпадают, попробуйте ещё раз"; continue; } | |
[[ ${#PW1} -lt 8 ]] && { warn "Пароль должен быть минимум 8 символов"; continue; } | |
# пытаемся задать пароль | |
if printf "%s:%s\n" "${APP_USER}" "${PW1}" | chpasswd; then | |
ok "Пароль для ${APP_USER} установлен" | |
break | |
else | |
err "Не удалось изменить пароль (возможно, не прошёл правила PAM). Попробуйте снова." | |
fi | |
done | |
} | |
# ── создаём пользователя, если нужно ─────────────────────────────────── | |
if id "$APP_USER" &>/dev/null; then | |
ok "Пользователь ${APP_USER} уже существует" | |
read -rp "Изменить ему пароль? (y/N): " CHG | |
[[ $CHG =~ ^[Yy]$ ]] && set_user_password | |
else | |
msg "Создаём пользователя ${APP_USER} и даём sudo" | |
adduser --disabled-password --gecos "" "$APP_USER" | |
usermod -aG sudo "$APP_USER" | |
set_user_password | |
fi | |
HOME_DIR=$(eval echo "~${APP_USER}") | |
################ Безопасное копирование SSH‑ключей root → ${APP_USER} ################ | |
if [[ -f /root/.ssh/authorized_keys ]]; then | |
msg "Добавляем SSH‑ключи root → ${APP_USER}" | |
install -d -m 700 "${HOME_DIR}/.ssh" | |
install -m 600 /dev/null "${HOME_DIR}/.ssh/authorized_keys" 2>/dev/null || true | |
# Объединяем, убираем дубликаты | |
cat /root/.ssh/authorized_keys "${HOME_DIR}/.ssh/authorized_keys" \ | |
| sort -u > "${HOME_DIR}/.ssh/authorized_keys.tmp" | |
mv "${HOME_DIR}/.ssh/authorized_keys.tmp" "${HOME_DIR}/.ssh/authorized_keys" | |
chown -R "${APP_USER}:${APP_USER}" "${HOME_DIR}/.ssh" | |
chmod 600 "${HOME_DIR}/.ssh/authorized_keys" | |
ok "Ключи добавлены; существующие не затронуты" | |
else | |
warn "У root нет /root/.ssh/authorized_keys — копировать нечего" | |
fi | |
################ Базовые функции ################ | |
update_system(){ | |
msg "Обновление системы и установка базовых пакетов" | |
apt update && apt upgrade -y | |
apt install -y \ | |
vim mosh tmux htop git curl wget unzip zip gcc build-essential make \ | |
software-properties-common | |
ok "Система обновлена и базовые пакеты установлены" | |
} | |
install_ohmyzsh(){ | |
if [[ -d "${HOME_DIR}/.oh-my-zsh" ]]; then | |
warn "Oh My Zsh уже установлен для ${APP_USER}" | |
return | |
fi | |
msg "Установка Oh My Zsh + тема Agnoster (для ${APP_USER})" | |
apt install -y zsh fonts-powerline | |
su - "$APP_USER" -c \ | |
'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended' | |
ZSHRC_PATH=$(eval echo "~${APP_USER}")/.zshrc | |
[[ -f $ZSHRC_PATH ]] && \ | |
sed -i 's/^ZSH_THEME=.*/ZSH_THEME="agnoster"/' "$ZSHRC_PATH" || \ | |
warn ".zshrc не найден — тему Agnoster пропустили" | |
chsh -s "$(command -v zsh)" "$APP_USER" | |
ok "Oh My Zsh установлен для ${APP_USER}" | |
} | |
install_python(){ | |
if command -v python3 &>/dev/null; then | |
ok "Python уже установлен" | |
else | |
msg "Установка Python" | |
apt install -y python3 python3-pip python3-venv | |
fi | |
} | |
install_poetry(){ | |
if su - "$APP_USER" -c 'command -v poetry' &>/dev/null; then | |
warn "Poetry уже установлен" | |
return | |
fi | |
msg "Установка Poetry для ${APP_USER}" | |
su - "$APP_USER" -c 'curl -sSL https://install.python-poetry.org | python3 -' | |
# Добавляем ~/.local/bin во все популярные shell‑конфиги пользователя | |
for RC in "${HOME_DIR}/.profile" "${HOME_DIR}/.bashrc" "${HOME_DIR}/.zshrc"; do | |
su - "$APP_USER" -c "grep -qxF 'export PATH=\$HOME/.local/bin:\$PATH' ${RC} 2>/dev/null || echo 'export PATH=\$HOME/.local/bin:\$PATH' >> ${RC}" | |
done | |
ok "Poetry установлен и добавлен в PATH" | |
} | |
install_postgres(){ | |
if command -v psql &>/dev/null; then | |
warn "PostgreSQL уже установлен" | |
systemctl enable --now postgresql | |
return | |
fi | |
msg "Установка PostgreSQL" | |
apt install -y postgresql postgresql-contrib | |
systemctl enable --now postgresql | |
ok "PostgreSQL установлен" | |
read -rp "Задать пароль для пользователя postgres? (y/N): " SET_PG_PW | |
if [[ $SET_PG_PW =~ ^[Yy]$ ]]; then | |
read -rsp "Введите пароль: " PGPASS; echo | |
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '${PGPASS}';" | |
ok "Пароль postgres обновлён" | |
fi | |
read -rp "Создать нового пользователя БД? (y/N): " CREATE_DB_USER | |
if [[ $CREATE_DB_USER =~ ^[Yy]$ ]]; then | |
read -rp "Имя нового пользователя: " DBUSER | |
read -rsp "Пароль для ${DBUSER}: " DBPASS; echo | |
read -rp "Имя базы (Enter = ${DBUSER}_db): " DBNAME | |
DBNAME=${DBNAME:-${DBUSER}_db} | |
sudo -u postgres psql <<EOF | |
CREATE USER ${DBUSER} WITH PASSWORD '${DBPASS}'; | |
CREATE DATABASE ${DBNAME} OWNER ${DBUSER}; | |
GRANT ALL PRIVILEGES ON DATABASE ${DBNAME} TO ${DBUSER}; | |
EOF | |
ok "Пользователь ${DBUSER} и база ${DBNAME} созданы" | |
fi | |
} | |
install_pgbouncer(){ | |
if systemctl is-active pgbouncer | grep active; then | |
warn "PgBouncer уже установлен" | |
systemctl enable --now pgbouncer | |
return | |
fi | |
msg "Установка и настройка PgBouncer (md5‑пароль)" | |
apt install -y pgbouncer | |
read -rp "Название базы (новая созданная БД ногово пользователя, default: postgres): " PGB_DB | |
PGB_DB=${PGB_DB:-postgres} | |
read -rp "DB‑user (новый пользователь, default: postgres): " PGB_USER | |
PGB_USER=${PGB_USER:-postgres} | |
read -rsp "Пароль для ${PGB_USER}: " PGB_PASS; echo | |
HASH=$(echo -n "${PGB_PASS}${PGB_USER}" | md5sum | awk '{print $1}') | |
HASH="md5${HASH}" | |
cat > /etc/pgbouncer/pgbouncer.ini <<EOF | |
[databases] | |
${PGB_DB} = host=127.0.0.1 port=5432 dbname=${PGB_DB} | |
[pgbouncer] | |
listen_addr = 0.0.0.0 | |
listen_port = 6432 | |
auth_type = md5 | |
auth_file = /etc/pgbouncer/userlist.txt | |
pool_mode = transaction | |
max_client_conn = 100 | |
default_pool_size = 20 | |
ignore_startup_parameters = extra_float_digits | |
EOF | |
echo "\"${PGB_USER}\" \"${HASH}\"" > /etc/pgbouncer/userlist.txt | |
chown ${APP_USER}:${APP_USER} /etc/pgbouncer/userlist.txt | |
chmod 600 /etc/pgbouncer/userlist.txt | |
systemctl enable --now pgbouncer | |
ok "PgBouncer запущен (порт 6432)" | |
} | |
install_nginx(){ | |
if command -v nginx &>/dev/null; then | |
warn "Nginx уже установлен" | |
else | |
msg "Установка Nginx" | |
apt install -y nginx | |
fi | |
systemctl enable --now nginx | |
ok "Nginx установлен" | |
} | |
install_redis(){ | |
if command -v redis-server &>/dev/null; then | |
warn "Redis уже установлен" | |
systemctl enable --now redis-server | |
return | |
fi | |
msg "Установка Redis" | |
apt install -y redis-server | |
systemctl enable --now redis-server | |
ok "Redis готов" | |
} | |
install_docker(){ | |
if command -v docker &>/dev/null; then | |
warn "Docker уже установлен" | |
else | |
msg "Установка Docker" | |
curl -fsSL https://get.docker.com | bash | |
fi | |
usermod -aG docker "$APP_USER" | |
if ! command -v docker-compose &>/dev/null; then | |
msg "Установка docker‑compose" | |
DC_VER=$(curl -fsSL https://api.github.com/repos/docker/compose/releases/latest | grep tag_name | cut -d\" -f4) | |
curl -L "https://github.com/docker/compose/releases/download/${DC_VER}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose | |
chmod +x /usr/local/bin/docker-compose | |
else | |
warn "docker‑compose уже установлен" | |
fi | |
ok "Docker + compose готовы" | |
} | |
configure_ufw(){ | |
if command -v ufw &>/dev/null && ufw status | grep -q active; then | |
warn "UFW уже активен" | |
return | |
fi | |
msg "Настройка UFW" | |
apt install -y ufw | |
ufw allow OpenSSH | |
ufw allow 'Nginx Full' | |
ufw --force enable | |
ok "UFW включён" | |
} | |
configure_fail2ban(){ | |
if systemctl is-active fail2ban | grep active; then | |
warn "Fail2ban уже установлен" | |
systemctl enable --now fail2ban | |
return | |
fi | |
msg "Установка Fail2ban" | |
apt install -y fail2ban | |
systemctl enable --now fail2ban | |
ok "Fail2ban защищает SSH" | |
} | |
configure_ssl(){ | |
if command -v certbot &>/dev/null; then | |
warn "Certbot уже установлен" | |
else | |
apt install -y certbot python3-certbot-nginx | |
fi | |
read -rp "Введите домен (example.com, оставьте пустым для пропуска): " DOMAIN | |
[[ -z $DOMAIN ]] && { warn "Домен пуст — SSL пропущен"; return; } | |
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos -m admin@"$DOMAIN" --redirect | |
ok "SSL настроен для $DOMAIN" | |
} | |
harden_ssh() { | |
msg "Усиление SSH" | |
# 1. Меняем конфиг | |
sed -i.bak -E 's/^#?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config | |
sed -i -E 's/^#?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config | |
# 2. Выбираем правильный systemd‑юнит (ssh или sshd) | |
if systemctl list-unit-files | grep -q '^sshd\.service'; then | |
SSH_UNIT=sshd | |
else | |
SSH_UNIT=ssh | |
fi | |
# 3. Перезагружаем службу | |
systemctl reload "${SSH_UNIT}" | |
ok "Root‑вход и пароли по SSH отключены (юнит: ${SSH_UNIT}.service)" | |
} | |
enable_unattended(){ | |
msg "Включение автоматических обновлений безопасности" | |
apt install -y unattended-upgrades | |
dpkg-reconfigure -f noninteractive unattended-upgrades | |
ok "Unattended‑upgrades активирован" | |
} | |
install_supervisor(){ | |
if systemctl is-active supervisor | grep active; then | |
warn "Supervisor уже установлен" | |
systemctl enable --now supervisor | |
return | |
fi | |
msg "Установка Supervisor" | |
apt install -y supervisor | |
systemctl enable --now supervisor | |
ok "Supervisor готов" | |
} | |
install_nodejs(){ | |
if su - "$APP_USER" -c 'command -v node' &>/dev/null; then | |
warn "Node.js уже установлен у ${APP_USER}" | |
return | |
fi | |
msg "Установка nvm + Node.js (LTS) для ${APP_USER}" | |
su - "$APP_USER" -c 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash' | |
su - "$APP_USER" -c 'export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh" && nvm install --lts' | |
ok "Node.js установлен" | |
} | |
create_swap(){ | |
[[ -f /swapfile ]] && { warn "Swap уже существует"; return; } | |
read -rp "Сколько ГБ swap создать? [2]: " SWAP_GB | |
SWAP_GB=${SWAP_GB:-2} | |
[[ $SWAP_GB =~ ^[0-9]+$ ]] || { err "Нужно целое число"; exit 1; } | |
msg "Создание ${SWAP_GB} ГБ swap" | |
fallocate -l "${SWAP_GB}G" /swapfile | |
chmod 600 /swapfile | |
mkswap /swapfile | |
swapon /swapfile | |
echo '/swapfile none swap sw 0 0' >> /etc/fstab | |
ok "Swap ${SWAP_GB} ГБ активирован" | |
} | |
################ Меню ################ | |
show_menu(){ | |
cat <<'EOF' | |
╔═══════════════════════════════════════════════════════════════════════╗ | |
║ Быстая настройка VPS ║ | |
║ Доступные опции ║ | |
╠═══════════════════════════════════════════════════════════════════════╣ | |
║ 1) Обновление системы │ 10) Fail2ban ║ | |
║ 2) Oh My Zsh + Agnoster │ 11) SSL (LE) ║ | |
║ 3) Python │ 12) AutoUpdates ║ | |
║ 4) PostgreSQL │ 13) Supervisor ║ | |
║ 5) PgBouncer │ 14) Node.js ║ | |
║ 6) Nginx │ 15) Swap‑файл ║ | |
║ 7) Redis │ 16) Poetry ║ | |
║ 8) Docker │ 17) Усилить SSH ║ | |
║ 9) UFW Firewall │ 18) Всё сразу (без UFW и SSH‑hard) ║ | |
╚═══════════════════════════════════════════════════════════════════════╝ | |
Введите номера через пробел: | |
EOF | |
read -r CHOICES | |
} | |
################ Запуск выбранных задач ################ | |
run_tasks(){ | |
for CH in $*; do | |
case $CH in | |
1) update_system ;; | |
2) install_ohmyzsh ;; | |
3) install_python ;; | |
4) install_postgres ;; | |
5) install_pgbouncer ;; | |
6) install_nginx ;; | |
7) install_redis ;; | |
8) install_docker ;; | |
9) configure_ufw ;; | |
10) configure_fail2ban ;; | |
11) configure_ssl ;; | |
12) enable_unattended ;; | |
13) install_supervisor ;; | |
14) install_nodejs ;; | |
15) create_swap ;; | |
16) install_poetry ;; | |
17) harden_ssh ;; | |
18) run_tasks "1 2 3 4 5 6 7 8 10 11 12 13 14 15 16" ;; # всё, но без UFW (9) и SSH‑hard (17) | |
*) warn "Неизвестная опция: $CH" ;; | |
esac | |
done | |
} | |
################ Тихий режим (YAML) ################ | |
CFG_FILE="" | |
[[ ${1:-} == "-c" ]] && CFG_FILE="$2" | |
if [[ -n $CFG_FILE ]]; then | |
command -v yq >/dev/null || apt install -y yq | |
CHOICES=$(yq '.tasks[]' "$CFG_FILE" | paste -sd' ' -) | |
else | |
show_menu | |
fi | |
run_tasks $CHOICES | |
ok "Все операции завершены!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment