用 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 练习同样的流程。