网络协议

VS Code Remote-SSH:远程开发 Linux 服务器状态面板

用 VS Code 的 Remote-SSH 直接在一台 Linux 服务器上写代码:从配置 SSH 密钥、连接服务器,到在远程开发一个“服务器状态面板”网页、断点调试、用端口转发在本机浏览器查看,最后注册成 systemd 服务长期运行。

BINBHO · 图文教程

用 VS Code 的 Remote-SSH 直接在一台 Linux 服务器上写代码:从配置 SSH 密钥、连接服务器,到在远程开发一个“服务器状态面板”网页、断点调试、用端口转发在本机浏览器查看,最后注册成 systemd 服务长期运行。

先看清成品

成品是一个只用 Python 标准库的状态面板:浏览器打开后显示主机名、CPU 负载、内存和磁盘占用、运行时长,每 5 秒刷新。服务只监听服务器的 127.0.0.1,靠 VS Code 端口转发访问,不对公网开放。本文代码已在 Linux 上实际运行,3 个测试通过,页面截图正常;VS Code 连接真实远程服务器的过程未在本文环境中操作。

做完之后你会得到什么?

成果 说明
SSH 配置 一条命令或点一下就能连上服务器
状态面板 / 网页,/api 返回 JSON
调试配置 在远程服务器上 F5 断点调试
任务配置 一键跑测试、一键重启服务
systemd 服务 开机自启,崩溃自动重启

需要准备哪些环境?

项目 要求 说明
VS Code 最新稳定版 本机
Remote - SSH 扩展 ms-vscode-remote.remote-ssh 本机安装
SSH 客户端 ssh 命令可用 Windows 10/11、macOS、Linux 都自带 OpenSSH
Linux 服务器 glibc 2.28+、内核 4.18+ 云服务器、家里的旧电脑或树莓派
服务器软件 openssh-server、bash、curl 或 wget、python3

官方明确支持 Ubuntu 20.04+、Debian 10+、RHEL/CentOS 8+、Raspberry Pi OS 10+。在服务器上用 ldd --version 查看 glibc 版本。

注意:CentOS 7、Ubuntu 18.04 等旧系统的 glibc 太低,新版 VS Code 已经无法连接。

第一步:怎样配置 SSH 密钥登录?

1. 本机生成密钥

ssh-keygen -t ed25519 -C "my-laptop"

一路回车,密钥保存在 ~/.ssh/id_ed25519。建议设置密码短语。

2. 把公钥放到服务器

macOS 和 Linux:

ssh-copy-id deploy@203.0.113.10

Windows PowerShell 没有 ssh-copy-id,可以这样做:

type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh deploy@203.0.113.10 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

3. 写 SSH 配置

编辑本机 ~/.ssh/config:

Host mybox
    HostName 203.0.113.10
    User deploy
    Port 22
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 30

203.0.113.10 是文档示例地址,换成你的服务器 IP。

4. 检查结果

ssh mybox "uname -a && python3 --version"

不需要输入服务器密码就能看到输出,说明密钥登录成功。

注意:确认密钥登录可用后,建议在服务器的 /etc/ssh/sshd_config 里关闭密码登录(PasswordAuthentication no),重启 sshd 前保持当前会话不要断开。

第二步:怎样用 VS Code 连接服务器?

1. 连接

按 F1,运行:

Remote-SSH: Connect to Host...

选择 mybox。第一次连接时,VS Code 会在服务器上自动安装 VS Code Server。

2. 检查结果

检查项 通过标准
左下角 显示 SSH: mybox
终端 打开的是服务器的 shell
打开文件夹 显示的是服务器上的目录

3. 安装远程扩展

扩展分两类:主题等界面类扩展装在本机;Python 等需要读代码的扩展要装在服务器上。在扩展面板里找到 Python,点击“在 SSH: mybox 中安装”。

第三步:在服务器上写什么代码?

在服务器上创建 ~/statusboard 并用 VS Code 打开。

statusboard/
├── .vscode/
│   ├── launch.json
│   └── tasks.json
├── deploy/
│   ├── statusboard.service
│   └── ssh_config.example
├── statusboard/
│   ├── __init__.py
│   ├── metrics.py
│   └── server.py
└── tests/
    └── test_metrics.py

1. 指标读取 statusboard/metrics.py

"""读取 Linux 服务器的基础指标,只用标准库。"""
import os
import shutil
import time
from pathlib import Path


