Scripts: Dokploy + Nginx Proxy Manager(OpenAppSec)

beta version

#!/bin/bash

# =============================================================================
# DOKPLOY + NGINX PROXY MANAGER + OPENAPPSEC INSTALLATION SCRIPT
# =============================================================================
# 
# Этот скрипт автоматически устанавливает и настраивает:
# - Dokploy (платформа для развертывания Docker приложений)
# - Nginx Proxy Manager (обратный прокси с веб-интерфейсом)
# - OpenAppSec (система защиты веб-приложений)
# 
# Архитектура: Internet → NPM + OpenAppSec → Dokploy Apps (БЕЗ Traefik)
# 
# Использование:
#   sudo ./install-dokploy-nginx-organized.sh        # Установка
#   sudo ./install-dokploy-nginx-organized.sh update # Обновление
#   sudo ./install-dokploy-nginx-organized.sh clean  # Очистка
# =============================================================================

set -e

# =============================================================================
# РАЗДЕЛ 1: НАСТРОЙКИ И ПЕРЕМЕННЫЕ
# =============================================================================
# Здесь можно изменить основные параметры установки

# Основные настройки
DOKPLOY_PORT="${PORT:-3000}"                    # Порт для Dokploy Dashboard
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}" # Email администратора NPM
NPM_ADMIN_PASSWORD="${NPM_ADMIN_PASSWORD:-SecurePassword123!}" # Пароль NPM
RELEASE_TAG="${RELEASE_TAG:-latest}"            # Версия Dokploy

# Цвета для логирования
RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"
NC="\033[0m" # No Color

# =============================================================================
# РАЗДЕЛ 2: ФУНКЦИИ ЛОГИРОВАНИЯ
# =============================================================================
# Функции для красивого вывода сообщений

log_info() {
    printf "${BLUE}[INFO]${NC} $1\n"
}

log_success() {
    printf "${GREEN}[SUCCESS]${NC} $1\n"
}

log_warning() {
    printf "${YELLOW}[WARNING]${NC} $1\n"
}

log_error() {
    printf "${RED}[ERROR]${NC} $1\n"
}

# =============================================================================
# РАЗДЕЛ 3: ПРОВЕРКА ПОРТОВ И СИСТЕМЫ
# =============================================================================
# Проверяет доступность портов и системные требования

check_port_open() {
    local port=$1
    local protocol=${2:-tcp}
    
    # Проверяем, слушает ли порт
    if ss -tulnp | grep ":${port} " >/dev/null 2>&1; then
        return 0  # Порт открыт/слушает
    else
        return 1  # Порт не открыт/не слушает
    fi
}

check_requirements() {
    log_info "Проверка системных требований..."
    
    # Проверяем, что запущено от root
    if [ "$(id -u)" != "0" ]; then
        log_error "Этот скрипт должен быть запущен от имени root"
        exit 1
    fi

    # Проверяем, что это не Mac OS
    if [ "$(uname)" = "Darwin" ]; then
        log_error "Этот скрипт должен быть запущен на Linux"
        exit 1
    fi

    # Проверяем, что не запущено внутри контейнера
    if [ -f /.dockerenv ]; then
        log_error "Этот скрипт должен быть запущен на хост-системе Linux, не внутри контейнера"
        exit 1
    fi

    # Проверяем, что порт 80 свободен (будет использоваться NPM)
    if ss -tulnp | grep ':80 ' >/dev/null; then
        log_error "Порт 80 уже используется (требуется для Nginx Proxy Manager)"
        exit 1
    fi

    # Проверяем, что порт 443 свободен (будет использоваться NPM)
    if ss -tulnp | grep ':443 ' >/dev/null; then
        log_error "Порт 443 уже используется (требуется для Nginx Proxy Manager)"
        exit 1
    fi

    # Проверяем, что порт Dokploy свободен
    if ss -tulnp | grep ":${DOKPLOY_PORT} " >/dev/null; then
        log_error "Порт ${DOKPLOY_PORT} уже используется (требуется для Dokploy)"
        exit 1
    fi

    log_success "Проверка системных требований пройдена"
}

# =============================================================================
# РАЗДЕЛ 4: НАСТРОЙКА FIREWALL (NFTABLES)
# =============================================================================
# Настраивает firewall для открытия только портов 80 и 443

detect_and_remove_ufw() {
    log_info "Проверка UFW firewall..."
    
    # Проверяем, установлен ли UFW
    if command -v ufw >/dev/null 2>&1; then
        log_info "UFW обнаружен, удаляем его..."
        
        # Отключаем UFW
        ufw --force disable >/dev/null 2>&1 || true
        
        # Останавливаем сервис UFW
        systemctl stop ufw >/dev/null 2>&1 || true
        systemctl disable ufw >/dev/null 2>&1 || true
        
        # Удаляем пакет UFW
        if command -v apt-get >/dev/null 2>&1; then
            apt-get remove -y ufw >/dev/null 2>&1 || true
            apt-get purge -y ufw >/dev/null 2>&1 || true
        elif command -v yum >/dev/null 2>&1; then
            yum remove -y ufw >/dev/null 2>&1 || true
        elif command -v dnf >/dev/null 2>&1; then
            dnf remove -y ufw >/dev/null 2>&1 || true
        fi
        
        log_success "UFW успешно удален"
    else
        log_info "UFW не найден, продолжаем настройку nftables"
    fi
}

