Mac + 日本服务器:把开发环境留在远端
Mac 如何通过 Tailscale、SSH 和 VS Code Remote SSH 使用远程服务器?记录环境配置、tmux 长任务、端口转发,以及连接超时和 Shell 启动问题的排查过程。
8 月中旬,我想解决一个很具体的问题:Mac 合上以后,正在跑的构建、测试和命令行任务能不能继续?换个地方打开电脑,能不能直接接着干,而不是重新找目录、装依赖、恢复终端?
于是有了这套远程开发环境:Mac 留在手边,负责编辑和查看结果;代码、运行时和长任务放在日本服务器上。日本只是这次服务器所在的位置,同样的组织方式也适用于其他地区的云主机,或者一台可以稳定访问的家中电脑。
这篇从 8 月 17 日的方案整理写起,也补上 8 月 19—20 日实际连接和排障的记录。文中的 jp-dev、tokyo-dev、dev 和项目目录均为示例;操作示范与当时已经完成的事情,会分别说明。
先决定哪些东西放在哪里
开始时,很容易把远程开发理解成“装一个远程连接工具”。真正整理起来,至少有四件事:机器之间怎么连、谁有权限登录、编辑器在哪里工作,以及断线后谁负责保留任务。
左右滑动查看完整表格
组件 | 在这套环境里的职责 | 它没有解决的问题 |
|---|---|---|
Tailscale | 提供 Mac 到服务器的私网连接 | 不会自动启动服务器的 SSH 服务 |
普通 SSH 与密钥 | 登录远端、传输命令和文件 | 不负责让应用自动恢复运行 |
VS Code Remote SSH | 用本机界面编辑远端目录 | 编辑器窗口本身不是任务守护进程 |
tmux | 将终端会话和其中的任务留在服务器 | 不能抵抗服务器重启、进程崩溃 |
我最后采用的是这四层组合。服务器保存项目工作目录、Git 仓库、依赖和开发服务,Mac 保存连接配置、自己的私钥,以及日常使用的编辑器。这样需要维护的是一个主要开发环境,而不是两份容易逐渐不同的项目副本。
它也有明确的代价:网络不好时,打开文件和交互会变慢;断网后不能继续编辑远端文件。需要离线工作时,仍应提前准备本地仓库。选择远程开发,是把持续运行和环境一致性放到了更高的位置。
第一步:先把私网和 SSH 接通
Tailscale 的作用是让两台设备进入允许互访的私网。服务器加入网络后,Mac 也需要登录相应网络;如果网络由团队管理,还可能涉及设备批准和访问规则。设备在线,只说明它加入了网络,并不等于所有服务端口都能访问。
示例中服务器的设备名是 tokyo-dev。在 Mac 终端先检查:
tailscale status
tailscale ping tokyo-dev如果设备名不能解析,可以先去管理页面核对名字以及 MagicDNS 设置,再用页面显示的 Tailscale 地址做对照。这里不用猜地址,也不需要把真实地址写进公开教程。Tailscale 设备连接说明
接下来是普通 SSH。服务器需要运行 SSH 服务,并已经准备好开发账号;Mac 持有私钥,服务器账号的 ~/.ssh/authorized_keys 保存相应公钥。本文继续使用普通 SSH 密钥认证,没有把 Tailscale SSH 当成必要条件。
如果还没有专用密钥,可以在 Mac 创建一把。下面文件名只是示例,执行前确认没有同名文件,不要覆盖已有私钥:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_jp_dev随后通过已有可信入口,把 .pub 文件内容交给服务器管理员,或追加到自己账号的 authorized_keys。私钥不需要上传服务器,也不应粘贴到聊天、工单或仓库。创建和使用密钥的参数可以对照 OpenSSH ssh-keygen 手册。
首次连接出现主机指纹时,应与云控制台或管理员提供的信息核对。我的 8 月 19 日记录中,也先确认了私网入口与原入口对应同一台主机,再继续配置编辑器。
第二步:用 SSH 别名收好连接信息
每天重复输入用户名、地址和密钥路径,很容易输错。我把这些信息集中到 Mac 的 ~/.ssh/config,日常只记住 jp-dev:
Host jp-dev
HostName tokyo-dev
User dev
IdentityFile ~/.ssh/id_ed25519_jp_dev
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 6其中 Host 是本机别名,HostName 是实际连接目标,User 是远端账号。IdentityFile 指定要使用的密钥;如果本机 SSH agent 管理了多把密钥,IdentitiesOnly 可以让这个连接只使用配置选定的身份。
两个 ServerAlive 参数负责检查连接是否还在响应。它们有助于及时发现失效连接,却不能阻止电脑休眠,也不会自动重连,更不会把普通终端里的任务变成后台服务。参数含义以 OpenSSH 客户端配置手册 为准。
配置完成后,先在 Mac 终端测试:
ssh jp-dev能进入远端以后,用 hostname、whoami 和 pwd 确认机器、用户和目录。这个小习惯很有用:在本机终端和远端终端之间切换时,先确认自己在哪里,再执行安装、删除或修改配置的命令。
第三步:让 VS Code 打开远端目录
基础 SSH 正常之后,再接入编辑器。Mac 安装 Microsoft 的 Remote - SSH 扩展,在命令面板执行 Remote-SSH: Connect to Host...,选择 jp-dev。
第一次连接会初始化远端的 VS Code Server。连接完成后,从这个窗口打开服务器上的项目目录,例如 ~/projects/demo。此时文件树展示的是远端文件,新建的集成终端也运行在远端;需要在远端工作的语言扩展,应按编辑器提示安装到对应主机。VS Code Remote SSH 文档
8 月 19 日的实际记录,到达了“SSH 连接成功、远端 Server 初始化完成、窗口显示远程主机”的状态。当时还没有打开项目目录,所以我没有把“编辑器连上了”写成整套工作流已经验收完成。后续进入具体项目,才继续检查文件树、终端和运行环境。
安装依赖时也要保留这个区分:Mac 上已经有 Node 或 Bun,不代表服务器上有;服务器有某个版本,也不代表符合当前项目要求。进入目录后先读项目说明和版本配置,再安装依赖。不要为了让一份教程看起来完整,就统一覆盖机器上所有项目的运行时。
远端 Git 的身份和访问权限也需要单独安排。Mac 能拉取仓库,不代表远端账号可以;也不应该因此把 Mac 的所有私钥复制过去。按项目需要配置独立权限,先确认仓库和分支,再开始修改。Remote SSH 给了一个熟悉的窗口,窗口背后仍是一台独立的机器。
第四步:长任务放进 tmux
tmux 是这套环境里对“合上电脑继续跑”最关键的一环。连接服务器之后,执行:
tmux new -As dev这条命令会创建名为 dev 的会话;已经有同名会话时,直接进入原来的会话。进入后再切换目录,启动构建、测试或其他长任务:
cd ~/projects/demo
# 接下来执行这个项目自己的构建、测试或开发命令需要离开时,按 Ctrl+b,松开,再按 d。这个动作是分离会话。下次重新 SSH 登录后,可以先看有哪些会话,再进入原来的那一个:
tmux ls
tmux attach -t dev分离和退出是两回事。 在 shell 里执行 exit 会结束这个 shell;按 Ctrl+c 通常会中断当前前台程序。想让任务继续,就分离会话,不要顺手把任务停止了。
一个会话里可以开多个窗口:默认快捷键 Ctrl+b 后按 c 新建窗口,按 n 切到下一个窗口。我更倾向于按项目组织会话,避免几天后看到许多相同名字的终端,却不知道哪一个正在跑服务。tmux 入门文档
这里仍有边界。tmux 保留的是服务器上的终端现场,服务器关机、进程崩溃或内存耗尽,任务一样可能结束。需要随开机自动启动、失败自动重启的正式服务,应另用服务管理方式,不能仅靠一个长期挂着的终端。
第五步:页面预览通过端口转发
项目跑在服务器上,浏览器在 Mac 上,这时两边的 localhost 指向不同机器。假设远端开发服务监听 127.0.0.1:3000,在 Mac 另开一个终端:
ssh -N -L 127.0.0.1:3000:127.0.0.1:3000 jp-dev命令左侧是 Mac 的监听地址,右侧是从服务器看过去的目标地址。保持这个连接,然后在 Mac 浏览器打开 http://127.0.0.1:3000。如果本机的 3000 已经被占用,可以只把左侧端口改成 3001,浏览器也跟着访问 3001。
VS Code 的 Ports 面板也能完成转发。需要注意,转发通道恢复和开发服务继续运行是两件事:Mac 断网后,服务可以仍在 tmux 里运行,但浏览器要等隧道重新建立才能访问。不能因为页面暂时打不开,就判断服务器上的程序已经退出。
这类预览不需要额外向公网开放开发端口。监听本机回环地址,也能避免无意把转发入口提供给同一局域网的其他设备。OpenSSH 端口转发参数
8 月 20 日:一次超时把排障顺序理清了
真正开始连接之后,问题往往不出在最先怀疑的地方。8 月 20 日,VS Code 出现 Connecting with SSH timed out。最初看起来像是服务器或密钥出了问题,但日志已经显示公钥认证成功,随后也收到了远端登录信息。
当时检查发现,Tailscale 连接从直连变成了中继,链路响应变慢,而编辑器仍按较短的等待时间结束连接。处理过程中,remote.SSH.connectTimeout 从当时的 15 秒改成 60 秒,重载后日志确认 SSH、远端 Server 和隧道均连接成功。
对应设置是 VS Code 的用户设置,合并到已有 JSON 中即可,不要覆盖其他配置:
{
"remote.SSH.connectTimeout": 60
}后来这台机器又按实际需求延长了等待时间,但我不会把“把超时调得更大”当作通用修复。超时只决定愿意等多久,不会改善线路本身;如果认证失败、远端程序报错,等再久也解决不了。
Tailscale 的直连和中继都是正常连接方式。中继不等于连不上,但延迟和吞吐可能影响体验;可以用 tailscale ping 观察当前路径,再决定排查网络还是应用。Tailscale 连接类型说明
我现在会按下面的层次看问题:
现象 | 优先核对的位置 |
|---|---|
设备名无法解析、目标不可达 | Tailscale 在线状态、名称、访问规则 |
SSH 提示公钥认证失败 | 账号、选中的密钥、服务器公钥配置 |
命令行能登录,编辑器连接失败 | Remote SSH 输出日志、启动脚本、等待时间 |
能打开项目,但命令不存在 | 远端运行时、PATH、当前 shell |
编辑器正常,浏览器打不开 | 远端服务是否运行、监听端口、转发状态 |
需要看 SSH 过程时,可以在 Mac 执行 ssh -v jp-dev。调试输出可能带有用户名、地址和本机路径,分享前应先去掉这些信息。
排查时我会保留已经能用的入口,一次只改一个环节。如果命令行连接正常,就先读编辑器日志;如果只是一项项目命令失败,就先看项目环境。不要把重新生成密钥、重装远端 Server、重启服务器一起做,否则即使恢复了,也很难知道是哪一步起作用,更可能打断原本还在运行的任务。
Shell 和文件传输,也要分清本机与远端
同一天的另一次处理与连接速度无关:交互终端想用 Fish,而 Remote SSH 还有自己的启动过程。那次最终保留了 Bash 作为账号登录 shell,并单独安排 Fish 作为日常终端,随后验证了远程连接和终端可用。
这是当时环境的处理结果,并不意味着 Fish 一定不能作为登录 shell。对新的环境,更稳妥的做法是先让默认配置连通,再逐项定制;尤其避免让每一种启动方式都无条件切换 shell、自动进入 tmux 或输出大段欢迎文字。官方排障文档也专门讨论了启动脚本对远端 Server 安装的影响。Remote SSH 排障文档
本机截图传到远端时,同样不能只凭终端文字能粘贴,就假设图片剪贴板也会同步。需要给远端程序查看图片,可以先把图片保存成文件,再从 Mac 上传:
scp ~/Downloads/example.png jp-dev:~/projects/demo/example.png这里假设本机图片和远端目录均已存在,目标也没有需要保留的同名文件。传完后,交给远端工具的是远端路径。涉及临时素材时,用完清理即可,不必为一张截图建立整套同步机制。
把断线恢复当作一次完整验收
最后,我给日常使用留了一套简单的检查顺序:在 tmux 中启动一个持续输出日志的任务,分离会话,断开 SSH,再连接回来;确认原会话和任务仍在,然后测试一次网络切换或短暂休眠。页面预览还要单独确认端口转发能够恢复。这些是复用方案时应执行的步骤,不能把前面的接入记录替代所有验收。
安全边界也尽量保持简单:开发账号只拿需要的权限,私钥留在自己的设备上,云控制台或其他可信恢复入口先保留。准备收紧公网 SSH 时,先在另一个连接中验证私网登录,再逐步调整,避免把自己锁在外面。
远端还需要备份。代码提交到仓库只覆盖已经提交的内容,数据库、未提交文件和本地配置要另有安排;密钥和环境变量也不应该为了备份方便被提交进代码库。
整理到这里,我希望每天真正需要记住的动作很少:连接 jp-dev,打开远端目录,进入对应的 tmux 会话;离开时分离,下次回来继续。网络仍然会变化,但开发现场有了一个明确的归处。