def meminfo(text=None):
    text = text if text is not None else Path("/proc/meminfo").read_text()
    kv = {}
    for line in text.splitlines():
        key, _, rest = line.partition(":")
        kv[key] = int(rest.split()[0])  # 单位 kB
    total, avail = kv["MemTotal"], kv.get("MemAvailable", kv["MemFree"])
    return {"total_mb": total // 1024, "used_mb": (total - avail) // 1024,
            "percent": round((total - avail) * 100 / total, 1)}


def uptime_hours(text=None):
    text = text if text is not None else Path("/proc/uptime").read_text()
    return round(float(text.split()[0]) / 3600, 1)


def disk(path="/"):
    u = shutil.disk_usage(path)
    return {"total_gb": round(u.total / 1e9, 1), "used_gb": round(u.used / 1e9, 1),
            "percent": round(u.used * 100 / u.total, 1)}


def snapshot():
    load1, load5, load15 = os.getloadavg()
    return {
        "host": os.uname().nodename,
        "time": time.strftime("%Y-%m-%d %H:%M:%S"),
        "cpus": os.cpu_count(),
        "load": [round(load1, 2), round(load5, 2), round(load15, 2)],
        "memory": meminfo(),
        "disk": disk("/"),
        "uptime_h": uptime_hours(),
    }

内存使用率按 MemAvailable 计算,比只看 MemFree 更接近真实占用,因为 Linux 会把空闲内存拿去做缓存。

2. 网页和接口 statusboard/server.py

"""一个极简的服务器状态面板:/ 返回网页,/api 返回 JSON。"""
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from statusboard.metrics import snapshot

PAGE = """<!doctype html><html lang="zh-CN"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>服务器状态</title>
<style>body{font-family:system-ui,sans-serif;max-width:640px;margin:40px auto;padding:0 16px}
.card{border:1px solid #ddd;border-radius:12px;padding:16px;margin:12px 0}
.bar{height:10px;background:#eee;border-radius:5px}.bar i{display:block;height:100%;background:#3b82f6;border-radius:5px}</style>
<h1 id="host">加载中…</h1><div id="cards"></div>
<script>
async function load(){
  const d = await (await fetch('/api')).json();
  document.getElementById('host').textContent = d.host + ' · ' + d.time;
  const bar = p => `<div class="bar"><i style="width:${p}%"></i></div>`;
  document.getElementById('cards').innerHTML = `
  <div class="card">CPU ${d.cpus} 核,负载 ${d.load.join(' / ')}</div>
  <div class="card">内存 ${d.memory.used_mb} / ${d.memory.total_mb} MB(${d.memory.percent}%)${bar(d.memory.percent)}</div>
  <div class="card">磁盘 ${d.disk.used_gb} / ${d.disk.total_gb} GB(${d.disk.percent}%)${bar(d.disk.percent)}</div>
  <div class="card">已运行 ${d.uptime_h} 小时</div>`;
}
load(); setInterval(load, 5000);
</script></html>"""


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/api":
            body, ctype = json.dumps(snapshot(), ensure_ascii=False).encode(), "application/json; charset=utf-8"
        elif self.path == "/":
            body, ctype = PAGE.encode(), "text/html; charset=utf-8"
        else:
            self.send_error(404)
            return
        self.send_response(200)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def main():
    host = os.environ.get("BIND", "127.0.0.1")
    port = int(os.environ.get("PORT", "8080"))
    print(f"状态面板运行在 http://{host}:{port}")
    ThreadingHTTPServer((host, port), Handler).serve_forever()


if __name__ == "__main__":
    main()

默认只监听 127.0.0.1,外网无法直接访问。

3. 测试 tests/test_metrics.py

from statusboard.metrics import meminfo, snapshot, uptime_hours

SAMPLE = """MemTotal:        8000000 kB
MemFree:          500000 kB
MemAvailable:    2000000 kB
"""


def test_meminfo_uses_available():
    m = meminfo(SAMPLE)
    assert m["total_mb"] == 7812
    assert m["used_mb"] == 5859
    assert m["percent"] == 75.0


def test_uptime():
    assert uptime_hours("7200.55 100.00") == 2.0


def test_snapshot_keys():
    s = snapshot()
    assert {"host", "load", "memory", "disk", "uptime_h"} <= s.keys()

在服务器上安装 pytest:Debian/Ubuntu 可以 sudo apt install python3-pytest。

python3 -m pytest -q

本文实际输出:

3 passed in 0.01s

第四步:怎样在远程断点调试?

1. launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "调试状态面板",
      "type": "debugpy",
      "request": "launch",
      "module": "statusboard.server",
      "env": { "PORT": "8080" },
      "justMyCode": true
    },
    {
      "name": "调试当前测试文件",
      "type": "debugpy",
      "request": "launch",
      "module": "pytest",
      "args": ["-q", "${file}"]
    }
  ]
}

