如何优雅地直连无公网 IP 的远程 GPU 服务器

前言

故事是这样的。

我在做 AIGC 相关的开发,主力环境是一台没有公网 IP 的 GPU 实例以及一台阿里云灵构服务器,主要用来跑 ComfyUI、训练模型。

但使用过程中有几个让人难受的问题:

  • 阿里云官方自带的远程桌面控制延迟巨大,体验感太差
  • 文件传输太过麻烦,阿里云灵构需要借助网盘中转,而魔搭 WebIDE 有文件大小限制

我第一时间想到的解决办法就是内网穿透,不过第三方的穿透工具不太稳定,要么限速要么频繁掉线。刚好手上有一台 99 元的阿里云服务器,我寻思着不用也是浪费,干脆直接拿它来当做跳板机

不过我并没有考虑自建 frp 的方式来实现穿透,因为安装软件比较麻烦,每台目标机器都要安装 frpc 客户端的话太消耗时间,

本来薅来的免费 GPU 算力时长就有限,所以省事高效是第一原则。

于是我选择了 SSH 反向隧道的方式来穿透

我想要的效果是:

  • 本地 VS Code 直接打开远程 GPU 的终端,并实现文件实时编辑和传输
  • 本地浏览器直接访问 127.0.0.1:8188 操作 ComfyUI
  • 服务器和本地电脑重启后自动打通隧道

实现原理

穿透的本质就是借道,借有公网IP服务器的道

网络拓扑如下:

1
2
3
4
5
6
┌──────────┐     SSH 隧道      ┌──────────┐     反向隧道      ┌──────────┐
│ 本地电脑 │ ───────────────▶ │ 阿里云 ECS │ ◀────────────── │ GPU 实例 │
│ │ │ (公网 IP) │ │ (无公网) │
└──────────┘ └──────────┘ └──────────┘

└── 浏览器访问 127.0.0.1:8188

关键点只有三个:

  1. GPU 主动连阿里云(因为 GPU 能出网,阿里云有公网)
  2. 建立反向隧道(-R):把 GPU 的端口“伸”到阿里云本机
  3. 本地再连阿里云(-L):把本地端口“引”到阿里云

最终效果:

  • localhost:8188 → 阿里云 → GPU 的 ComfyUI
  • VS Code ssh gpu → 阿里云 → GPU 的终端

具体实现

准备工作:阿里云侧

确保阿里云允许端口转发:

1
2
3
# 在阿里云上执行
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
sysctl -p

这里需要明白的是,由于我们走的是 SSH 22 端口通道,所以无需去阿里云安全组开放 2222 和 8188 等端口,有公网 ip 即可,22 端口默认都是开放的


第一步:GPU 实例配置

执行以下指令,让GPU 实例主动将端口暴露给阿里云服务器(记得将IP替换成你的):

1
2
3
4
5
6
7
8
9
10
11
12
# 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 上跑的脚本,核心逻辑是:密钥复用 + 反向隧道 +前台运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/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后台运行, 以下是补充开机自启动功能的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
#!/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 "============================================"

两个版本, 根据自身需求任选其一即可


第二步:本地电脑的配置

在本地电脑终端执行以下命令

1
2
3
4
5
6
7
8
9
10
11
# 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

但是我还要实现终端访问,所以我们需要另起一个终端,并输入以下指令:

1
ssh -J root@阿里云公网 IP:22 -p 2222 ubuntu@127.0.0.1

或者

在用户目录下的 config文件中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ===== 阿里云跳板机 =====
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:

1
2

cat /etc/passwd | grep -E '/bin/bash|/bin/zsh'

同样的,本地电脑也有一键配置脚本,全自动化的同时不破坏原有的 SSH配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/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 终端里执行以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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

或者直接从阿里云中复制公钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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 CodeRemote-SSH插件 来连接,因为它既可以访问终端,又能访问文件,省去了手敲命令,一举两得,而且用起来也比较方便:

VS Code里:

  • F1Remote-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

1
2
3
4
5
6
7
8
9
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,互不干扰。

问题汇总

  1. ssh gpu登录GPU提示需要输入root@127.0.0.1 password

    问题出在公钥上, 检查本地comfy_key.pub公钥和GPUauthorized_keys中存储的公钥是否一致

    另外有一点需要注意的是, 不要使用老版ssh-rsa格式密钥, 目前很多新系统, 推荐使用新的ssh-ed25519格式密钥

  2. 关于密钥问题

    本地连接阿里云和连接GPU服务器可以使用不同的密钥, 比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 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密钥

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

0%