check_ports_accessible() {
    log_info "Проверка конфигурации firewall..."
    
    # Тестируем, доступны ли порты 80 и 443
    local port_80_ok=false
    local port_443_ok=false
    
    # Проверяем, блокирует ли nftables порты
    if command -v nft >/dev/null 2>&1; then
        # Проверяем, разрешены ли порты в nftables
        if nft list ruleset 2>/dev/null | grep -q "tcp dport { 80, 443 } accept\|tcp dport 80 accept.*tcp dport 443 accept"; then
            port_80_ok=true
            port_443_ok=true
        fi
    else
        # Если нет nftables, предполагаем, что порты нужно настроить
        port_80_ok=false
        port_443_ok=false
    fi
    
    if [ "$port_80_ok" = true ] && [ "$port_443_ok" = true ]; then
        log_success "Порты 80 и 443 правильно настроены"
        return 0
    else
        log_info "Порты 80 и 443 нуждаются в настройке"
        return 1
    fi
}

install_nftables() {
    log_info "Установка nftables..."
    
    # Исправляем проблему с рабочей директорией
    cd /tmp
    
    # Устанавливаем nftables
    if command -v apt-get >/dev/null 2>&1; then
        apt-get update -qq
        apt-get install -y -qq nftables
    elif command -v yum >/dev/null 2>&1; then
        yum install -y nftables
    elif command -v dnf >/dev/null 2>&1; then
        dnf install -y nftables
    else
        log_error "Менеджер пакетов не найден, не удается установить nftables"
        return 1
    fi
    
    # Включаем и запускаем сервис nftables
    systemctl enable nftables
    systemctl start nftables
    
    log_success "nftables установлен и запущен"
}

remove_other_firewalls() {
    log_info "Отключение ufw..."
    
    if command -v ufw >/dev/null 2>&1; then
        ufw --force disable
        systemctl stop ufw
        systemctl disable ufw
        log_success "ufw отключен"
    fi
    
    # Также отключаем iptables-persistent если присутствует
    if systemctl is-enabled netfilter-persistent >/dev/null 2>&1; then
        systemctl stop netfilter-persistent >/dev/null 2>&1 || true
        systemctl disable netfilter-persistent >/dev/null 2>&1 || true
        log_info "netfilter-persistent отключен"
    fi
}

configure_nftables() {
    log_info "Настройка правил nftables firewall..."
    
    # Создаем конфигурацию nftables
    cat > /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f

# Очищаем все предыдущие состояния
flush ruleset

# Определяем переменные - ТОЛЬКО порты 80 и 443
define ALLOWED_TCP_PORTS = { 80, 443 }

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        
        # Разрешаем loopback трафик
        iif "lo" accept
        
        # Разрешаем установленные и связанные соединения
        ct state established,related accept
        
        # Разрешаем ICMP (ping)
        ip protocol icmp accept
        ip6 nexthdr ipv6-icmp accept
        
        # Разрешаем ТОЛЬКО HTTP (80) и HTTPS (443) отовсюду
        tcp dport $ALLOWED_TCP_PORTS accept
        
        # Логируем и отбрасываем все остальное
        log prefix "nftables-drop: " drop
    }
    
    chain forward {
        type filter hook forward priority filter; policy accept;
        
        # Разрешаем Docker контейнерам общаться
        iifname "docker*" accept
        oifname "docker*" accept
    }
    
    chain output {
        type filter hook output priority filter; policy accept;
    }
}

# NAT таблица для Docker (если нужно)
table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
    }
    
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        
        # Masquerade для Docker сетей
        ip saddr 172.16.0.0/12 oifname != "docker*" masquerade
    }
}
EOF

    # Применяем конфигурацию
    nft -f /etc/nftables.conf
    
    if [ $? -eq 0 ]; then
        log_success "Правила nftables успешно применены"
        
        # Сохраняем конфигурацию для постоянного использования
        if command -v netfilter-persistent >/dev/null 2>&1; then
            netfilter-persistent save
        fi
        
        # Показываем текущие правила
        log_info "Текущие правила nftables:"
        nft list ruleset | head -20
        
        return 0
    else
        log_error "Не удалось применить правила nftables"
        return 1
    fi
}

setup_firewall() {
    log_info "Настройка конфигурации firewall..."
    
    # Шаг 1: Удаляем UFW если присутствует
    detect_and_remove_ufw
    
    # Шаг 2: Устанавливаем nftables если не присутствует
    if ! command -v nft >/dev/null 2>&1; then
        install_nftables
    fi
    
    # Шаг 3: Настраиваем nftables для открытия ТОЛЬКО портов 80 и 443
    if ! check_ports_accessible; then
        log_info "Настройка nftables для открытия портов 80 и 443..."
        remove_other_firewalls
        
        configure_nftables
        
        # Проверяем конфигурацию
        sleep 2
        if check_ports_accessible; then
            log_success "Firewall настроен успешно - порты 80 и 443 открыты"
        else
            log_warning "Конфигурация firewall может потребовать ручной настройки"
        fi
    else
        log_success "Firewall уже правильно настроен - порты 80 и 443 открыты"
    fi
}

# =============================================================================
# РАЗДЕЛ 5: УСТАНОВКА И НАСТРОЙКА DOCKER
# =============================================================================
# Устанавливает Docker, docker-compose и настраивает права пользователей