2. 开始调试

在 server.py 的 do_GET 里点断点,按 F5 选择“调试状态面板”。

VS Code 会检测到远程的 8080 端口并自动转发,右下角弹出提示,点击“在浏览器中打开”。

3. 检查结果

本机浏览器打开 http://localhost:8080,断点被触发;继续运行后看到面板。/api 返回类似:

{"host": "mybox", "time": "2026-10-21 13:05:00", "cpus": 8, "load": [0.12, 0.08, 0.05], "memory": {"total_mb": 16002, "used_mb": 8904, "percent": 55.6}, "disk": {"total_gb": 134.2, "used_gb": 24.5, "percent": 18.2}, "uptime_h": 3.6}

数值取决于你的服务器。底部“端口”面板里能看到转发列表,也可以手动添加。

4. 常用任务 tasks.json

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "运行测试",
      "type": "shell",
      "command": "python3 -m pytest -q",
      "group": { "kind": "test", "isDefault": true },
      "problemMatcher": []
    },
    {
      "label": "重启服务",
      "type": "shell",
      "command": "sudo systemctl restart statusboard && systemctl --no-pager status statusboard | head -5",
      "problemMatcher": []
    }
  ]
}

按 Ctrl+Shift+P 运行 Tasks: Run Task 选择任务。“运行测试”被设为默认测试任务。

第五步:怎样让它长期运行?

1. systemd 服务文件

[Unit]
Description=Server status board
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/statusboard
Environment=BIND=127.0.0.1
Environment=PORT=8080
ExecStart=/usr/bin/python3 -m statusboard.server
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target

把 User 和 WorkingDirectory 改成你的用户名和路径。

2. 安装并启动

sudo cp deploy/statusboard.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now statusboard
systemctl status statusboard

3. 检查结果

检查项 命令 期望
服务状态 systemctl status statusboard active (running)
接口 curl -s localhost:8080/api 返回 JSON
日志 journalctl -u statusboard -n 20 有“状态面板运行在”字样
重启后 sudo reboot 再检查 自动运行

不在 VS Code 里时,也可以用 SSH 端口转发访问:

ssh -L 8080:127.0.0.1:8080 mybox

注意:不要为了方便把 BIND 改成 0.0.0.0 暴露到公网。这个面板没有登录功能,确实需要外网访问时,应放在带认证的反向代理或零信任隧道后面。

出问题时该怎么调?

1. 连接时卡在 Setting up SSH Host

打开“输出”面板,选择 Remote - SSH 查看日志。常见原因:

日志关键词 解决方法
Permission denied (publickey) 公钥没放好,或 ~/.ssh 权限不对
Could not establish connection 先在终端 ssh mybox 确认能连
GLIBC_2.28 not found 服务器系统太旧,升级系统
下载 VS Code Server 超时 服务器访问外网受限,检查网络或代理

2. 端口 8080 被占用

OSError: [Errno 98] Address already in use

说明 systemd 服务已经在跑。调试前先 sudo systemctl stop statusboard,或在 launch.json 里把 PORT 改成 8081。

3. 内存数字和 free 命令对不上

free -m 的 used 不含缓存,算法和本文略有不同。本文按 MemTotal - MemAvailable 计算,和 free 里 total - available 一致。

常见问题怎么处理?

现象 原因 解决方法
每次都要输密码 用了密码登录 配置密钥,SSH 配置里写 IdentityFile
断线后要重连 网络空闲超时 配置 ServerAliveInterval 30
服务器上 Python 扩展没生效 扩展装在了本机 在扩展面板点“在 SSH 中安装”
sudo 任务要密码卡住 任务终端需要交互 在终端面板里输入密码
服务启动失败 路径或用户写错 journalctl -u statusboard 看报错
磁盘只显示根分区 代码只统计 / 修改 disk() 传入其他挂载点

项目资源

用途 地址
Remote-SSH 文档 https://code.visualstudio.com/docs/remote/ssh
Linux 远程开发要求 https://code.visualstudio.com/docs/remote/linux
远程开发排错 https://code.visualstudio.com/docs/remote/troubleshooting
Python 调试配置 https://code.visualstudio.com/docs/python/debugging
VS Code 任务 https://code.visualstudio.com/docs/debugtest/tasks
systemd.service 手册 https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
Python http.server https://docs.python.org/zh-cn/3/library/http.server.html

建议先只做到第二步:用 VS Code 连上服务器,左下角显示 SSH: mybox,在远程终端里执行一条命令。

连接稳定后再写状态面板。手头没有服务器时,可以先在本机用 WSL 或虚拟机里的 Ubuntu 练习同样的流程。