前言
故事是这样的。
我在做 AIGC 相关的开发,主力环境是一台没有公网 IP 的 GPU 实例以及一台阿里云灵构服务器 ,主要用来跑 ComfyUI、训练模型。
但使用过程中有几个让人难受的问题:
- 阿里云官方自带的远程桌面控制延迟巨大,体验感太差
- 文件传输太过麻烦,阿里云灵构需要借助网盘中转,而魔搭 WebIDE 有文件大小限制
我第一时间想到的解决办法就是内网穿透,不过第三方的穿透工具不太稳定,要么限速要么频繁掉线。刚好手上有一台 99 元的阿里云服务器,我寻思着不用也是浪费,干脆直接拿它来当做跳板机
不过我并没有考虑自建 frp 的方式来实现穿透,因为安装软件比较麻烦,每台目标机器都要安装 frpc 客户端的话太消耗时间,
本来薅来的免费 GPU 算力时长就有限,所以省事高效是第一原则。
于是我选择了 SSH 反向隧道的方式来穿透
我想要的效果是:
- 本地 VS Code 直接打开远程 GPU 的终端,并实现文件实时编辑和传输
- 本地浏览器直接访问
127.0.0.1:8188操作 ComfyUI - 服务器和本地电脑重启后自动打通隧道
实现原理
穿透的本质就是借道,借有公网IP服务器的道
网络拓扑如下:
scss
┌──────────┐ SSH 隧道 ┌──────────┐ 反向隧道 ┌──────────┐
│ 本地电脑 │ ───────────────▶ │ 阿里云 ECS │ ◀────────────── │ GPU 实例 │
│ │ │ (公网 IP) │ │ (无公网) │
└──────────┘ └──────────┘ └──────────┘
↑
└── 浏览器访问 127.0.0.1:8188
关键点只有三个:
- GPU 主动连阿里云(因为 GPU 能出网,阿里云有公网)
- 建立反向隧道(-R):把 GPU 的端口"伸"到阿里云本机
- 本地再连阿里云(-L):把本地端口"引"到阿里云
最终效果:
localhost:8188→ 阿里云 → GPU 的 ComfyUI- VS Code
ssh gpu→ 阿里云 → GPU 的终端
具体实现
准备工作:阿里云侧
确保阿里云允许端口转发:
bash
# 在阿里云上执行
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
sysctl -p
这里需要明白的是,由于我们走的是 SSH 22 端口通道,所以无需去阿里云安全组开放 2222 和 8188 等端口,有公网 ip 即可,22 端口默认都是开放的
第一步:GPU 实例配置
执行以下指令,让GPU 实例主动将端口暴露给阿里云服务器(记得将IP替换成你的):
bash
# 1. 创建 GPU 专用密钥
ssh-keygen -t ed25519 -f ~/.ssh/gpu_key -N ""
# 2. 把公钥拷贝到阿里云(让 GPU 能免密连阿里云)
ssh-copy-id -i ~/.ssh/gpu_key.pub root@阿里云公网 IP
# 3. 建立反向隧道:把 GPU 的 SSH 和 ComfyUI 映射到阿里云
ssh -N \
-R 127.0.0.1:2222:127.0.0.1:22 \
-R 127.0.0.1:8188:127.0.0.1:8188 \
-i ~/.ssh/gpu_key \
root@阿里云公网 IP
当然了,如果你不想每次手动挨个复制命令,我还准备了一键安装脚本,以下是在 GPU 上跑的脚本,核心逻辑是:密钥复用 + 反向隧道 +前台运行:
bash
#!/bin/bash
# gpu_tunnel_setup.sh
# 用法:sudo ./gpu_tunnel_setup.sh <实例ID> [INDEX]
# 例:sudo ./gpu_tunnel_setup.sh gpu-01
# sudo ./gpu_tunnel_setup.sh gpu-01 3
set -euo pipefail
# ===== 配置区 =====
ALIYUN_HOST="阿里云服务器IP"
ALIYUN_USER="root"
PERSIST_DIR="./gpu_keys"
KEY_NAME="gpu_key"
SERVICE_NAME="gpu-tunnel"
SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d"
SUPERVISORD_CONF="/etc/supervisor/supervisord.conf"
SOCK_FILE="/var/run/supervisor.sock"
# ==================
SSH_BASE=2222
COMFY_BASE=8188
# ===== 颜色 =====
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "\n${CYAN}==> $*${NC}"; }
# ===== 参数校验 =====
if [[ -z "${1:-}" ]]; then
err "用法: $0 <实例ID> [INDEX]"
echo " 例: $0 gpu-01"
echo " 例: $0 gpu-01 3"
exit 1
fi
INSTANCE_ID="$1"
if [[ -n "${2:-}" ]]; then
INDEX="$2"
else
INDEX=$(echo "$INSTANCE_ID" | grep -oE '[0-9]+' | head -n1 || true)
INDEX=${INDEX:-1}
fi
[[ "$INDEX" -eq 0 ]] && INDEX=1
SSH_PORT=$(( SSH_BASE + INDEX - 1 ))
COMFY_PORT=$(( COMFY_BASE + INDEX - 1 ))
echo "============================================"
echo " GPU 反向隧道部署"
echo " 实例ID : $INSTANCE_ID"
echo " INDEX : $INDEX"
echo " SSH端口 : $SSH_PORT"
echo " ComfyUI : $COMFY_PORT"
echo "============================================"
# ===== 密钥 =====
step "准备 SSH 密钥"
mkdir -p "$PERSIST_DIR"
chmod 700 "$PERSIST_DIR"
KEY_PATH="$PERSIST_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"
if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
info "复用已有密钥: $KEY_PATH"
else
info "生成新密钥..."
ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "gpu-$INSTANCE_ID"
chmod 600 "$KEY_PATH"
chmod 644 "$PUB_PATH"
fi
chmod 600 "$KEY_PATH"
chmod 644 "$PUB_PATH"
# ===== 安装 autossh =====
step "安装 autossh"
if ! command -v autossh &>/dev/null; then
info "安装 autossh..."
if command -v apt &>/dev/null; then
apt update && apt install -y autossh
elif command -v yum &>/dev/null; then
yum install -y autossh
elif command -v dnf &>/dev/null; then
dnf install -y autossh
else
err "无法安装 autossh,请手动安装"
exit 1
fi
fi
AUTOSSH_BIN=$(which autossh)
info "autossh: $AUTOSSH_BIN"
# ===== 运行用户 =====
if [[ -n "${SUDO_USER:-}" ]]; then
RUN_USER="$SUDO_USER"
else
RUN_USER="$(whoami)"
fi
if [[ "$RUN_USER" == "root" ]]; then
HOME_DIR="/root"
else
HOME_DIR="$(eval echo ~$RUN_USER)"
fi
mkdir -p "$HOME_DIR/.ssh"
chmod 700 "$HOME_DIR/.ssh"
info "运行用户: $RUN_USER"
info "HOME目录: $HOME_DIR"
# ===== 信任主机 + 公钥 =====
step "配置免密登录"
info "信任阿里云主机 ($ALIYUN_HOST)..."
ssh-keyscan -H "$ALIYUN_HOST" >> "$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true
sort -u "$HOME_DIR/.ssh/known_hosts" -o "$HOME_DIR/.ssh/known_hosts"
if ssh -o BatchMode=yes -o ConnectTimeout=5 \
-i "$KEY_PATH" "$ALIYUN_USER@$ALIYUN_HOST" "echo ok" &>/dev/null; then
info "免密登录已就绪"
else
info "写入公钥到 $ALIYUN_USER@$ALIYUN_HOST..."
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
"$ALIYUN_USER@$ALIYUN_HOST" \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
echo '$(cat "$PUB_PATH")' >> ~/.ssh/authorized_keys && \
sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys"
info "公钥写入完成"
fi
# ===== 端口检测 =====
step "端口检测"
for PORT in $SSH_PORT $COMFY_PORT; do
if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then
warn "端口 $PORT 已被占用"
fi
done
# ===== 构建 autossh 命令 =====
build_cmd() {
echo "export AUTOSSH_GATETIME=0 && export AUTOSSH_POLL=30 && \
export HOME=$HOME_DIR && \
$AUTOSSH_BIN -M 0 -N \
-R 127.0.0.1:${SSH_PORT}:127.0.0.1:22 \
-R 127.0.0.1:${COMFY_PORT}:127.0.0.1:8188 \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o UserKnownHostsFile=$HOME_DIR/.ssh/known_hosts \
-i ${KEY_PATH} \
${ALIYUN_USER}@${ALIYUN_HOST}"
}
# ===== 前台启动 =====
step "前台启动 autossh 隧道"
FOREGROUND_CMD=$(build_cmd)
info "执行命令:"
info "$FOREGROUND_CMD"
echo ""
info "按 Ctrl+C 停止隧道"
echo "============================================"
# 直接前台执行,autossh 输出直接打到终端
eval "$FOREGROUND_CMD"
将脚本上传到 GPU 实例中并执行,启动反向隧道(记得将阿里云IP替换成你的服务器)
如果你不想每次启动服务器都运行一遍脚本, 那我们可以开启 systemd 后台运行, 以下是补充开机自启动功能的版本:
sh
#!/bin/bash
# gpu_tunnel_setup.sh
# 全环境兼容 GPU 反向隧道部署 / 停止 / 状态查看
# 用法:
# 部署: sudo ./gpu_tunnel_setup.sh <实例ID> [INDEX]
# 停止: sudo ./gpu_tunnel_setup.sh <实例ID> --stop
# 状态: sudo ./gpu_tunnel_setup.sh <实例ID> --status
set -euo pipefail
# ===== 配置区 =====
ALIYUN_HOST="阿里云服务器IP"
ALIYUN_USER="root"
PERSIST_DIR="./gpu_keys"
KEY_NAME="gpu_key"
SERVICE_NAME="gpu-tunnel"
# ==================
SSH_BASE=2222
COMFY_BASE=8188
# ===== 颜色 =====
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "\n${CYAN}==> $*${NC}"; }
# ===== 参数解析 =====
MODE="deploy"
if [[ -z "${1:-}" ]]; then
err "用法: $0 <实例ID> [INDEX|--stop|--status]"
exit 1
fi
INSTANCE_ID="$1"
if [[ "${2:-}" == "--stop" ]] || [[ "${1:-}" == "--stop" ]]; then
MODE="stop"
fi
if [[ "${2:-}" == "--status" ]] || [[ "${1:-}" == "--status" ]]; then
MODE="status"
fi
# 运行用户
if [[ -n "${SUDO_USER:-}" ]]; then
RUN_USER="$SUDO_USER"
else
RUN_USER="$(whoami)"
fi
if [[ "$RUN_USER" == "root" ]]; then
HOME_DIR="/root"
else
HOME_DIR="$(eval echo ~$RUN_USER)"
fi
# ============================================================
# 查找同端口的 autossh 进程 PID(兼容不同 pgrep 版本)
# ============================================================
find_autossh_pids() {
local port="$1"
# 方法1: pgrep -f(大多数系统)
local pids
pids=$(pgrep -f "autossh" 2>/dev/null || true)
if [[ -n "$pids" ]]; then
echo "$pids" | while read pid; do
if [[ -d "/proc/$pid" ]]; then
cmdline=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' || true)
if echo "$cmdline" | grep -q "$port"; then
echo "$pid"
fi
fi
done
fi
}
# ============================================================
# --status 模式
# ============================================================
if [[ "$MODE" == "status" ]]; then
echo "============================================"
echo " GPU 隧道状态"
echo "============================================"
# 进程
echo ""
echo "--- 进程 ---"
if pgrep -af autossh >/dev/null 2>&1; then
pgrep -af autossh
echo ""
echo "--- 隧道端口映射 ---"
pgrep -af autossh | grep -oP '(?<=127\.0\.0\.1:)\d+' | sort -u | while read PORT; do
info "远程 ${ALIYUN_HOST} 上监听 127.0.0.1:${PORT}"
done
else
warn "没有运行中的 autossh 进程"
fi
# systemd
echo ""
echo "--- systemd ---"
if command -v systemctl &>/dev/null && systemctl list-unit-files 2>/dev/null | grep -q "${SERVICE_NAME}"; then
systemctl status ${SERVICE_NAME}.service --no-pager 2>/dev/null | head -5 || true
else
info "无 systemd 服务"
fi
# supervisor
echo ""
echo "--- supervisor ---"
if [[ -S /var/run/supervisor.sock ]]; then
supervisorctl -c /etc/supervisor/supervisord.conf status 2>/dev/null || true
else
info "supervisor 未运行"
fi
# crontab
echo ""
echo "--- crontab ---"
crontab -l 2>/dev/null | grep -v '^#' | grep -v '^$' | grep 'autossh' || info "无 crontab 条目"
# .bashrc
echo ""
echo "--- shell 自启动 ---"
if grep -q "GPU_TUNNEL_AUTO_START" ~/.bashrc 2>/dev/null; then
info ".bashrc 中有自启动片段"
else
info ".bashrc 中无自启动"
fi
exit 0
fi
# ============================================================
# --stop 模式
# ============================================================
if [[ "$MODE" == "stop" ]]; then
echo "============================================"
echo " 停止 GPU 隧道"
echo "============================================"
step "终止 autossh 进程..."
if pgrep -f 'autossh' >/dev/null 2>&1; then
pkill -9 -f 'autossh' || true
sleep 1
info "✅ autossh 进程已终止"
else
info "没有运行中的 autossh 进程"
fi
step "停止 systemd 服务..."
if command -v systemctl &>/dev/null; then
systemctl stop ${SERVICE_NAME}.service 2>/dev/null || true
systemctl disable ${SERVICE_NAME}.service 2>/dev/null || true
rm -f /etc/systemd/system/${SERVICE_NAME}.service 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
info "✅ systemd 已清理"
fi
step "停止 supervisor..."
if [[ -S /var/run/supervisor.sock ]]; then
supervisorctl -c /etc/supervisor/supervisord.conf stop ${SERVICE_NAME} 2>/dev/null || true
supervisorctl -c /etc/supervisor/supervisord.conf remove ${SERVICE_NAME} 2>/dev/null || true
fi
if [[ -f "/etc/supervisor/conf.d/${SERVICE_NAME}.conf" ]]; then
rm -f "/etc/supervisor/conf.d/${SERVICE_NAME}.conf"
info "✅ supervisor 配置已移除"
fi
if pgrep -x supervisord >/dev/null 2>&1; then
pkill -x supervisord 2>/dev/null || true
info "✅ supervisord 已停止"
fi
step "清理 crontab..."
TMP_CRON=$(mktemp)
crontab -l 2>/dev/null | grep -v 'autossh' | grep -v 'gpu-tunnel' > "$TMP_CRON" || true
crontab "$TMP_CRON" 2>/dev/null || true
rm -f "$TMP_CRON"
info "✅ crontab 已清理"
step "清理 shell 自启动..."
if grep -q "GPU_TUNNEL_AUTO_START" ~/.bashrc 2>/dev/null; then
sed -i '/# GPU_TUNNEL_AUTO_START_/,+6d' ~/.bashrc
info "✅ .bashrc 自启动已移除"
fi
echo ""
echo "============================================"
echo -e "${GREEN}✅ GPU 隧道已完全停止并清理${NC}"
echo "============================================"
exit 0
fi
# ============================================================
# deploy 模式
# ============================================================
INDEX="${2:-}"
if [[ -z "$INDEX" ]]; then
INDEX=$(echo "$INSTANCE_ID" | grep -oE '[0-9]+' | head -n1 || true)
INDEX=${INDEX:-1}
fi
[[ "$INDEX" -eq 0 ]] && INDEX=1
SSH_PORT=$(( SSH_BASE + INDEX - 1 ))
COMFY_PORT=$(( COMFY_BASE + INDEX - 1 ))
echo "============================================"
echo " GPU 反向隧道部署(全环境兼容)"
echo " 实例ID : $INSTANCE_ID"
echo " INDEX : $INDEX"
echo " SSH端口 : $SSH_PORT"
echo " ComfyUI : $COMFY_PORT"
echo "============================================"
# ===== 密钥 =====
step "准备 SSH 密钥"
mkdir -p "$PERSIST_DIR"
chmod 700 "$PERSIST_DIR"
KEY_PATH="$PERSIST_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"
if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
info "复用已有密钥: $KEY_PATH"
else
info "生成新密钥..."
ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "gpu-$INSTANCE_ID"
chmod 600 "$KEY_PATH"
chmod 644 "$PUB_PATH"
fi
chmod 600 "$KEY_PATH"
chmod 644 "$PUB_PATH"
# ===== 安装 autossh =====
step "安装 autossh"
if ! command -v autossh &>/dev/null; then
info "安装 autossh..."
if command -v apt &>/dev/null; then
apt update && apt install -y autossh
elif command -v yum &>/dev/null; then
yum install -y autossh
elif command -v dnf &>/dev/null; then
dnf install -y autossh
else
err "无法安装 autossh,请手动安装"
exit 1
fi
fi
AUTOSSH_BIN=$(which autossh)
info "autossh: $AUTOSSH_BIN"
mkdir -p "$HOME_DIR/.ssh"
chmod 700 "$HOME_DIR/.ssh"
info "运行用户: $RUN_USER"
info "HOME目录: $HOME_DIR"
# ===== 免密登录 =====
step "配置免密登录"
ssh-keyscan -H "$ALIYUN_HOST" >> "$HOME_DIR/.ssh/known_hosts" 2>/dev/null || true
sort -u "$HOME_DIR/.ssh/known_hosts" -o "$HOME_DIR/.ssh/known_hosts"
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 \
-i "$KEY_PATH" "$ALIYUN_USER@$ALIYUN_HOST" "echo ok" &>/dev/null; then
info "写入公钥..."
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
"$ALIYUN_USER@$ALIYUN_HOST" \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
echo '$(cat "$PUB_PATH")' >> ~/.ssh/authorized_keys && \
sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys && \
chmod 600 ~/.ssh/authorized_keys"
info "公钥写入完成"
else
info "免密登录已就绪"
fi
# ===== 端口检测(说明是远程端口)=====
step "端口检测"
for PORT in $SSH_PORT $COMFY_PORT; do
info "端口 $PORT 将在远程主机 ${ALIYUN_HOST} 上监听(本地无需监听)"
done
# ===== autossh 命令 =====
build_autossh_cmd() {
echo "$AUTOSSH_BIN -M 0 -N" \
"-R 127.0.0.1:${SSH_PORT}:127.0.0.1:22" \
"-R 127.0.0.1:${COMFY_PORT}:127.0.0.1:8188" \
"-o ServerAliveInterval=30" \
"-o ServerAliveCountMax=3" \
"-o ExitOnForwardFailure=yes" \
"-o UserKnownHostsFile=$HOME_DIR/.ssh/known_hosts" \
"-i ${KEY_PATH}" \
"${ALIYUN_USER}@${ALIYUN_HOST}"
}
AUTOSSH_CMD=$(build_autossh_cmd)
LOG_FILE="/var/log/gpu-tunnel/${INSTANCE_ID}.log"
mkdir -p /var/log/gpu-tunnel
# ============================================================
# 层级 1:systemd
# ============================================================
try_systemd() {
if ! command -v systemctl &>/dev/null; then
return 1
fi
if [[ ! -d /run/systemd/system ]]; then
return 1
fi
if [[ "$(ps -p 1 -o comm= 2>/dev/null)" != "systemd" ]]; then
return 1
fi
step "检测到 systemd,部署 service..."
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=GPU Reverse Tunnel ($INSTANCE_ID)
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=$RUN_USER
Environment=HOME=$HOME_DIR
Environment=AUTOSSH_GATETIME=0
Environment=AUTOSSH_POLL=30
ExecStart=/bin/sh -c '$AUTOSSH_CMD'
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}.service
systemctl restart ${SERVICE_NAME}.service
sleep 2
if systemctl is-active --quiet ${SERVICE_NAME}.service; then
info "✅ systemd 服务已启动并开机自启"
return 0
else
warn "systemd 服务启动失败"
return 1
fi
}
# ============================================================
# 层级 2:supervisor(修复版:timeout + 快速失败)
# ============================================================
try_supervisor() {
if ! command -v supervisord &>/dev/null; then
warn "supervisor 未安装,跳过"
return 1
fi
step "尝试 supervisor 部署..."
mkdir -p /etc/supervisor/conf.d
mkdir -p /var/run /var/log/supervisor
if [[ ! -f /etc/supervisor/supervisord.conf ]]; then
cat > /etc/supervisor/supervisord.conf <<EOF
[supervisord]
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/supervisor
nodaemon=false
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[include]
files = /etc/supervisor/conf.d/*.conf
EOF
fi
cat > "/etc/supervisor/conf.d/${SERVICE_NAME}.conf" <<EOF
[program:${SERVICE_NAME}]
command=$AUTOSSH_CMD
directory=$HOME_DIR
user=$RUN_USER
environment=HOME="$HOME_DIR",AUTOSSH_GATETIME="0",AUTOSSH_POLL="30"
autostart=true
autorestart=true
startsecs=5
startretries=999
redirect_stderr=true
stdout_logfile=$LOG_FILE
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
EOF
info "程序配置已写入"
# 清理旧残留
rm -f /var/run/supervisor.sock /var/run/supervisord.pid
# timeout 限制启动,5秒起不来直接放弃
if command -v timeout &>/dev/null; then
timeout 5 supervisord -c /etc/supervisor/supervisord.conf >/dev/null 2>&1 &
else
supervisord -c /etc/supervisor/supervisord.conf >/dev/null 2>&1 &
fi
local SUPERVISORD_PID=$!
sleep 2
if [[ -S /var/run/supervisor.sock ]]; then
# 先 stop 再 start,避免 "already started" 问题
supervisorctl -c /etc/supervisor/supervisord.conf stop ${SERVICE_NAME} 2>/dev/null || true
supervisorctl -c /etc/supervisor/supervisord.conf reread 2>/dev/null || true
supervisorctl -c /etc/supervisor/supervisord.conf update 2>/dev/null || true
supervisorctl -c /etc/supervisor/supervisord.conf start ${SERVICE_NAME} 2>/dev/null || true
sleep 1
info "✅ supervisor 部署完成"
return 0
else
kill -9 $SUPERVISORD_PID 2>/dev/null || true
rm -f /var/run/supervisor.sock /var/run/supervisord.pid
warn "supervisord 无法创建 socket(容器环境不兼容),跳过"
return 1
fi
}
# ============================================================
# 层级 3:crontab 守护
# ============================================================
try_crontab() {
step "部署 crontab 守护..."
if ! command -v cron &>/dev/null && ! command -v crond &>/dev/null; then
info "安装 cron..."
if command -v apt &>/dev/null; then
apt update && apt install -y cron
elif command -v yum &>/dev/null; then
yum install -y cronie || yum install -y cron || true
fi
fi
TMP_CRON=$(mktemp)
crontab -u "$RUN_USER" -l 2>/dev/null | grep -v "autossh.*${SSH_PORT}" | grep -v "gpu-tunnel" > "$TMP_CRON" || true
echo "* * * * * pgrep -f 'autossh' | while read pid; do if cat /proc/\$pid/cmdline 2>/dev/null | tr '\\0' ' ' | grep -q '${SSH_PORT}'; then exit 0; fi; done && (export AUTOSSH_GATETIME=0 AUTOSSH_POLL=30 HOME=$HOME_DIR; $AUTOSSH_CMD >> $LOG_FILE 2>&1 &)" >> "$TMP_CRON"
echo "@reboot (export AUTOSSH_GATETIME=0 AUTOSSH_POLL=30 HOME=$HOME_DIR; $AUTOSSH_CMD >> $LOG_FILE 2>&1 &)" >> "$TMP_CRON"
crontab -u "$RUN_USER" "$TMP_CRON" 2>/dev/null || true
rm -f "$TMP_CRON"
if command -v service &>/dev/null; then
service cron start 2>/dev/null || service crond start 2>/dev/null || true
fi
info "✅ crontab 守护已部署(每分钟检查 + @reboot 自启)"
return 0
}
# ============================================================
# 层级 4:shell 自启动兜底
# ============================================================
setup_shell_autostart() {
step "配置 shell 自启动兜底..."
local SHELL_RC=""
if [[ "$RUN_USER" == "root" ]]; then
SHELL_RC="/root/.bashrc"
else
SHELL_RC="$HOME_DIR/.bashrc"
[[ -f "$SHELL_RC" ]] || SHELL_RC="$HOME_DIR/.profile"
[[ -f "$SHELL_RC" ]] || touch "$SHELL_RC"
fi
local SNIPPET_MARKER="# GPU_TUNNEL_AUTO_START_${INSTANCE_ID}"
if ! grep -q "$SNIPPET_MARKER" "$SHELL_RC" 2>/dev/null; then
cat >> "$SHELL_RC" <<EOF
$SNIPPET_MARKER
if ! pgrep -f "autossh" | while read pid; do cat /proc/\$pid/cmdline 2>/dev/null | tr '\\0' ' ' | grep -q '${SSH_PORT}' && echo found; done | grep -q found; then
export AUTOSSH_GATETIME=0
export AUTOSSH_POLL=30
export HOME=$HOME_DIR
$AUTOSSH_CMD >> $LOG_FILE 2>&1 &
fi
EOF
info "✅ shell 自启动已写入 $SHELL_RC"
else
info "shell 自启动已存在,跳过"
fi
}
# ============================================================
# 主流程
# ============================================================
DEPLOYED=false
step "检测环境并部署..."
# ① systemd
if try_systemd; then
DEPLOYED=true
info "托管方式: systemd"
else
warn "systemd 不可用,尝试下一层..."
fi
# ② supervisor
if [[ "$DEPLOYED" == "false" ]]; then
if try_supervisor; then
DEPLOYED=true
info "托管方式: supervisor"
else
warn "supervisor 不可用,尝试下一层..."
fi
fi
# ③ crontab
if [[ "$DEPLOYED" == "false" ]]; then
if try_crontab; then
DEPLOYED=true
info "托管方式: crontab"
fi
fi
# ④ shell 兜底
setup_shell_autostart
# ⑤ 清理同端口旧进程,然后启动
step "确保没有同端口的旧进程..."
OLD_PIDS=$(find_autossh_pids "$SSH_PORT")
if [[ -n "$OLD_PIDS" ]]; then
echo "$OLD_PIDS" | while read pid; do
if [[ -n "$pid" ]]; then
kill -9 "$pid" 2>/dev/null || true
info "已终止旧进程 PID=$pid"
fi
done
sleep 1
fi
if ! pgrep -f "autossh" | while read pid; do
if [[ -d "/proc/$pid" ]]; then
cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | grep -q "$SSH_PORT" && echo "found"
fi
done | grep -q "found"; then
step "立即启动隧道..."
export AUTOSSH_GATETIME=0 AUTOSSH_POLL=30 HOME=$HOME_DIR
nohup $AUTOSSH_CMD >> "$LOG_FILE" 2>&1 &
sleep 2
if pgrep -f "autossh" | while read pid; do
cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | grep -q "$SSH_PORT" && echo "found"
done | grep -q "found"; then
info "✅ 隧道进程已启动"
else
warn "⚠️ 进程启动可能失败,检查日志: $LOG_FILE"
tail -10 "$LOG_FILE" 2>/dev/null || true
fi
else
info "隧道进程已在运行"
fi
# ===== 完成 =====
echo ""
echo "============================================"
if [[ "$DEPLOYED" == "true" ]]; then
echo -e "${GREEN}✅ GPU 隧道部署完成!${NC}"
else
echo -e "${YELLOW}⚠️ 部署完成(仅 shell 兜底模式)${NC}"
fi
echo ""
echo "📌 连接信息:"
echo " SSH : ssh -p $SSH_PORT $ALIYUN_USER@$ALIYUN_HOST"
echo " ComfyUI: http://$ALIYUN_HOST:$COMFY_PORT"
echo ""
echo "📌 管理:"
echo " 查看状态: $0 $INSTANCE_ID --status"
echo " 停止服务: $0 $INSTANCE_ID --stop"
echo " 查看日志: tail -f $LOG_FILE"
echo "============================================"
两个版本, 根据自身需求任选其一即可
第二步:本地电脑的配置
在本地电脑终端执行以下命令
bash
# 1. 创建 SSH 密钥
ssh-keygen -t ed25519 -f ~/.ssh/comfy_key -N ""
# 2. 把公钥拷贝到阿里云
ssh-copy-id -i ~/.ssh/comfy_key.pub root@阿里云公网 ip
# 3. 打通隧道:把本机 8188 引到阿里云 → GPU
ssh -N \
-L 8188:127.0.0.1:8188 \
-i ~/.ssh/comfy_key \
root@阿里云公网 IP
此时我们在浏览器上直接输入http://127.0.0.1:8188即可访问 GPU 实例中的 ComfyUI 了
但是我还要实现终端访问,所以我们需要另起一个终端,并输入以下指令:
bash
ssh -J root@阿里云公网 IP:22 -p 2222 ubuntu@127.0.0.1
或者
在用户目录下的 config 文件中添加如下配置:
yaml
# ===== 阿里云跳板机 =====
Host aliyun
HostName 阿里云公网 IP
User root
IdentityFile ~/.ssh/comfy_key
Port 22
StrictHostKeyChecking accept-new
# ===== GPU(经阿里云跳转)=====
Host gpu
HostName 127.0.0.1
User ubuntu
Port 2222
IdentityFile ~/.ssh/comfy_key
ProxyJump aliyun
ServerAliveInterval 30
ServerAliveCountMax 3
然后在终端中运行 ssh gpu,一样的效果,不过为了方便 VS Code 连接,建议采用配置的形式
这里需要注意的是, GPU 部分的 User 字段该填 root 还是 ubuntu ,取决于你的 GPU 服务器的设置,可以运行以下指令查看是否为 root:
bash
cat /etc/passwd | grep -E '/bin/bash|/bin/zsh'
同样的,本地电脑也有一键配置脚本,全自动化的同时不破坏原有的 SSH 配置,如下:
bash
#!/bin/bash
# local_tunnel_setup.sh
# 兼容 Linux / macOS
set -e
ALIYUN_HOST="阿里云服务器IP"
ALIYUN_USER="root"
TUNNEL_PORT="8188"
KEY_DIR="$HOME/.ssh"
KEY_NAME="comfy_key"
BLOCK_NAME="aliyun-tunnel"
mkdir -p "$KEY_DIR/config.d"
chmod 700 "$KEY_DIR"
touch "$KEY_DIR/config"
chmod 600 "$KEY_DIR/config"
KEY_PATH="$KEY_DIR/$KEY_NAME"
PUB_PATH="$KEY_PATH.pub"
# 密钥复用
if [[ -f "$KEY_PATH" && -f "$PUB_PATH" ]]; then
echo "✅ 复用本地密钥"
else
ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "local@$(hostname)"
fi
# 安全合并 SSH 配置(不覆盖!)
INCLUDE_LINE="Include $KEY_DIR/config.d/*.conf"
if ! grep -qxF "$INCLUDE_LINE" "$KEY_DIR/config"; then
if [ -s "$KEY_DIR/config" ]; then
sed -i '' "1i\\
$INCLUDE_LINE
" "$KEY_DIR/config"
else
echo "$INCLUDE_LINE" > "$KEY_DIR/config"
fi
fi
# 写独立配置片段
cat > "$KEY_DIR/config.d/${BLOCK_NAME}.conf" <<EOF
# Managed by local_tunnel.sh
Host aliyun
HostName $ALIYUN_HOST
User $ALIYUN_USER
Port 22
IdentityFile $KEY_PATH
StrictHostKeyChecking accept-new
Host gpu
HostName 127.0.0.1
Port 2222
User root
ProxyJump aliyun
IdentityFile $KEY_PATH
IdentitiesOnly yes
EOF
# 注册公钥
ssh-copy-id -i "$PUB_PATH" "$ALIYUN_USER@$ALIYUN_HOST" || true
# 跨平台自启
case "$(uname)" in
Linux)
cat > /etc/systemd/system/local-comfy-tunnel.service <<EOF
[Unit]
Description=Local SSH Tunnel to GPU
After=network-online.target
[Service]
Type=simple
User=$USER
ExecStart=/usr/bin/ssh -N \\
-L ${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT} \\
-o ServerAliveInterval=30 \\
-o ServerAliveCountMax=3 \\
-i ${KEY_PATH} \\
${ALIYUN_USER}@${ALIYUN_HOST}
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable local-comfy-tunnel.service
systemctl restart local-comfy-tunnel.service
;;
Darwin)
PLIST="$HOME/Library/LaunchAgents/com.user.local-comfy-tunnel.plist"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.local-comfy-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>$(which autossh)</string>
<string>-M</string><string>0</string>
<string>-N</string>
<string>-L</string>
<string>${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT}</string>
<string>-i</string>
<string>${KEY_PATH}</string>
<string>${ALIYUN_USER}@${ALIYUN_HOST}</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>
EOF
launchctl load "$PLIST"
;;
esac
echo "✅ 本地隧道已启动,访问 http://127.0.0.1:$TUNNEL_PORT"
该脚本兼容 Mac 和 Linux 平台
直接拷贝到本地电脑运行即可,(记得将阿里云IP替换成你的服务器IP)
第三步:将本地电脑公钥拷贝至GPU服务器
隧道打通之后, 阿里云服务器作为跳板机只负责数据传输, 不负责身份的验证
要想实现本地和GPU服务器ssh访问, 必须将本地的comfy_key公钥拷贝至GPU服务器的./ssh/authorized_keys文件中
在 GPU 终端里执行以下命令:
bash
# 1. 创建目录(没有就建,有了也不报错)
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 2. 创建 authorized_keys(没有就新建,有了就打开编辑)
# 把本地 ~/.ssh/comfy_key.pub 的输出粘贴进去
vim ~/.ssh/authorized_keys
# 3. 保存退出后,改权限
chmod 600 ~/.ssh/authorized_keys
# 4. 确认内容
cat ~/.ssh/authorized_keys
或者直接从阿里云中复制公钥:
bash
# 1. 创建目录和文件(没有就建)
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 2. 从阿里云拉取 authorized_keys 并追加到本地
ssh root@阿里云IP "cat ~/.ssh/authorized_keys" >> ~/.ssh/authorized_keys
# 3. 去重(防止重复执行导致多行重复)
sort -u ~/.ssh/authorized_keys -o ~/.ssh/authorized_keys
# 4. 改权限
chmod 600 ~/.ssh/authorized_keys
# 5. 确认内容
cat ~/.ssh/authorized_keys
第四步:开始访问
到这里,我想大家应该都知道怎么用了,这里之所以推荐大家使用VS Code的Remote-SSH插件 来连接,因为它既可以访问终端,又能访问文件,省去了手敲命令,一举两得,而且用起来也比较方便:
在VS Code 里:
F1→Remote-SSH: Connect to Host- 选择
gpu
会直接进到 GPU 的终端
它和阿里云是独立的互不影响,你想访问阿里云终端,还是按照原来的 ssh -p 22 root@阿里云公网 IP就行
多 GPU 怎么扩展?
假设我有 3 台 GPU,做法是:
| GPU | SSH 端口 | ComfyUI 端口 |
|---|---|---|
| gpu-01 | 2222 | 8188 |
| gpu-02 | 2223 | 8189 |
| gpu-03 | 2224 | 8190 |
本地 ~/.ssh/config:
bash
Host gpu-01
HostName 127.0.0.1
Port 2222
ProxyJump aliyun-relay
Host gpu-02
HostName 127.0.0.1
Port 2223
ProxyJump aliyun-relay
VS Code 可以同时连多台 GPU,互不干扰。
问题汇总
-
ssh gpu登录GPU提示需要输入root@127.0.0.1 password问题出在公钥上, 检查本地
comfy_key.pub公钥和GPU中authorized_keys中存储的公钥是否一致另外有一点需要注意的是, 不要使用老版
ssh-rsa格式密钥, 目前很多新系统, 推荐使用新的ssh-ed25519格式密钥 -
关于密钥问题
本地连接阿里云和连接
GPU服务器可以使用不同的密钥, 比如:yaml# Managed by local_tunnel.sh # 连接阿里云使用aliyun密钥对, 连接gpu使用comfy_key密钥对 Host aliyun HostName 阿里云服务器IP User root Port 22 IdentityFile /Users/xxx/.ssh/aliyun StrictHostKeyChecking accept-new Host gpu HostName 127.0.0.1 Port 2222 User root ProxyJump aliyun IdentityFile /Users/xxx/.ssh/comfy_key IdentitiesOnly yes隧道建立是先连接阿里云服务器, 成功后再连接GPU服务器, 阿里云只负责数据中转, 而不会拿着
comfy_key密钥去替我们连接GPU, 这点需要明白,所以, 他们不一定非得使用相同密钥对, 之前我们为了方便管理和使用, 共用了相同的
comfy_key密钥
音乐+技术公众号《乱码三千》,欢迎关注~
本文为作者原创 转载时请注明出处 谢谢