install_docker() {
    log_info "Установка Docker..."
    
    # Исправляем проблему с рабочей директорией
    cd /tmp
    
    command_exists() {
        command -v "$@" > /dev/null 2>&1
    }

    if command_exists docker; then
        log_info "Docker уже установлен"
    else
        log_info "Docker не найден, устанавливаем..."
        curl -sSL https://get.docker.com | sh
        log_success "Docker успешно установлен"
    fi
    
    # Включаем и запускаем сервис Docker
    log_info "Включение сервиса Docker..."
    systemctl enable docker
    
    if ! systemctl is-active --quiet docker 2>/dev/null; then
        log_info "Запуск Docker daemon..."
        systemctl start docker
        
        # Ждем запуска Docker daemon
        local attempts=0
        local max_attempts=30
        while [ $attempts -lt $max_attempts ]; do
            if systemctl is-active --quiet docker 2>/dev/null; then
                log_success "Docker daemon успешно запущен"
                break
            fi
            log_info "Ожидание запуска Docker daemon... (попытка $((attempts + 1))/$max_attempts)"
            sleep 2
            attempts=$((attempts + 1))
        done
        
        if [ $attempts -eq $max_attempts ]; then
            log_error "Docker daemon не удалось запустить"
            exit 1
        fi
    fi
    
    # Проверяем, что Docker работает
    if ! docker version >/dev/null 2>&1; then
        log_error "Docker работает неправильно"
        exit 1
    fi

    # Устанавливаем docker-compose если не присутствует
    if ! command_exists docker-compose; then
        log_info "Установка docker-compose..."
        local compose_version="v2.29.7"
        local compose_url="https://github.com/docker/compose/releases/download/${compose_version}/docker-compose-$(uname -s)-$(uname -m)"
        
        if curl -L "$compose_url" -o /usr/local/bin/docker-compose; then
            chmod +x /usr/local/bin/docker-compose
            ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
            log_success "Docker-compose успешно установлен"
        else
            log_warning "Не удалось скачать docker-compose, пробуем альтернативный метод..."
            if docker compose version >/dev/null 2>&1; then
                log_info "Используем Docker Compose plugin вместо этого"
                echo '#!/bin/bash' > /usr/local/bin/docker-compose
                echo 'docker compose "$@"' >> /usr/local/bin/docker-compose
                chmod +x /usr/local/bin/docker-compose
                log_success "Создан wrapper для docker-compose"
            else
                log_error "Не удалось установить docker-compose"
                exit 1
            fi
        fi
    fi

    # Добавляем текущего пользователя в группу docker если не root и SUDO_USER установлен
    if [ "$(id -u)" = "0" ] && [ -n "$SUDO_USER" ]; then
        log_info "Добавление пользователя $SUDO_USER в группу docker..."
        usermod -aG docker "$SUDO_USER"
        log_success "Пользователь $SUDO_USER добавлен в группу docker"
        log_info "Примечание: Пользователю может потребоваться выйти и войти снова для применения изменений группы"
    fi

    # Устанавливаем дополнительные инструменты
    log_info "Установка дополнительных необходимых инструментов..."
    if command_exists apt-get; then
        apt-get update -qq
        apt-get install -y -qq jq curl wget
    elif command_exists yum; then
        yum install -y jq curl wget
    elif command_exists dnf; then
        dnf install -y jq curl wget
    else
        log_warning "Менеджер пакетов не найден, предполагаем, что инструменты уже установлены"
    fi

    log_success "Установка Docker завершена"
}

# =============================================================================
# РАЗДЕЛ 6: ОЧИСТКА СУЩЕСТВУЮЩИХ УСТАНОВОК
# =============================================================================
# Удаляет предыдущие установки для чистой установки

cleanup_existing() {
    log_info "Очистка существующих установок..."
    
    # Останавливаем и удаляем NPM контейнеры
    docker stop npm-attachment appsec-agent 2>/dev/null || true
    docker rm npm-attachment appsec-agent 2>/dev/null || true
    
    # Удаляем сервисы Dokploy
    docker service rm dokploy-postgres dokploy-redis dokploy 2>/dev/null || true
    
    # Удаляем Traefik если существует (он нам не нужен)
    docker stop dokploy-traefik 2>/dev/null || true
    docker rm dokploy-traefik 2>/dev/null || true
    
    # Останавливаем OpenAppSec compose если директория существует
    if [ -d "/opt/openappsec-npm" ]; then
        cd /opt/openappsec-npm && docker-compose down 2>/dev/null || true
    fi
    
    # Удаляем директорию OpenAppSec
    rm -rf /opt/openappsec-npm
    
    # Покидаем swarm если в нем
    docker swarm leave --force 2>/dev/null || true
    
    log_success "Очистка завершена"
}

# =============================================================================
# РАЗДЕЛ 7: НАСТРОЙКА DOCKER SWARM
# =============================================================================
# Инициализирует Docker Swarm для Dokploy

setup_docker_swarm() {
    log_info "Настройка Docker Swarm..."
    
    get_ip() {
        local ip=""
        
        # Сначала пробуем IPv4
        ip=$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
        
        if [ -z "$ip" ]; then
            ip=$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
        fi
        
        if [ -z "$ip" ]; then
            ip=$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
        fi

        # Если нет IPv4, пробуем IPv6
        if [ -z "$ip" ]; then
            ip=$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
            
            if [ -z "$ip" ]; then
                ip=$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
            fi
            
            if [ -z "$ip" ]; then
                ip=$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
            fi
        fi

        if [ -z "$ip" ]; then
            log_error "Не удалось автоматически определить IP адрес сервера"
            log_error "Пожалуйста, установите переменную окружения ADVERTISE_ADDR вручную"
            exit 1
        fi

        echo "$ip"
    }

    advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
    log_info "Используем advertise address: $advertise_addr"
    
    docker swarm init --advertise-addr $advertise_addr
    
    if [ $? -ne 0 ]; then
        log_error "Не удалось инициализировать Docker Swarm"
        exit 1
    fi

    log_success "Docker Swarm инициализирован"
}

# =============================================================================
# РАЗДЕЛ 8: СОЗДАНИЕ DOCKER СЕТИ
# =============================================================================
# Создает единую сеть для всех сервисов

create_network() {
    log_info "Создание Docker сети..."
    
    # Удаляем существующую сеть если она есть
    docker network rm -f dokploy-network 2>/dev/null || true
    
    # Создаем overlay сеть
    docker network create --driver overlay --attachable dokploy-network
    
    log_success "Сеть 'dokploy-network' создана"
}

