Skip to content

Instantly share code, notes, and snippets.

@nodir-malikov
Last active April 11, 2025 11:44
Show Gist options
  • Save nodir-malikov/b8f61d9667f27519cc88ebcf594a4e09 to your computer and use it in GitHub Desktop.
Save nodir-malikov/b8f61d9667f27519cc88ebcf594a4e09 to your computer and use it in GitHub Desktop.
Автоматическая настройка VPS под Python проекты
#!/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