Files
3dp-manager/install.sh
T
Den Piligrim 1077f11fc7 fix creds
2026-05-21 23:33:30 +03:00

658 lines
21 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
set -euo pipefail
#################################
# КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ
#################################
PROJECT_DIR="/opt/3dp-manager"
DOCKER_USER="denpiligrim"
DOCKER_TAG="dp-new-release"
IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}"
IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}"
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
die() { error "$1"; }
resolve_compose_cmd() {
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=("docker" "compose")
return 0
fi
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=("docker-compose")
return 0
fi
warn "Не найден Docker Compose (ни v2 plugin, ни v1 binary). Пытаемся установить..."
apt-get update
apt-get install -y docker-compose-plugin || true
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=("docker" "compose")
return 0
fi
apt-get install -y docker-compose || true
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=("docker-compose")
return 0
fi
die "Не удалось установить Docker Compose. Установите docker compose plugin (v2) или docker-compose (v1)."
}
check_containers_running() {
log "Проверка статуса контейнеров..."
local timeout=${1:-60}
local elapsed=0
local failed=0
while [ $elapsed -lt $timeout ]; do
failed=0
# Получаем статус всех контейнеров текущего compose проекта
# Формат: NAME\tSTATUS (например: "3dp-postgres\tUp 2 days" или "3dp-postgres\tError")
while IFS=$'\t' read -r container_name status; do
if [ -n "$container_name" ] && [ -n "$status" ]; then
# Проверяем, что статус содержит Up/running/healthy/restarting
# Up, Up 2 days, Up Less than a second, (healthy), running, restarting
if ! echo "$status" | grep -qiE "^up|running|healthy|restarting"; then
failed=1
warn "Контейнер $container_name в статусе: $status"
fi
fi
done < <("${COMPOSE_CMD[@]}" ps --format "table {{.Name}}\t{{.Status}}" --all 2>/dev/null | tail -n +2)
if [ $failed -eq 0 ]; then
log "Все контейнеры запущены успешно"
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
return 1
}
get_random_port() {
local MIN=${1:-3000}
local MAX=${2:-6999}
while :; do
PORT=$(shuf -i "$MIN-$MAX" -n 1)
if ! ss -ltun | awk '{print $4}' | grep -q ":$PORT\$"; then
echo "$PORT"
return
fi
done
}
cleanup_previous_install_data() {
log "Checking for previous 3dp-manager installation data..."
if [[ -f docker-compose.yml ]]; then
warn "Existing docker-compose.yml found. Stopping previous installation and removing its volumes so newly generated credentials are used."
"${COMPOSE_CMD[@]}" down --volumes --remove-orphans || true
else
warn "docker-compose.yml not found. Removing known 3dp-manager containers if they still exist."
fi
docker rm -f 3dp-postgres 3dp-backend 3dp-frontend >/dev/null 2>&1 || true
for volume in 3dp-manager_pg_data 3dpmanager_pg_data; do
if docker volume inspect "$volume" >/dev/null 2>&1; then
warn "Removing stale Postgres volume: $volume"
docker volume rm "$volume" >/dev/null 2>&1 || true
fi
done
}
#################################
# ASCII-баннер
#################################
echo "==================================================="
echo " ____ ____ _ ___ _ "
echo " / __ \___ ____ / __ \(_) (_)___ ______(_)___ ___ "
echo " / / / / _ \/ __ \/ /_/ / / / / __ \/ ___/ / __ \`__ \ "
echo " / /_/ / __/ / / / ____/ / / / /_/ / / / / / / / / /"
echo "/_____/\___/_/ /_/_/ /_/_/_/\__, /_/ /_/_/ /_/ /_/ "
echo " /____/ "
echo ""
echo " 3DP-MANAGER FOR 3X-UI "
echo "==================================================="
echo ""
#################################
# ПРОВЕРКИ И УСТАНОВКА ЗАВИСИМОСТЕЙ
#################################
if [[ $EUID -ne 0 ]]; then
error "Этот скрипт должен быть запущен от имени root"
fi
. /etc/os-release
if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then
die "Этот скрипт поддерживает только Ubuntu или Debian: $ID"
fi
if [ $(free -m | grep Mem: | awk '{print $2}') -lt 2000 ]; then
if [ $(free -m | grep Swap: | awk '{print $2}') -eq 0 ]; then
log "Мало RAM и нет Swap. Создаем swap-файл 2GB..."
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
log "Swap создан."
fi
fi
log "Проверка зависимостей..."
if ! command -v curl &> /dev/null; then apt-get update && apt-get install -y curl; fi
if ! command -v jq &> /dev/null; then apt-get install -y jq; fi
if ! command -v openssl &> /dev/null; then apt-get install -y openssl; fi
if ! command -v tar &> /dev/null; then apt-get install -y tar; fi
if ! command -v hostname &> /dev/null; then apt-get install -y net-tools || apt-get install -y hostname; fi
# Установка Docker
log "Проверка Docker"
if command -v docker >/dev/null 2>&1; then
log "Docker уже установлен"
else
log "Docker не найден, будет установлен из официального репозитория"
# Add Docker's official GPG key:
apt update
apt install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
if [[ "$ID" == "ubuntu" ]]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc || die "Ошибка добавления ключа Docker"
else
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc || die "Ошибка добавления ключа Docker"
fi
chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME}
tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: $CODENAME
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable docker
systemctl start docker
fi
resolve_compose_cmd
log "Compose команда: ${COMPOSE_CMD[*]}"
#################################
# ЗАГРУЗКА ПРОЕКТА
#################################
log "Подготовка директории $PROJECT_DIR..."
mkdir -p "$PROJECT_DIR"
mkdir -p "$PROJECT_DIR/server"
mkdir -p "$PROJECT_DIR/client"
cd "$PROJECT_DIR"
cleanup_previous_install_data
#################################
# СБОР ДАННЫХ: SSL / HTTPS
#################################
UI_HOST=""
USE_SSL=false
CERT_PATH=""
KEY_PATH=""
echo ""
echo -e "${GREEN}Выберите тип SSL/HTTPS сертификации:${NC}"
echo " 1) HTTPS — Let's Encrypt (нужен реальный домен, привязанный к IP, + открыты порты 80/443 в UFW)"
echo " 2) HTTPS — Self-signed (самоподписанный на IP сервера, для тестов/VM)"
echo " 3) HTTPS — Свои сертификаты (указать пути)"
echo " 4) HTTP — без шифрования"
echo ""
printf "Ваш выбор (1/2/3/4) [\033[0;31m4\033[0m]: "
read -r ssl_choice
case "$ssl_choice" in
1)
echo ""
read -rp "Введите домен (должен быть привязан к IP этого сервера): " INPUT_HOST
if [ -z "$INPUT_HOST" ]; then
warn "Домен не указан. Переключение на HTTP."
else
UI_HOST="$INPUT_HOST"
LE_CERT="/etc/letsencrypt/live/$UI_HOST/fullchain.pem"
LE_KEY="/etc/letsencrypt/live/$UI_HOST/privkey.pem"
NEED_NEW_CERT=true
if [[ -f "$LE_CERT" && -f "$LE_KEY" ]]; then
# Проверяем что сертификат не истёк и ключ соответствует
if openssl x509 -checkend 0 -noout -in "$LE_CERT" 2>/dev/null && \
openssl x509 -noout -pubkey -in "$LE_CERT" 2>/dev/null > /tmp/cert_pub && \
openssl pkey -pubout -in "$LE_KEY" 2>/dev/null > /tmp/key_pub && \
diff -q /tmp/cert_pub /tmp/key_pub >/dev/null 2>&1; then
log "Найдены валидные сертификаты для $UI_HOST."
USE_SSL=true
CERT_PATH="$LE_CERT"
KEY_PATH="$LE_KEY"
NEED_NEW_CERT=false
else
warn "Найдены сертификаты для $UI_HOST, но они истекли или невалидны. Очистка..."
# Удаляем невалидные файлы
rm -rf "/etc/letsencrypt/live/$UI_HOST"
rm -rf "/etc/letsencrypt/archive/$UI_HOST"
rm -rf "/etc/letsencrypt/renewal/$UI_HOST.conf"
fi
fi
if [ "$NEED_NEW_CERT" = true ]; then
log "Получение Let's Encrypt сертификата для $UI_HOST..."
read -e -p "Email для уведомлений Let's Encrypt: " LE_EMAIL
LE_EMAIL=$(echo "$LE_EMAIL" | tr -cd 'a-zA-Z0-9.@_-')
if command -v certbot &>/dev/null; then
log "Certbot уже установлен."
else
log "Установка certbot..."
apt update
apt install -y certbot
fi
# КРИТИЧЕСКИЙ ШАГ: Принудительно удаляем старые сертификаты из базы Certbot
certbot delete --cert-name "$UI_HOST" --non-interactive >/dev/null 2>&1 || true
# Запрашиваем новый сертификат принудительно (--force-renewal и --cert-name)
certbot certonly --standalone \
--agree-tos \
--non-interactive \
--force-renewal \
--cert-name "$UI_HOST" \
--email "$LE_EMAIL" \
-d "$UI_HOST" || warn "Не удалось получить сертификат Let's Encrypt"
if [[ -f "$LE_CERT" && -f "$LE_KEY" ]]; then
USE_SSL=true
CERT_PATH="$LE_CERT"
KEY_PATH="$LE_KEY"
log "Let's Encrypt сертификат получен для $UI_HOST"
else
warn "Сертификат не получен. Переключение на HTTP."
fi
fi
fi
;;
2)
UI_HOST=$(hostname -I | awk '{print $1}')
log "Генерация самоподписанного сертификата для $UI_HOST..."
mkdir -p "/etc/letsencrypt/live/$UI_HOST"
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout "/etc/letsencrypt/live/$UI_HOST/privkey.pem" \
-out "/etc/letsencrypt/live/$UI_HOST/fullchain.pem" \
-subj "/CN=$UI_HOST" \
-addext "subjectAltName=IP:$UI_HOST" 2>/dev/null
if [[ -f "/etc/letsencrypt/live/$UI_HOST/fullchain.pem" && -f "/etc/letsencrypt/live/$UI_HOST/privkey.pem" ]]; then
USE_SSL=true
CERT_PATH="/etc/letsencrypt/live/$UI_HOST/fullchain.pem"
KEY_PATH="/etc/letsencrypt/live/$UI_HOST/privkey.pem"
log "Self-signed сертификат сгенерирован для $UI_HOST"
warn "Браузер будет предупреждать — это нормально для тестов."
else
warn "Не удалось сгенерировать сертификат. Переключение на HTTP."
fi
;;
3)
read -rp "Введите домен для панели (например panel.example.com; Enter — использовать IP): " INPUT_HOST
read -rp "Путь к fullchain.pem: " user_cert
read -rp "Путь к privkey.pem: " user_key
if [[ -f "$user_cert" && -f "$user_key" ]]; then
USE_SSL=true
CERT_PATH="$user_cert"
KEY_PATH="$user_key"
if [ -n "$INPUT_HOST" ]; then
UI_HOST="$INPUT_HOST"
else
UI_HOST=$(hostname -I | awk '{print $1}')
warn "Домен не указан. В итоговом URL будет использован IP: $UI_HOST"
fi
else
warn "Файлы не найдены. Переключение на HTTP."
fi
;;
*)
UI_HOST=$(hostname -I | awk '{print $1}')
log "Будет использоваться HTTP."
;;
esac
if [ -z "$UI_HOST" ]; then
UI_HOST=$(hostname -I | awk '{print $1}')
log "Используется IP сервера: $UI_HOST"
fi
#################################
# СБОР ДАННЫХ
################################
FINAL_PORT=$(get_random_port)
# --- 4. Генерация паролей ---
DB_PASS=$(openssl rand -base64 12)
JWT_SECRET=$(openssl rand -base64 32)
ADMIN_USER=$(openssl rand -base64 8)
ADMIN_PASS=$(openssl rand -base64 12)
# Определяем ALLOWED_ORIGINS из домена или IP
if [[ -n "${UI_HOST:-}" ]]; then
URL_SCHEME="http"
if [[ "$USE_SSL" == "true" ]]; then
URL_SCHEME="https"
fi
ALLOWED_ORIGINS="${URL_SCHEME}://${UI_HOST}"
else
ALLOWED_ORIGINS=""
fi
log "Сгенерированы секретные ключи для БД и JWT."
#################################
# ГЕНЕРАЦИЯ ФАЙЛОВ DOCKER
#################################
cat > server/.env <<EOF
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=admin
DB_PASSWORD=${DB_PASS}
DB_NAME=3dp_manager
ADMIN_LOGIN=${ADMIN_USER}
ADMIN_PASSWORD=${ADMIN_PASS}
PORT=3100
LOG_LEVEL=error
ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-}
EOF
if [[ "$USE_SSL" == "true" ]]; then
# === ВАРИАНТ С SSL ===
# 1. Nginx Config
cat > client/nginx-client.conf <<EOF
server {
listen 443 ssl;
server_name $UI_HOST;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50M;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3100/api/;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
location /bus/ {
proxy_pass http://backend:3100/bus/;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
}
EOF
# 2. Docker Compose
cat > docker-compose.yml <<EOF
name: 3dp-manager
services:
postgres:
image: postgres:18-alpine
container_name: 3dp-postgres
restart: always
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: 3dp_manager
volumes:
- pg_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d 3dp_manager"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
backend:
image: ${IMAGE_SERVER}
container_name: 3dp-backend
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: admin
DB_PASSWORD: ${DB_PASS}
DB_NAME: 3dp_manager
JWT_SECRET: ${JWT_SECRET}
ADMIN_LOGIN: ${ADMIN_USER}
ADMIN_PASSWORD: ${ADMIN_PASS}
PORT: 3100
networks:
- app-network
frontend:
image: ${IMAGE_CLIENT}
container_name: 3dp-frontend
restart: always
depends_on:
- backend
ports:
- "${FINAL_PORT}:443"
volumes:
- ./client/nginx-client.conf:/etc/nginx/conf.d/default.conf:ro
- ${CERT_PATH}:/etc/nginx/certs/fullchain.pem:ro
- ${KEY_PATH}:/etc/nginx/certs/privkey.pem:ro
networks:
- app-network
volumes:
pg_data:
networks:
app-network:
driver: bridge
EOF
else
# === ВАРИАНТ БЕЗ SSL (HTTP) ===
# 1. Nginx Config
cat > client/nginx-client.conf <<EOF
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50M;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3100/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$http_host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
location /bus/ {
proxy_pass http://backend:3100/bus/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$http_host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
}
EOF
# 2. Docker Compose
cat > docker-compose.yml <<EOF
name: 3dp-manager
services:
postgres:
image: postgres:18-alpine
container_name: 3dp-postgres
restart: always
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: 3dp_manager
volumes:
- pg_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d 3dp_manager"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
backend:
image: ${IMAGE_SERVER}
container_name: 3dp-backend
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: admin
DB_PASSWORD: ${DB_PASS}
DB_NAME: 3dp_manager
JWT_SECRET: ${JWT_SECRET}
ADMIN_LOGIN: ${ADMIN_USER}
ADMIN_PASSWORD: ${ADMIN_PASS}
PORT: 3100
networks:
- app-network
frontend:
image: ${IMAGE_CLIENT}
container_name: 3dp-frontend
restart: always
depends_on:
- backend
ports:
- "${FINAL_PORT}:80"
volumes:
- ./client/nginx-client.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- app-network
volumes:
pg_data:
networks:
app-network:
driver: bridge
EOF
fi
#################################
# ЗАПУСК
#################################
log "Сборка и запуск контейнеров..."
# Останавливаем старые, если были
"${COMPOSE_CMD[@]}" down || true
# Подтягиваем свежие образы, потому что тег релиза переиспользуется.
"${COMPOSE_CMD[@]}" pull || warn "Не удалось обновить образы перед запуском. Будет использован локальный кэш, если он есть."
# Запускаем сборку и старт
"${COMPOSE_CMD[@]}" up --build -d --remove-orphans
# Проверка: все ли контейнеры запустились
if ! check_containers_running 60; then
error "Не удалось запустить контейнеры. Логи:"
"${COMPOSE_CMD[@]}" logs --tail=50
die "Установка прервана из-за ошибки запуска контейнеров"
fi
log "Очистка кэша сборки..."
docker image prune -f
if LC_ALL=C ufw status 2>/dev/null | grep -q "Status: active"; then
echo "Найден активный UFW. Настраиваю правила..."
ufw allow 443/tcp
ufw allow 443/udp
ufw allow 8443/tcp
ufw allow 8443/udp
ufw allow 10000:60000/tcp
ufw allow 10000:60000/udp
fi
echo ""
echo "==================================================="
if [[ "$USE_SSL" == "true" ]]; then
echo -e "${GREEN}✔ Установка завершена! Доступно по адресу: https://${UI_HOST}:${FINAL_PORT}${NC}"
else
echo -e "${GREEN}✔ Установка завершена! Доступно по адресу: http://${UI_HOST}:${FINAL_PORT}${NC}"
fi
echo -e "${GREEN}Логин: ${ADMIN_USER}${NC}"
echo -e "${GREEN}Пароль: ${ADMIN_PASS}${NC}"
echo ""
echo "Немедленно измените пароль в Настройках утилиты!"
echo "==================================================="