# =============================================================================
# РАЗДЕЛ 9: УСТАНОВКА DOKPLOY СЕРВИСОВ
# =============================================================================
# Устанавливает PostgreSQL, Redis и Dokploy (БЕЗ Traefik)

install_dokploy_services() {
    log_info "Установка сервисов Dokploy (без Traefik)..."
    
    # Создаем директории dokploy
    mkdir -p /etc/dokploy
    chmod 777 /etc/dokploy

    # Сервис PostgreSQL
    log_info "Создание сервиса PostgreSQL..."
    docker service create \
        --name dokploy-postgres \
        --constraint 'node.role==manager' \
        --network dokploy-network \
        --env POSTGRES_USER=dokploy \
        --env POSTGRES_DB=dokploy \
        --env POSTGRES_PASSWORD=amukds4wi9001583845717ad2 \
        --mount type=volume,source=dokploy-postgres-database,target=/var/lib/postgresql/data \
        postgres:16

    # Сервис Redis
    log_info "Создание сервиса Redis..."
    docker service create \
        --name dokploy-redis \
        --constraint 'node.role==manager' \
        --network dokploy-network \
        --mount type=volume,source=redis-data-volume,target=/data \
        redis:7

    # Скачиваем образ Dokploy
    docker pull dokploy/dokploy:${RELEASE_TAG}

    # Основной сервис Dokploy (без Traefik)
    log_info "Создание основного сервиса Dokploy..."
    
    # Устанавливаем переменные окружения для Dokploy
    local dokploy_env=""
    if [ -n "$DATABASE_URL" ]; then
        dokploy_env="$dokploy_env -e DATABASE_URL=$DATABASE_URL"
    fi
    if [ -n "$REDIS_HOST" ]; then
        dokploy_env="$dokploy_env -e REDIS_HOST=$REDIS_HOST"
    fi
    
    docker service create \
        --name dokploy \
        --replicas 1 \
        --network dokploy-network \
        --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
        --mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
        --mount type=volume,source=dokploy-docker-config,target=/root/.docker \
        --publish published=${DOKPLOY_PORT},target=3000,mode=host \
        --update-parallelism 1 \
        --update-order stop-first \
        --constraint 'node.role == manager' \
        -e ADVERTISE_ADDR=$advertise_addr \
        $dokploy_env \
        dokploy/dokploy:${RELEASE_TAG}

    log_success "Сервисы Dokploy созданы (без Traefik)"
}

# =============================================================================
# РАЗДЕЛ 10: СОЗДАНИЕ КОНФИГУРАЦИИ OPENAPPSEC + NPM
# =============================================================================
# Создает docker-compose.yml и конфигурацию для OpenAppSec + NPM

create_openappsec_compose() {
    log_info "Создание конфигурации OpenAppSec + NPM..."
    
    mkdir -p /opt/openappsec-npm
    cd /opt/openappsec-npm

    # Создаем docker-compose.yml (БЕЗ устаревшей строки version)
    cat > docker-compose.yml << 'EOF'
services:
  appsec-npm:
    container_name: npm-attachment
    image: 'ghcr.io/openappsec/nginx-proxy-manager-attachment:latest'
    ipc: host
    restart: unless-stopped
    ports:
      - '80:80'   # Публичный HTTP порт
      - '443:443' # Публичный HTTPS порт
      - '81:81'   # Админский веб-порт
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
      - ./appsec-logs:/ext/appsec-logs
      - ./appsec-localconfig:/ext/appsec
    networks:
      - dokploy-network
      - default

  appsec-agent:
    container_name: appsec-agent
    image: 'ghcr.io/openappsec/agent:latest'
    network_mode: service:appsec-npm
    ipc: host
    restart: unless-stopped
    environment:
      - user_email=${APPSEC_EMAIL}
      - nginxproxymanager=true
      - autoPolicyLoad=true
    volumes:
      - ./appsec-config:/etc/cp/conf
      - ./appsec-data:/etc/cp/data
      - ./appsec-logs:/var/log/nano_agent
      - ./appsec-localconfig:/ext/appsec
    command: /cp-nano-agent --standalone

networks:
  dokploy-network:
    external: true
EOF

    # Создаем .env файл
    cat > .env << EOF
APPSEC_EMAIL=${ADMIN_EMAIL}
APPSEC_LEARNING_MODE=true
APPSEC_ML_ENABLED=true
EOF

    log_success "Конфигурация OpenAppSec + NPM создана"
}

# =============================================================================
# РАЗДЕЛ 11: УСТАНОВКА OPENAPPSEC + NPM
# =============================================================================
# Устанавливает и настраивает OpenAppSec с оптимизированной политикой

