Skip to content

一键部署脚本(hy2、vless、anytls)

环境变量文件 .env

shell
CF_TOKEN="onXxS9ffo7N3OP0E2wKdJPKOus-T4bp5yZNDY3gV"
CF_ZONE_ID="690e7331d42c074ac3fb5ed586e003b0"
DOMAIN=ilym.top
XRAY_PREFIX=flare
ANYTLS_PREFIX=anytest
XRAY_KEY="7898568a-d1bc-4fb5-ad14-515e049ac4b6"
XRAY_PRIVATE_KEY="uHcaFeAxt93EriyUbHY3hpEq-tTQ0g7NmdBSRFmv0lM"
XRAY_XHTTP_PATH="/wHR7itXI"
ANYTLS_NAME="ilymee"
ANYTLS_PASSWORD="6309e3b1-b3cb-43f4-9dbf-39cdb1a0a276"
HY2_USER="youarenotmydalinganymor407de"
HY2_PASSWORD="78cf7dfa-_ebf@c5f2-407d-@efbd=e6d-b94edfe649c08b9f=wee?dd65_79e5d4"
HY2_MASQUERADE="https://demo.cloudreve.org"

docker-compose.yml

shell
services:
  xray:
    image: ghcr.io/xtls/xray-core:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    container_name: nginx-xray
    restart: always
    user: "0:0"
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /home/xray/log:/tmp/xray
      - /home/nginx/cert:/etc/nginx/cert
    configs:
      - source: xray_config
        target: /home/xray/xconfig.json
    network_mode: host
    command: run -c /home/xray/xconfig.json

  sing-box:
    image: ghcr.io/sagernet/sing-box
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    container_name: nginx-sing-box
    restart: always
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /home/sing-box/log:/etc/sing-box/log
      - /home/nginx/cert:/etc/nginx/cert
    configs:
      - source: singbox_config
        target: /etc/sing-box/config.json
    network_mode: host
    command: -D /var/lib/sing-box -C /etc/sing-box/ run
    
  nginx:
    image: nginx:1.27.1
    container_name: nginx
    restart: always
    network_mode: host
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /home/nginx/log:/var/log/nginx
      - /home/nginx/html:/usr/share/nginx/html
      - /home/nginx/cert:/etc/nginx/cert

    configs:
      - source: nginx_conf
        target: /etc/nginx/nginx.conf
      - source: nginx_default_conf
        target: /etc/nginx/conf.d/default.conf
      
  hysteria:
    image: tobyxdd/hysteria:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    container_name: nginx-hysteria
    restart: always
    environment:
      - TZ=Asia/Shanghai
      - HYSTERIA_LOG_LEVEL=warn
    volumes:
      - /home/hysteria/log:/home/hysteria/log
      - /home/nginx/cert:/etc/nginx/cert
    configs:
      - source: hysteria_yaml
        target: /etc/hysteria.yaml
      - source: rules_txt
        target: /home/hysteria/acl/rules.txt
    entrypoint: ["/bin/sh", "-c"]
    command: ["hysteria server -c /etc/hysteria.yaml 2>&1 | tee -a /home/hysteria/log/hysteria.log"]
    network_mode: host

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_POLL_INTERVAL=86400
      - DOCKER_API_VERSION=1.44

  acme-worker:
    image: neilpang/acme.sh:latest
    container_name: acme-worker
    restart: always
    environment:
      - CF_Token=${CF_TOKEN}
      - CF_Zone_ID=${CF_ZONE_ID}
      - DOMAIN=${DOMAIN}
    volumes:
      - /home/nginx/cert:/home/nginx/cert
      - /var/run/docker.sock:/var/run/docker.sock 
      - acme-data:/acme.sh 
    configs:
      - source: acme_init_script
        target: /run_acme.sh
    command: sh /run_acme.sh

volumes:
  acme-data:

configs:
  xray_config:
    content: |
      {
        "log": {
            "access": "/tmp/xray/access.log",
            "error": "/tmp/xray/error.log",
            "loglevel": "warn"
        },
        "routing": {
            "domainStrategy": "IPIfNonMatch",
            "rules": [
                {
                    "type": "field",
                    "protocol": [
                        "bittorrent"
                    ],
                    "outboundTag": "block"
                },
                {
                    "type": "field",
                    "ip": [
                        "geoip:private"
                    ],
                    "outboundTag": "block"
                },
                {
                    "type": "field",
                    "ip": [
                        "geoip:cn"
                    ],
                    "outboundTag": "block"
                },
                {
                    "type": "field",
                    "domain": [
                        "geosite:category-ads-all"
                    ],
                    "outboundTag": "block"
                }
            ]
        },
        "inbounds": [
            {
                "listen": "0.0.0.0",
                "port": 1443,
                "protocol": "vless",
                "settings": {
                    "clients": [
                        {
                            "id": "${XRAY_KEY}",
                            "flow": "xtls-rprx-vision"
                        }
                    ],
                    "decryption": "none",
                    "fallbacks": [
                        {
                            "dest": 1880
                        }
                    ]
                },
                "streamSettings": {
                    "network": "raw",
                    "security": "tls",
                    "tlsSettings": {
                        "rejectUnknownSni": true,
                        "minVersion": "1.2",
                        "certificates": [
                            {
                                "ocspStapling": 3600,
                                "certificateFile": "/etc/nginx/cert/cert.pem",
                                "keyFile": "/etc/nginx/cert/key.pem"
                            }
                        ]
                    },
                    "rawSettings": {
                        "acceptProxyProtocol": true
                    }
                },
                "sniffing": {
                    "enabled": true,
                    "destOverride": [
                        "http",
                        "tls",
                        "quic"
                    ]
                }
            },
            {
                "listen": "0.0.0.0",
                "port": 3443,
                "protocol": "vless",
                "settings": {
                    "clients": [
                        {
                            "id": "${XRAY_KEY}",
                            "flow": "xtls-rprx-vision"
                        }
                    ],
                    "decryption": "none",
                    "fallbacks": [
                        {
                            "dest": 1880
                        }
                    ]
                },
                "streamSettings": {
                    "network": "raw",
                    "security": "reality",
                    "realitySettings": {
                        "target": 8443,
                        "xver": 1,
                        "show": true,
                        "serverNames": [
                            "flare.${DOMAIN}"
                        ],
                        "privateKey": "${XRAY_PRIVATE_KEY}",
                        "shortIds": [
                            ""
                        ]
                    },
                    "rawSettings": {
                        "acceptProxyProtocol": true
                    }
                },
                "sniffing": {
                    "enabled": true,
                    "destOverride": [
                        "http",
                        "tls",
                        "quic"
                    ]
                }
            },
            {
                "listen": "0.0.0.0",
                "port": 2024,
                "protocol": "vless",
                "settings": {
                    "clients": [
                        {
                            "id": "${XRAY_KEY}"
                        }
                    ],
                    "decryption": "none"
                },
                "streamSettings": {
                    "network": "xhttp",
                    "xhttpSettings": {
                        "path": "${XRAY_XHTTP_PATH}"
                    }
                },
                "sniffing": {
                    "enabled": true,
                    "destOverride": [
                        "http",
                        "tls",
                        "quic"
                    ]
                }
            }
        ],
        "outbounds": [
            {
                "protocol": "freedom",
                "tag": "direct"
            },
            {
                "protocol": "blackhole",
                "tag": "block"
            }
        ]
      }

  singbox_config:
    content: |
      {
        "log": {
            "disabled": false,
            "level": "warn",
            "output": "/etc/sing-box/log/sing-box.log",
            "timestamp": true
        },
        "inbounds": [
            {
                "type": "anytls",
                "tag": "anytls-in",
                "listen": "::",
                "listen_port": 4443,
                "users": [
                    {
                        "name": "${ANYTLS_NAME}",
                        "password": "${ANYTLS_PASSWORD}"
                    }
                ],
                "tls": {
                    "enabled": true,
                    "server_name": "${ANYTLS_PREFIX}.${DOMAIN}",
                    "certificate_path": "/etc/nginx/cert/cert.pem",
                    "key_path": "/etc/nginx/cert/key.pem"
                }
            }
        ],
        "route": {
            "rules": [
                {
                    "action": "sniff"
                },
                {
                    "ip_is_private": true,
                    "outbound": "block"
                }
            ],
            "final": "direct"
        },
        "outbounds": [
            {
                "type": "direct",
                "tag": "direct"
            },
            {
                "type": "block",
                "tag": "block"
            }
        ]
      }

  nginx_conf:
    content: |
      user  nginx;
      worker_processes  auto;
      error_log  /var/log/nginx/error.log warn;
      pid        /var/run/nginx.pid;
       
      events {
          worker_connections  1024;
          use epoll;
          multi_accept on;
      }
       
      http {
          server_tokens off;
          include       /etc/nginx/mime.types;
          default_type  application/octet-stream;
           
          # 注意这里的 Nginx 变量全部改成了 $$
          map $$http_x_forwarded_for $$clientRealIp {
              "" $$remote_addr;
              "~*(?P<firstAddr>([0-9a-f]{0,4}:){1,7}[0-9a-f]{1,4}|([0-9]{1,3}\.){3}[0-9]{1,3})$$" $$firstAddr;
          }
       
          log_format  main  '$$clientRealIp $$remote_addr $$remote_user [$$time_local] "$$request" '
                          '$$status $$body_bytes_sent "$$http_referer" '
                          '"$$http_user_agent" $$http_x_forwarded_for '
                          '"$$upstream_addr" "$$upstream_status" "$$upstream_response_time" "$$request_time" ';
       
          access_log  /var/log/nginx/access.log  main;
       
          sendfile           on;
          keepalive_timeout  65;
           
          gzip       on;
          gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
       
          include /etc/nginx/conf.d/*.conf;
      }
       
      stream {
          error_log /var/log/nginx/stream_error.log warn;
          # Nginx 变量用 $$
          map $$ssl_preread_server_name $$name {
              # 自定义环境变量保持单 $
              ${XRAY_PREFIX}.${DOMAIN} xray_backend;
              ${ANYTLS_PREFIX}.${DOMAIN} sing_box_wrapper;
              default web_backend;
          }
           
          upstream sing_box_wrapper {
              server 127.0.0.1:14443; 
          }
           
          server {
              listen 127.0.0.1:14443 proxy_protocol; 
              proxy_pass 127.0.0.1:4443;
          }
       
          upstream web_backend {
              server 127.0.0.1:8443;
          }
           
          upstream hy2_backend {
              server 127.0.0.1:2443;
          }
           
          upstream hy2_backend_80 {
              server 127.0.0.1:446;
          }
       
          upstream xray_backend {
              server 127.0.0.1:1443;
          }
       
          server {
              listen 443;
              listen [::]:443;
               
              ssl_preread on;
              error_log /var/log/nginx/stream_443.log warn;
              # Nginx 变量用 $$
              proxy_pass $$name;
              proxy_protocol on;
          }
           
          server {
              listen 443 udp reuseport;
              listen [::]:443 udp reuseport;
       
              proxy_pass    hy2_backend;
              proxy_timeout 20s;
          }
      }

  nginx_default_conf:
    content: |
      server {
        error_log /var/log/nginx/error_80.log warn;
        listen 80;
        listen [::]:80;
        # Nginx 变量用 $$
        return 301 https://$$host$$request_uri;
      }
      
      server {
          listen 127.0.0.1:8443 quic reuseport;
          listen 127.0.0.1:8443 ssl proxy_protocol reuseport;
      
          http2 on;
      
          set_real_ip_from 127.0.0.1;
          real_ip_header   proxy_protocol;
      
          ssl_certificate       /etc/nginx/cert/cert.pem;
          ssl_certificate_key   /etc/nginx/cert/key.pem;
      
          ssl_protocols             TLSv1.2 TLSv1.3;
          ssl_prefer_server_ciphers on;
          ssl_ciphers               ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305;
          ssl_ecdh_curve            secp521r1:secp384r1:secp256r1:x25519;
      
          # 自定义环境变量保持单 $
          location ${XRAY_XHTTP_PATH} {
              grpc_pass grpc://127.0.0.1:2024;
              # Nginx 变量用 $$
              grpc_set_header Host $$host;
              grpc_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
          }
      
          location = /favicon.ico {
              access_log off;
              log_not_found off;
              return 204;
          }
          
          location / {
              add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
              add_header Alt-Svc 'h3=":443"; ma=86400';
      
              # Nginx 变量用 $$
              proxy_set_header   X-Real-IP $$remote_addr;
              proxy_set_header   X-Forwarded-For $$proxy_add_x_forwarded_for;
              proxy_set_header   Host $$host;
              proxy_pass         http://127.0.0.1:1880;
              proxy_http_version 1.1;
              proxy_set_header   Upgrade $$http_upgrade;
              proxy_set_header   Connection "upgrade";
          }
      }

  hysteria_yaml:
    content: |
      listen: :2443 
      tls:
        cert: /etc/nginx/cert/cert.pem
        key: /etc/nginx/cert/key.pem
      auth:
        type: userpass
        userpass: 
          ${HY2_USER}: ${HY2_PASSWORD}
      resolver:
        type: tcp
        tcp:
          addr: 8.8.8.8:53 
          timeout: 4s 
        udp:
          addr: 1.1.1.1:53 
          timeout: 4s
        tls:
          addr: 1.1.1.1:853 
          timeout: 10s
          sni: cloudflare-dns.com 
          insecure: false 
        https:
          addr: 1.1.1.1:443 
          timeout: 10s
          sni: cloudflare-dns.com
          insecure: false
      masquerade:
        type: proxy
        proxy:
          url: ${HY2_MASQUERADE}
          rewriteHost: true
        listenHTTP: :1880
        listenHTTPS: :5443
      acl:
        file: /home/hysteria/acl/rules.txt  
      outbounds:
        - name: warp
          type: socks5
          socks5:
            addr: 127.0.0.1:40000
        - name: drt
          type: direct
      sniff:
        enable: true 
        timeout: 2s 
        rewriteDomain: false 
        tcpPorts: 443,19999-29999
        udpPorts: all 
      speedTest: true 
  
  rules_txt:
    content: |
      # 直连所有其他地址
      drt(all)

  acme_init_script:
    content: |
      #!/bin/sh
      apk add --no-cache curl openssl

      # 注意:在 docker-compose.yml 中的 Shell 变量和命令替换,必须使用 $$ 转义
      IS_TEMP=$$(openssl x509 -in /home/nginx/cert/cert.pem -noout -issuer 2>/dev/null | grep "temp.cert")

      if [ -n "$$IS_TEMP" ] || [ ! -f "/home/nginx/cert/cert.pem" ]; then
        echo "检测到临时证书或无证书,开始通过 Cloudflare DNS 申请真实证书..."
        
        # 这里的 ${ANYTLS_PREFIX} 和 ${DOMAIN} 保留单 $,因为它们确实是需要 Docker Compose 注入的环境变量
        acme.sh --issue --dns dns_cf -d ${DOMAIN} -d *.${DOMAIN} --server letsencrypt
        
        echo "安装真实证书,并重启所有依赖证书的服务..."
        acme.sh --install-cert -d ${DOMAIN} \
          --key-file /home/nginx/cert/key.pem \
          --fullchain-file /home/nginx/cert/cert.pem \
          --reloadcmd "curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/nginx/restart && \
                       curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/nginx-xray/restart && \
                       curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/nginx-sing-box/restart && \
                       curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/nginx-hysteria/restart"
      else
        echo "真实证书已存在,跳过初始申请流程。"
      fi

      exec crond -f

一键运行脚本

shell
#!/bin/bash

# 开启严格模式:遇到错误立即退出
set -e

# 1. 检查是否为 root 用户
if [ "$EUID" -ne 0 ]; then
  echo "❌ 权限不足:请使用 root 用户或加上 sudo 执行此脚本。"
  exit 1
fi

echo "======================================================="
echo "       Docker 环境一键安装 & 代理/Web 服务部署脚本       "
echo "======================================================="

# 2. 检查并安装 Docker
if ! command -v docker &> /dev/null; then
    echo "未检测到 Docker,开始自动安装..."
    # 使用官方安装脚本,并指定阿里云镜像源加速安装
    curl -fsSL https://get.docker.com | bash -s docker #--mirror Aliyun
    
    echo "启动 Docker 服务并设置开机自启..."
    systemctl enable docker
    systemctl start docker
    echo "✅ Docker 安装成功!"
else
    echo "✅ 经检测,Docker 已安装,跳过该步骤。"
fi

# 3. 检查 Docker Compose (现在的 Docker 官方推荐使用 docker compose 插件,安装脚本已默认包含)
DOCKER_COMPOSE_CMD=""
if docker compose version &> /dev/null; then
    DOCKER_COMPOSE_CMD="docker compose"
    echo "✅ 检测到新版 Docker Compose 插件 ($DOCKER_COMPOSE_CMD)。"
elif docker-compose --version &> /dev/null; then
    DOCKER_COMPOSE_CMD="docker-compose"
    echo "✅ 检测到独立版 Docker Compose ($DOCKER_COMPOSE_CMD)。"
else
    echo "❌ 找不到 Docker Compose 组件!尝试自动安装插件..."
    # 尝试补充安装插件版
    if command -v apt-get &> /dev/null; then
        apt-get update && apt-get install -y docker-compose-plugin
        DOCKER_COMPOSE_CMD="docker compose"
    elif command -v yum &> /dev/null; then
        yum install -y docker-compose-plugin
        DOCKER_COMPOSE_CMD="docker compose"
    else
        echo "❌ 无法自动安装,请手动安装 docker-compose-plugin。"
        exit 1
    fi
fi

echo "-------------------------------------------------------"

# 4. 检查前置配置文件
if [ ! -f "docker-compose.yml" ]; then
    echo "❌ 错误:当前目录下未找到 docker-compose.yml!请确保脚本与配置文件在同一目录。"
    exit 1
fi

if [ ! -f ".env" ]; then
    echo "⚠️ 警告:当前目录下未找到 .env 文件!"
    echo "ACME 申请证书需要 Cloudflare 凭证。如果你已经把变量硬编码到了 compose 文件中,可以忽略此警告。"
    read -p "是否继续部署?(y/n) " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

# ================= 新增部分:预生成临时证书占位 =================
echo "-------------------------------------------------------"
CERT_DIR="/home/nginx/cert"
mkdir -p "$CERT_DIR"

if [ ! -f "$CERT_DIR/cert.pem" ]; then
    echo "🔧 生成临时自签名证书,防止代理服务首次启动时因找不到证书而崩溃退出..."
    # 使用 openssl 生成一个有效期 1 天的临时证书,颁发者标识为 "temp.cert"
    openssl req -x509 -nodes -newkey rsa:2048 -days 1 \
        -keyout "$CERT_DIR/key.pem" \
        -out "$CERT_DIR/cert.pem" \
        -subj "/CN=temp.cert" >/dev/null 2>&1
    echo "✅ 临时证书生成完毕。"
fi
# ===============================================================
# 5. 执行部署操作
echo "🚀 开始拉取镜像并部署服务..."
# 使用动态识别到的命令执行
$DOCKER_COMPOSE_CMD up -d

echo "======================================================="
echo "🎉 部署完成!"
echo "👉 你可以使用以下命令查看运行状态:"
echo "   $DOCKER_COMPOSE_CMD ps"
echo "👉 查看证书申请进度或相关日志:"
echo "   $DOCKER_COMPOSE_CMD -f acme-worker"
echo "======================================================="