install_openappsec() {
    log_info "Установка OpenAppSec + Nginx Proxy Manager..."
    
    cd /opt/openappsec-npm
    
    # Создаем необходимые директории
    mkdir -p data letsencrypt appsec-logs appsec-localconfig appsec-config appsec-data
    
    # Создаем оптимизированную конфигурацию политики OpenAppSec
    log_info "Создание оптимизированной политики OpenAppSec (local_policy.yaml)..."
    cat > appsec-localconfig/local_policy.yaml << 'EOF'
policies:
  default:
    triggers:
      - appsec-smart-log-trigger
    mode: detect
    practices:
      - webapp-smart-practice
    custom-response: appsec-smart-response
  specific-rules:
    # Полное доверие к локальным админским интерфейсам
    - name: localhost-admin-bypass
      host: 
        - "127.0.0.1"
        - "127.0.0.1:81"
        - "127.0.0.1:3000"
        - "localhost"
        - "localhost:81"
        - "localhost:3000"
      uri:
        - "/api/*"
        - "/dashboard/*" 
        - "/_next/*"
        - "/static/*"
        - "/admin/*"
        - "/login"
        - "/auth/*"
        - "/nginx/*"
        - "/openappsec-log/*"
        - "/settings"
        - "/settings/*"
        - "/proxy"
        - "/proxy/*"
      mode: bypass
    
    # Bypass для всех localhost referer'ов (ГЛАВНОЕ ИСПРАВЛЕНИЕ!)
    - name: localhost-referer-bypass
      referer:
        - "http://127.0.0.1:*"
        - "https://127.0.0.1:*"
        - "http://localhost:*"
        - "https://localhost:*"
        - "http://172.*:*"
        - "https://172.*:*"
      mode: bypass
    
    # Docker внутренние сети
    - name: docker-networks-bypass
      source-ip:
        - "172.16.0.0/12"  # Docker networks
        - "192.168.0.0/16" # Private networks
        - "10.0.0.0/8"     # Private networks
      mode: bypass
    
    # Dokploy специфические пути
    - name: dokploy-admin-paths
      host: "*"
      uri:
        - "/api/auth/*"
        - "/api/deploy/*"
        - "/api/docker/*"
        - "/api/projects/*"
        - "/api/services/*"
        - "/_next/static/*"
        - "/favicon.ico"
        - "/manifest.json"
      mode: bypass
    
    # NPM админка
    - name: npm-admin-paths
      host: "*"
      uri:
        - "/api/nginx/*"
        - "/api/users/*"
        - "/api/settings/*"
        - "/api/certificates/*"
        - "/assets/*"
        - "/js/*"
        - "/css/*"
      mode: bypass

practices:
  - name: webapp-smart-practice
    # Веб-атаки с умными настройками для localhost
    web-attacks:
      max-body-size-kb: 2000000  # Увеличено для админок
      max-header-size-bytes: 204800  # Увеличено для сложных запросов
      max-object-depth: 50
      max-url-size-bytes: 65536
      minimum-confidence: medium  # Средний уровень для баланса
      override-mode: detect
      protections:
        csrf-protection: detect
        error-disclosure: detect
        non-valid-http-methods: false  # Отключено для REST API
        open-redirect: detect
    
    # Продвинутая анти-бот защита с исключениями для localhost
    anti-bot:
      injected-URIs: []
      validated-URIs: 
        - "/api/auth/login"
        - "/api/users/profile"
        - "/nginx/proxy"
        - "/settings"
      override-mode: detect
    
    # Snort сигнатуры с пониженной чувствительностью для localhost
    snort-signatures:
      configmap:
        - sql_injection
        - cross_site_scripting
        # НЕ включаем remote_code_execution для localhost!
        - local_file_inclusion
        - path_traversal
      override-mode: detect
    
    # Валидация OpenAPI
    openapi-schema-validation:
      configmap: []
      override-mode: detect

# Умное логирование с фильтрацией localhost событий
log-triggers:
  - name: appsec-smart-log-trigger
    access-control-logging:
      allow-events: true
      drop-events: true
    additional-suspicious-events-logging:
      enabled: true
      minimum-severity: medium  # Средний уровень для уменьшения шума
      response-body: false
      response-code: true
    appsec-logging:
      all-web-requests: false  # Отключено для производительности
      detect-events: true
      prevent-events: true
    extended-logging:
      http-headers: false  # Отключено для приватности
      request-body: false  # Отключено для производительности
      url-path: true
      url-query: false  # Отключено для админок
    log-destination:
      cloud: false
      stdout:
        format: json

# Умные ответы
custom-responses:
  - name: appsec-smart-response
    mode: response-code-only
    http-response-code: 403
EOF

    log_success "Политика OpenAppSec настроена для игнорирования ложных срабатываний localhost"
    
    # Запускаем сервисы
    log_info "Запуск контейнеров OpenAppSec + NPM..."
    docker-compose up -d
    
    log_success "OpenAppSec + NPM установлены на портах 80/443"
}

# =============================================================================
# РАЗДЕЛ 12: ОЖИДАНИЕ ЗАПУСКА СЕРВИСОВ
# =============================================================================
# Ждет, пока все сервисы полностью запустятся

wait_for_services() {
    log_info "Ожидание запуска сервисов..."
    
    # Ждем готовности NPM
    local npm_ready=false
    local attempts=0
    local max_attempts=30
    
    while [ "$npm_ready" = false ] && [ $attempts -lt $max_attempts ]; do
        if curl -s http://localhost:81 > /dev/null 2>&1; then
            npm_ready=true
            log_success "NPM готов"
        else
            log_info "Ожидание запуска NPM... (попытка $((attempts + 1))/$max_attempts)"
            sleep 10
            attempts=$((attempts + 1))
        fi
    done
    
    if [ "$npm_ready" = false ]; then
        log_error "NPM не удалось запустить в ожидаемое время"
        exit 1
    fi
    
    # Ждем готовности Dokploy
    local dokploy_ready=false
    attempts=0
    
    while [ "$dokploy_ready" = false ] && [ $attempts -lt $max_attempts ]; do
        if curl -s http://localhost:${DOKPLOY_PORT} > /dev/null 2>&1; then
            dokploy_ready=true
            log_success "Dokploy готов"
        else
            log_info "Ожидание запуска Dokploy... (попытка $((attempts + 1))/$max_attempts)"
            sleep 10
            attempts=$((attempts + 1))
        fi
    done
    
    if [ "$dokploy_ready" = false ]; then
        log_error "Dokploy не удалось запустить в ожидаемое время"
        exit 1
    fi
}

# =============================================================================
# РАЗДЕЛ 13: АВТОМАТИЧЕСКАЯ НАСТРОЙКА NPM
# =============================================================================
# Автоматически настраивает NPM и создает proxy host для Dokploy

configure_npm_automatically() {
    log_info "Автоматическая настройка Nginx Proxy Manager..."
    
    # Ждем еще немного для полной готовности сервисов
    sleep 15
    
    # Получаем токен авторизации
    log_info "Получение токена авторизации NPM..."
    local auth_response=$(curl -s -X POST http://localhost:81/api/tokens \
        -H "Content-Type: application/json" \
        -d '{
            "identity": "[email protected]",
            "secret": "changeme"
        }')
    
    local token=$(echo "$auth_response" | jq -r '.token // empty')
    
    if [ -z "$token" ] || [ "$token" = "null" ]; then
        log_warning "Не удалось получить токен авторизации NPM автоматически. Вам потребуется настроить NPM вручную."
        return 1
    fi
    
    log_success "Получен токен авторизации NPM"
    
    # Изменяем email и пароль администратора
    log_info "Обновление учетных данных администратора NPM..."
    local user_update=$(curl -s -X PUT http://localhost:81/api/users/1 \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer $token" \
        -d "{
            \"email\": \"${ADMIN_EMAIL}\",
            \"nickname\": \"Administrator\",
            \"is_disabled\": false,
            \"roles\": [\"admin\"]
        }")
    
    # Устанавливаем новый пароль
    local password_set=$(curl -s -X PUT http://localhost:81/api/users/1/auth \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer $token" \
        -d "{
            \"type\": \"password\",
            \"current\": \"changeme\",
            \"secret\": \"${NPM_ADMIN_PASSWORD}\"
        }")
    
    log_success "Учетные данные администратора NPM обновлены"
    
    # Создаем proxy host для Dokploy - пробуем несколько подходов
    log_info "Создание proxy host для Dokploy..."
    
    # Метод 1: Пробуем получить реальный IP контейнера сервиса
    local dokploy_container_id=$(docker ps --filter "ancestor=dokploy/dokploy:latest" --format "{{.ID}}" | head -1)
    local dokploy_ip=""
    
    if [ -n "$dokploy_container_id" ]; then
        local network_id=$(docker network inspect dokploy-network --format '{{.Id}}' 2>/dev/null)
        if [ -n "$network_id" ]; then
            dokploy_ip=$(docker inspect $dokploy_container_id --format "{{range .NetworkSettings.Networks}}{{if eq .NetworkID \"$network_id\"}}{{.IPAddress}}{{end}}{{end}}" 2>/dev/null)
            if [ -n "$dokploy_ip" ] && [ "$dokploy_ip" != "<no value>" ] && [ "$dokploy_ip" != ".IPAddress" ]; then
                log_info "Найден IP контейнера Dokploy: $dokploy_ip"
            else
                dokploy_ip=""
            fi
        fi
    fi
    
    # Метод 2: Пробуем инспекцию сервиса
    if [ -z "$dokploy_ip" ]; then
        local network_id=$(docker network inspect dokploy-network --format '{{.Id}}' 2>/dev/null)
        if [ -n "$network_id" ]; then
            dokploy_ip=$(docker service inspect dokploy --format "{{range .Endpoint.VirtualIPs}}{{if eq .NetworkID \"$network_id\"}}{{.Addr}}{{end}}{{end}}" 2>/dev/null | cut -d'/' -f1)
        fi
        if [ -n "$dokploy_ip" ] && [ "$dokploy_ip" != "<no value>" ] && [ "$dokploy_ip" != ".IPAddress" ]; then
            log_info "Найден IP сервиса Dokploy: $dokploy_ip"
        else
            dokploy_ip=""
        fi
    fi
    
    # Метод 3: Используем имя сервиса как резерв
    if [ -z "$dokploy_ip" ]; then
        dokploy_ip="dokploy"
        log_info "Используем имя сервиса 'dokploy' как резерв"
    fi
    
    local dokploy_port=3000
    
    # Тестируем соединение перед созданием proxy host
    log_info "Тестирование соединения с Dokploy на $dokploy_ip:$dokploy_port..."
    local test_result="200"
    
    # Пробуем тестировать соединение изнутри сети
    if docker run --rm --network dokploy-network alpine/curl:latest -s --connect-timeout 5 http://$dokploy_ip:$dokploy_port >/dev/null 2>&1; then
        test_result="200"
        log_success "Успешно подключились к Dokploy на $dokploy_ip:$dokploy_port"
    else
        test_result="000"
        log_warning "Не удается достичь Dokploy на $dokploy_ip:$dokploy_port, но попробуем создать proxy host в любом случае"
    fi
    
    # Ждем еще немного для полной готовности NPM
    sleep 5
    
    # Создаем proxy host с улучшенной обработкой ошибок
    log_info "Создание proxy host с настройками: Domain=dokploy.local, Target=$dokploy_ip:$dokploy_port"
    local proxy_host_response=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST http://localhost:81/api/nginx/proxy-hosts \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer $token" \
        -d "{
            \"domain_names\": [\"dokploy.local\"],
            \"forward_scheme\": \"http\",
            \"forward_host\": \"$dokploy_ip\",
            \"forward_port\": $dokploy_port,
            \"access_list_id\": 0,
            \"certificate_id\": 0,
            \"ssl_forced\": false,
            \"caching_enabled\": false,
            \"block_exploits\": true,
            \"advanced_config\": \"proxy_set_header Host \\$host;\\nproxy_set_header X-Real-IP \\$remote_addr;\\nproxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;\\nproxy_set_header X-Forwarded-Proto \\$scheme;\\nproxy_read_timeout 300;\\nproxy_connect_timeout 300;\\nproxy_send_timeout 300;\",
            \"meta\": {
                \"letsencrypt_agree\": false,
                \"dns_challenge\": false
            },
            \"allow_websocket_upgrade\": true,
            \"http2_support\": true,
            \"forward_host_header\": true,
            \"hsts_enabled\": false,
            \"hsts_subdomains\": false
        }")
    
    # Извлекаем HTTP статус и тело ответа
    local http_status=$(echo "$proxy_host_response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
    local proxy_host_body=$(echo "$proxy_host_response" | sed 's/HTTPSTATUS:[0-9]*$//')
    
    log_info "Статус ответа NPM API: $http_status"
    
    if [ "$http_status" = "201" ] || [ "$http_status" = "200" ]; then
        if echo "$proxy_host_body" | jq -e '.id' > /dev/null 2>&1; then
            local proxy_id=$(echo "$proxy_host_body" | jq -r '.id')
            log_success "Proxy host успешно создан для Dokploy (ID: $proxy_id, Target: $dokploy_ip:$dokploy_port)"
        else
            log_success "Proxy host создан для Dokploy (Target: $dokploy_ip:$dokploy_port)"
        fi
    else
        log_warning "Не удалось создать proxy host автоматически (HTTP $http_status)"
        log_info "Ответ: $proxy_host_body"
        log_info "Вы можете создать его вручную в NPM с этими настройками:"
        log_info "  Domain: dokploy.local"
        log_info "  Forward Host: $dokploy_ip"
        log_info "  Forward Port: $dokploy_port"
        log_info "  Scheme: http"
        log_info "  Advanced config: Добавьте proxy таймауты для лучшей стабильности"
        
        # Пробуем альтернативный подход - создаем без advanced config
        log_info "Пробуем создать proxy host без advanced config..."
        local simple_proxy_response=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST http://localhost:81/api/nginx/proxy-hosts \
            -H "Content-Type: application/json" \
            -H "Authorization: Bearer $token" \
            -d "{
                \"domain_names\": [\"dokploy.local\"],
                \"forward_scheme\": \"http\",
                \"forward_host\": \"$dokploy_ip\",
                \"forward_port\": $dokploy_port,
                \"access_list_id\": 0,
                \"certificate_id\": 0,
                \"ssl_forced\": false,
                \"caching_enabled\": false,
                \"block_exploits\": false,
                \"meta\": {
                    \"letsencrypt_agree\": false,
                    \"dns_challenge\": false
                },
                \"allow_websocket_upgrade\": true,
                \"http2_support\": false,
                \"forward_host_header\": true,
                \"hsts_enabled\": false,
                \"hsts_subdomains\": false
            }")
        
        local simple_http_status=$(echo "$simple_proxy_response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
        local simple_proxy_body=$(echo "$simple_proxy_response" | sed 's/HTTPSTATUS:[0-9]*$//')
        
        if [ "$simple_http_status" = "201" ] || [ "$simple_http_status" = "200" ]; then
            if echo "$simple_proxy_body" | jq -e '.id' > /dev/null 2>&1; then
                local simple_proxy_id=$(echo "$simple_proxy_body" | jq -r '.id')
                log_success "Простой proxy host успешно создан для Dokploy (ID: $simple_proxy_id, Target: $dokploy_ip:$dokploy_port)"
            else
                log_success "Простой proxy host создан для Dokploy (Target: $dokploy_ip:$dokploy_port)"
            fi
        else
            log_warning "Обе попытки создания proxy host не удались"
            log_info "Простой ответ: $simple_proxy_body"
        fi
    fi
}

# =============================================================================
# РАЗДЕЛ 14: ИСПРАВЛЕНИЕ ПРАВ DOCKER
# =============================================================================
# Исправляет права Docker для текущего пользователя

fix_docker_permissions() {
    log_info "Исправление прав Docker для текущего пользователя..."
    
    # Добавляем текущего пользователя в группу docker если еще не добавлен
    if [ -n "$SUDO_USER" ]; then
        usermod -aG docker "$SUDO_USER" 2>/dev/null || true
        log_success "Пользователь $SUDO_USER добавлен в группу docker"
        
        # Пробуем исправить права сокета
        chmod 666 /var/run/docker.sock 2>/dev/null || true
        
        log_info "Для использования Docker без sudo, выполните: newgrp docker"
        log_info "Или выйдите и войдите снова"
    else
        log_warning "Не удалось определить текущего пользователя для прав Docker"
    fi
}

# =============================================================================
# РАЗДЕЛ 15: ТЕСТИРОВАНИЕ NPM PROXY
# =============================================================================
# Тестирует работу NPM proxy

test_npm_proxy() {
    log_info "Тестирование конфигурации NPM proxy..."
    
    # Ждем активации proxy
    sleep 10
    
    # Тестируем, работает ли proxy, проверяя, разрешается ли dokploy.local через NPM
    local test_result=$(curl -s -H "Host: dokploy.local" http://localhost:80 2>/dev/null | head -c 100)
    
    if echo "$test_result" | grep -q "DOCTYPE\|html\|Dokploy" 2>/dev/null; then
        log_success "NPM proxy работает! Вы можете получить доступ к Dokploy через dokploy.local"
    else
        log_info "Тест NPM proxy неубедителен. Рекомендуется ручная проверка."
    fi
}

# =============================================================================
# РАЗДЕЛ 16: ПОЛУЧЕНИЕ ПУБЛИЧНОГО IP
# =============================================================================
# Получает публичный IP адрес сервера

get_public_ip() {
    local ip=""
    
    # Сначала пробуем IPv4
    ip=$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
    
    if [ -z "$ip" ]; then
        ip=$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
    fi
    
    if [ -z "$ip" ]; then
        ip=$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
    fi

    if [ -z "$ip" ]; then
        echo "localhost"
    else
        echo "$ip"
    fi
}

format_ip_for_url() {
    local ip="$1"
    if echo "$ip" | grep -q ':'; then
        # IPv6
        echo "[${ip}]"
    else
        # IPv4
        echo "${ip}"
    fi
}

# =============================================================================
# РАЗДЕЛ 17: СООБЩЕНИЕ О ЗАВЕРШЕНИИ
# =============================================================================
# Показывает итоговое сообщение с информацией о доступе

show_completion_message() {
    local public_ip=$(get_public_ip)
    local formatted_addr=$(format_ip_for_url "$public_ip")
    
    echo ""
    echo "===================================================================================="
    printf "${GREEN}🎉 Установка Dokploy + Nginx Proxy Manager завершена!${NC}\n"
    echo "===================================================================================="
    echo ""
    printf "${BLUE}Развернутая архитектура:${NC}\n"
    printf "  Internet → Nginx Proxy Manager + OpenAppSec → Dokploy Apps (БЕЗ Traefik)\n"
    echo ""
    printf "${BLUE}Установленные сервисы:${NC}\n"
    printf "  ✅ Dokploy (с PostgreSQL, Redis) - БЕЗ Traefik\n"
    printf "  ✅ OpenAppSec + Nginx Proxy Manager\n"
    printf "  ✅ Единая Docker сеть: dokploy-network\n"
    echo ""
    printf "${BLUE}URL для доступа:${NC}\n"
    printf "  🌐 Dokploy Dashboard: ${YELLOW}http://${formatted_addr}:${DOKPLOY_PORT}${NC}\n"
    printf "  🔒 Nginx Proxy Manager: ${YELLOW}http://${formatted_addr}:81${NC}\n"
    echo ""
    printf "${BLUE}Поток трафика:${NC}\n"
    printf "  • Порт 80/443: Публичный трафик → NPM + OpenAppSec (слой безопасности)\n"
    printf "  • NPM перенаправляет напрямую к: сервису Dokploy (порт 3000)\n"
    printf "  • Ваши приложения, развернутые в Dokploy, будут доступны через NPM\n"
    echo ""
    printf "${BLUE}Учетные данные NPM:${NC}\n"
    printf "  Email: ${YELLOW}${ADMIN_EMAIL}${NC}\n"
    printf "  Пароль: ${YELLOW}${NPM_ADMIN_PASSWORD}${NC}\n"
    echo ""
    printf "${BLUE}Конфигурация:${NC}\n"
    printf "  • Порт Dokploy: ${DOKPLOY_PORT}\n"
    printf "  • Тег релиза: ${RELEASE_TAG}\n"
    printf "  • Proxy host настроен для: dokploy.local\n"
    printf "  • Firewall: nftables настроен (порты 80, 443 открыты)\n"
    printf "  • Права пользователя Docker: настроены для $SUDO_USER\n"
    echo ""
    printf "${YELLOW}⚠️  Важные примечания:${NC}\n"
    printf "  • Traefik НЕ установлен - NPM обрабатывает всю маршрутизацию\n"
    printf "  • Настройте домены ваших приложений через интерфейс NPM\n"
    printf "  • Направьте ваши домены на этот IP сервера: ${formatted_addr}\n"
    printf "  • Для локального тестирования добавьте в /etc/hosts: ${formatted_addr} dokploy.local\n"
    printf "  • Выполните 'newgrp docker' или выйдите/войдите для использования Docker без sudo\n"
    echo ""
    printf "${GREEN}Подождите 15 секунд для полного запуска всех сервисов!${NC}\n"
    echo "===================================================================================="
}

# =============================================================================
# РАЗДЕЛ 18: ФУНКЦИЯ ОБНОВЛЕНИЯ
# =============================================================================
# Обновляет Dokploy и NPM до последних версий

update_dokploy() {
    log_info "Обновление Dokploy..."
    
    # Скачиваем последний образ
    docker pull dokploy/dokploy:${RELEASE_TAG}
    
    # Обновляем сервис
    docker service update --image dokploy/dokploy:${RELEASE_TAG} dokploy
    
    # Обновляем OpenAppSec + NPM
    if [ -d "/opt/openappsec-npm" ]; then
        cd /opt/openappsec-npm
        docker-compose pull
        docker-compose up -d
    fi
    
    log_success "Dokploy и NPM успешно обновлены"
}

# =============================================================================
# РАЗДЕЛ 19: ОСНОВНАЯ ФУНКЦИЯ
# =============================================================================
# Главная функция, которая выполняет всю установку

main() {
    log_info "Запуск установки Dokploy + Nginx Proxy Manager..."
    log_info "Архитектура: Internet → NPM + OpenAppSec → Dokploy Apps (БЕЗ Traefik)"
    
    cleanup_existing
    check_requirements
    setup_firewall
    install_docker
    setup_docker_swarm
    create_network
    install_dokploy_services
    create_openappsec_compose
    install_openappsec
    wait_for_services
    configure_npm_automatically
    fix_docker_permissions
    test_npm_proxy
    
    show_completion_message
}

# =============================================================================
# РАЗДЕЛ 20: ОБРАБОТКА АРГУМЕНТОВ СКРИПТА
# =============================================================================
# Обрабатывает аргументы командной строки

case "${1:-}" in
    "update")
        update_dokploy
        ;;
    "clean")
        log_info "Выполнение полной очистки..."
        cleanup_existing
        docker system prune -af --volumes
        log_success "Полная очистка завершена"
        ;;
    *)
        main
        ;;
esac

Last updated

Was this helpful?