从本机迁到 Linux VPS

把本地项目安顿到远方:运行环境、数据、入口与验收。

这一页的路标

这一章帮你做什么

写给自己电脑上已有一个在跑的服务、想把它搬到 VPS 常驻的人。前提:你大概知道这个服务读了哪些数据、由谁启动;有一台可登录的候选 VPS。读完你会得到:一份可填写的迁移工单、一个不接正式流量的候选实例,以及“先冻结、再切入口、最后开放唯一 writer”的验收顺序。

本机仍负责正式服务时,先验证 VPS 的兼容性、恢复与服务管理。 本机盘点:源码、数据、依赖,以及正在写数据的任务;VPS 候选:独立配置和测试入口,验证 systemd 与恢复;正式切换:停写 → 最终同步,切入口 → 唯一 writer
Mac 的 GUI、钥匙串与本地路径,不会自动变成 Linux 服务。本站原图
适用环境与验证范围

目标为 Ubuntu 24.04、systemd 和普通非 root SSH 用户;源端按 macOS、Linux/WSL 或 Windows 分支操作,Docker 仅用于原项目已有容器的情况。

本章是待填写的迁移教程,未执行真实迁移;文档与命令静态检查不代替候选、数据、入口和客户端验收。交给 agent 时使用 agent 入口。具体记录见来源与维护。

迁移的对象是一个可持续运行的服务:代码、数据、启动方式、定时任务、入口和恢复办法都要一起考虑。先在 VPS 上建立不接正式流量的候选实例,再决定切换。把本机目录复制过去,只完成了其中一小步。

本文以 Ubuntu 24.04、systemd、普通非 root SSH 用户为示例;Docker 仅适用于原项目已经使用容器的分支。命令中的 candidate-vps、operator、example_app 都是合成名称,必须先替换并核对。参考资料查阅日期:2026-10-02。

先复制填写迁移工单和服务清单,把真实记录放在自己的非公开运维目录。用它们记录源与目标、数据边界、停机窗口和恢复证据。

1. 先判断哪些值得迁

工作负载 适合迁移的理由 需要先解决的条件
无界面的 API、网站、小型后台任务 要持续在线,本机经常休眠 能在 Linux 安装、独立启动;有明确数据与维护责任人
已有 Linux container 的服务 运行环境已有声明 镜像支持目标 CPU;持久卷、secrets 和依赖仍需另迁
只在工作时使用的开发服务器 通常留本机更直接 若要公开提供服务,先补齐正式启动、认证、日志与恢复方式
依赖桌面登录、浏览器个人 profile、Keychain、Windows COM、USB 或本地文件交互的工具 通常保留本机执行部分 把可独立的 server 部分拆出;不要把个人桌面状态整包上传
高 I/O、大量私人数据、GPU 工作或收费软件 可能适合,也可能成本更高 比较实测资源、存储/流量费、授权和数据位置要求

写下迁移收益和可接受停机时间。没有 Linux 运行路径、没有可恢复备份,或无法确认哪些程序会写数据时,先解决这些问题。保留“本机 agent + VPS API”的分工也是有效结果,不必迁走全部内容。

2. 清点实际运行的东西

每项至少记下:服务名、运行机器/用户、版本、启动命令或 unit、监听地址、数据目录、secrets 来源、入口、依赖、writer、备份恢复方式、调度 owner,以及“迁移 / 留本机 / 退休”的决定。进程列表只是线索;还要覆盖 Docker、systemd timers、cron、LaunchAgents、Windows 计划任务、tunnel、证书续期、备份任务、DNS 和客户端配置。

在源本机:只执行符合该 OS 的只读命令。 输出可能包含私人路径或命令参数,只在本地检查,不要原样提交到 Git。

macOS 终端:

uname -m
lsof -nP -iTCP -sTCP:LISTEN
launchctl list
crontab -l

Windows PowerShell:

$env:PROCESSOR_ARCHITECTURE
Get-NetTCPConnection -State Listen
Get-ScheduledTask | Select-Object TaskPath, TaskName, State
Get-Service | Where-Object Status -eq 'Running'

Linux 或 WSL 内的终端:

uname -m
ss -lntup
systemctl list-units --type=service --state=running
systemctl list-timers --all
crontab -l

在实际拥有 Docker daemon 的机器,仅当项目用 Docker:

docker context show
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
docker volume ls

预期得到运行项清单,并能把每一项追到源配置。crontab -l 报没有 crontab 可以记录为“该用户无”;不能因此推断其他用户或系统 cron 都没有。Docker context 若指向远程 daemon,先停止盘点并确认操作对象;命令所在电脑不一定是容器所在机器。逐项读取已确认的配置,避免输出完整环境变量或 docker inspect 的 secret 值。

3. 做一次兼容性检查

维度 检查与处理
OS 和桌面依赖 .app、.exe、LaunchAgent plist、Windows Service/任务计划不直接变成 Linux 服务。找项目的 Linux 入口,缺失时先改造或保留本机部分。
CPU 架构 在源与目标分别看 uname -m;arm64/aarch64 与 x86_64/amd64 的原生模块、二进制、镜像可能不同。按目标架构重新构建并运行项目检查。
路径与大小写 把用户目录、盘符和反斜线改为配置项。Linux 常见文件系统区分大小写;Config.json 与 config.json 的混用可能在迁移后才暴露。实际文件系统行为以检查结果为准。
依赖与环境 记录运行时版本和 lockfile;在目标重建依赖。不要复制 macOS/Windows 的 .venv、node_modules、缓存或 Homebrew 安装目录充当 Linux 安装。
权限和身份 映射服务用户与目标 UID/GID、读写目录和可执行位;不要照搬源机所有 owner。代码通常只读,业务数据目录只授予服务所需的权限。
配置和 secrets Git 中只放无密钥示例;真实 token、数据库密码、SSH 私钥、tunnel 凭据通过批准的安全方式单独交付。优先使用应用支持的 secret file/credential 机制,环境变量中的敏感配置也不进入 Git。
时间与调度 核对时区、cron 解释、timer 补跑行为。候选机的邮件发送、同步、付款或导出 job 默认不启动,避免重复副作用。
网络 localhost 在 VPS 上指 VPS 自己。记录数据库、API、回调地址及 outbound 要求;把开发端口改成明确的 loopback 或批准的接口。

如果项目用 Docker,先在目标审阅 Compose 的镜像平台、bind mounts、named volumes、端口、用户和 restart policy。docker compose config --quiet 可检查配置是否可解析;它不验证运行兼容性。不要把 Docker Desktop VM 或整个 Docker 数据根目录作为跨 OS 迁移方案。Linux Docker 发布端口会参与自己的防火墙路径,不能只看到 UFW 的 deny 就认为端口未公开;优先使用 127.0.0.1:主机端口:容器端口,再从外部验证实际暴露面。Docker 官方防火墙说明

4. 建立独立候选机

本节前提是目标 VPS 已准备好,并有服务商控制台恢复通道。Ubuntu 24.04、SSH、systemd、所需运行时和传输工具应按各自官方安装文档准备;本文不把一条全系统安装/升级命令当作迁移步骤。

在目标 VPS,普通 SSH 会话;sudo 项需要管理员权限:

hostnamectl
uname -m
df -h / /var
free -h
systemctl --version
command -v python3
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

预期 OS、CPU、磁盘空间和目标角色与工单一致。通过服务商控制台或已信任渠道比对 SSH host key fingerprint,再保存新的 SSH alias;不要用 StrictHostKeyChecking=no 跳过身份核验。空间预算要容纳候选数据、备份/恢复副本和工作空间,不能只按源码体积估算。任一条件不符,先停在候选阶段,不触碰源 writer。

目标机保留独立的 machine ID、SSH host keys 和 mesh 节点身份。只复制应用声明的代码/数据/配置,不复制整份 /etc、/var/lib、本机 Keychain 或 mesh state。systemd 的 machine ID 是系统身份;Tailscale node state 包含识别设备的密钥。systemd 镜像身份说明、Tailscale node state

5. 先预拷贝可复制的文件

先用项目自身的 build/release 流程生成 Linux 可用的产物目录。例子假定源机的 migration-example/release/ 仅含已审阅的代码或静态文件;数据库、上传目录和 secrets 不混在其中。目标 staging 目录必须是本次迁移专用的新目录,不覆盖其他部署。

在目标 VPS,以 operator 用户运行:

install -d -m 0700 "$HOME/migration-example/release"

在源本机 macOS / Linux / 已配置 SSH 和 rsync 的 WSL,替换 alias 与源目录后运行:

rsync -rlt --dry-run --itemize-changes \
  "$HOME/migration-example/release/" \
  operator@candidate-vps:/home/operator/migration-example/release/

先检查路径方向与文件列表。只有列表符合预期且传输已授权,才去掉 --dry-run:

rsync -rlt --itemize-changes --partial \
  "$HOME/migration-example/release/" \
  operator@candidate-vps:/home/operator/migration-example/release/

这里的源路径尾部 / 表示复制目录内容;-rlt 保留目录结构、链接与时间,不试图照搬源机 owner。审阅 symlink,不能让它指向个人目录或目标机其他文件。Windows 原生 PowerShell 没有内置 rsync;可用已准备好的 WSL 运行本例,或改用经过清单核对的 SFTP 传输,不能照抄 POSIX 路径。两端的 rsync 必须均已可用。rsync 官方手册

本例不带 --delete。重复预拷贝不会自动清理源已删除、目标仍存在的文件;在最终冻结后审阅这些差异,或使用新的候选版本目录。任何非零返回、权限失败、磁盘不足或意外路径都先停止,不通过扩大 sudo 权限、加删除参数来“修好”。大传输使用长任务的 owner 与恢复方法。

6. 把一个 dev 命令交给 systemd 管理

下面是无 secrets、无写入、仅 loopback 的静态文件实验,用来学习“启动命令 → service → 日志 → 停止”的过程。Python http.server 不适合作为生产 Web server;它不提供认证,而且会跟随文件 symlink。目录只放本次合成页面,不放代码仓库、私人文件或任何 symlink。真实应用应使用项目支持的 production server。Python 3.12 http.server

在目标 Ubuntu VPS:先确认 /srv/field-demo、field-demo 用户及 field-demo.service 都不是已有业务。若已有同名对象,停下选择另一套名称,不覆盖。

getent passwd field-demo
getent group field-demo
systemctl status field-demo.service --no-pager
ls -ld /srv/field-demo

这一组在全新实验上预期显示不存在;若显示已有对象,不继续下面创建步骤。确认 /usr/bin/python3 存在且创建演示服务已获授权后:

sudo useradd --system --user-group --no-create-home \
  --home-dir /srv/field-demo --shell /usr/sbin/nologin field-demo
sudo install -d -o root -g root -m 0755 /srv/field-demo/public
printf '%s\n' 'field-demo candidate' | sudo tee /srv/field-demo/public/index.html >/dev/null
sudo chmod 0644 /srv/field-demo/public/index.html
sudoedit /etc/systemd/system/field-demo.service

将以下内容保存到刚打开的新 unit:

[Unit]
Description=Loopback static migration demonstration
After=network.target

[Service]
Type=exec
User=field-demo
Group=field-demo
WorkingDirectory=/srv/field-demo/public
ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1 --directory /srv/field-demo/public
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

仍在目标 VPS:

sudo systemd-analyze verify /etc/systemd/system/field-demo.service
sudo systemctl daemon-reload
sudo systemctl start field-demo.service
systemctl status field-demo.service --no-pager
journalctl -u field-demo.service -n 30 --no-pager
ss -lntp 'sport = :8080'
curl --noproxy '*' --fail --max-time 5 http://127.0.0.1:8080/

预期 verify 无 unit 错误、进程运行、只监听 127.0.0.1:8080,响应正文是 field-demo candidate。active 只证明进程存在;正文检查才能确认这次请求到了预期目录。若失败,先 sudo systemctl stop field-demo.service,检查 journal 中的路径、用户权限、端口占用,不开放防火墙来处理本地启动错误。Type=exec、重启策略和 journal 属于 systemd 的 service/执行语义。systemd service 源文档、systemd execution 源文档

在管理电脑,已验证 candidate-vps SSH alias;保持此窗口打开:

ssh -N -L 18080:127.0.0.1:8080 operator@candidate-vps

在管理电脑浏览器打开 http://127.0.0.1:18080/,应看见同一正文。Ctrl-C 关闭的是本地 SSH 转发,VPS service 继续运行。这里无需新增公网入站端口,也没有配置公网认证或 tunnel。若只做实验,最后在目标执行 sudo systemctl stop field-demo.service;若明确决定保留并随开机启动,验收后才执行 sudo systemctl enable field-demo.service。enable 不替代一次真实的重启恢复验收;重启须安排窗口。

真实应用沿用同样的 owner、绝对路径和日志思路,但从自己的 runbook 填写 ExecStart、数据目录和 secrets。不要把 .env 上传到静态目录。若应用只能读取 environment file,单独放在受限目录,限制读取权限;service 环境不是 secret vault,别把密码拼进命令行、日志或 Git。

7. 数据库先做恢复演练,再做最终快照

预拷贝允许源继续写,最终迁移必须定义一致性时刻:暂停 API 写入口、queue consumer、定时 job、同步器和管理员手工操作,等待在途事务结束,再生成最终快照。数据库与上传文件有关联时,二者必须属于同一冻结窗口;仅数据库内部一致还不够。

SQLite

不要在写入中只复制主 .sqlite 文件;WAL 模式可能还有相关状态。用应用的备份功能、SQLite Backup API 或 CLI .backup 生成独立快照。.backup 使用 backup 机制;VACUUM INTO 也是一致性快照选项,但输出文件必须不存在或为空,并可能占用更多 CPU。SQLite Backup API、SQLite VACUUM INTO

在源本机的 POSIX 终端,SQLite CLI 已安装;路径是示例,先替换成已核实的文件。 目标快照名称必须尚不存在,源文件必须存在且属于本次应用;SQLite 打开一个错误的新路径可能创建空数据库,不能只凭命令返回 0 判定成功。

sqlite3 /path/to/example-app/app.sqlite \
  ".backup '/path/to/private-migration/final-app.sqlite'"
sqlite3 /path/to/private-migration/final-app.sqlite 'PRAGMA integrity_check;'

Windows 原生环境使用同版本 sqlite3.exe,并把参数换成真实 Windows 文件路径;.backup 目标字符串可写成 C:/private-migration/final-app.sqlite。不把 POSIX /path/to 原样粘到 PowerShell。

预期完整性结果是 ok,并且应用关键表/记录符合冻结时的清单。确认备份命令完成后,通过已批准的 SSH/SFTP 路径把快照与同窗口的附件复制到候选机的私人 staging,并保留独立备份。在隔离候选机上用快照副本启动对应版本的应用,读取已知记录与关联附件;要做试写时使用可丢弃的演练副本,正式切换前重新从最终快照恢复。完整性检查不能证明业务数据齐全。失败则保留源和快照证据,查清原因;不切入口、不用空文件覆盖源。

PostgreSQL

小型数据库可用 logical dump/restore,避免跨 OS/版本直接复制运行中的 data directory。pg_dump 得到单库一致快照,但不包含集群级角色和 tablespace;与附件或其他库的一致性仍由应用冻结协调。dump 工具不能比源 server 的 major version 更旧,导入较旧 server 不受通用保证。先记录源、dump 工具与目标版本,再选相同版本或已演练的升级路径。PostgreSQL pg_dump

在源数据库所在机器或经批准的管理客户端;连接参数必须指向源库。 下面假定已经有 app_backup 备份角色、受限的本地备份目录和安全认证方式,不在连接 URL 或 shell history 里输入密码。

pg_dump --version
pg_dump --host=127.0.0.1 --username=app_backup \
  --format=custom --file=/path/to/private-migration/example-app.dump \
  --dbname=example_app

把成功完成的 dump 通过批准的加密传输复制到候选机,并保留源机之外的一份独立副本。失败或中断的 dump 不得当成最终快照。

在目标 VPS,已准备目标 PostgreSQL 与 migration_owner;只恢复到尚不存在的新候选库:

createdb --host=127.0.0.1 --username=migration_owner \
  --template=template0 example_app_candidate
pg_restore --host=127.0.0.1 --username=migration_owner \
  --exit-on-error --no-owner --no-privileges \
  --dbname=example_app_candidate /path/to/private-migration/example-app.dump

这组选项让候选对象归恢复角色所有,跳过源 ACL;它适合单应用演练,不是所有生产库的权限迁移方案。上线前按应用需要建立角色、权限、extensions 和 schema owner,逐项验收。若业务必须保留原 ownership/ACL,先制定角色映射和恢复步骤,再选择不同参数。不要为避开错误把 --exit-on-error 去掉,也不要对已有正式库加 --clean。恢复报错时保留失败候选库供诊断;修正后创建另一个明确命名的新库演练。PostgreSQL pg_restore

恢复成功后用应用检查核心记录、权限和附件,再在演练库做一条合成记录的写入、读回与业务级删除。dump 可列出目录、数据库可以连接,都不能替代这一步。数据库很大或停机预算无法容纳 dump/restore 时,应另选经过演练的 replication/backup 方案;不要临场缩短冻结步骤。

8. 切入口、开放唯一 writer、观察

阶段 源本机 目标 VPS 推进条件
预拷贝 / 恢复演练 正常服务,唯一正式 writer 入口隔离;只读或使用演练副本 能独立恢复,业务检查通过
最终冻结 所有正式 writer 已暂停 writer 保持关闭 在途操作已结束,最终快照与附件同步完成
候选验收 保持冻结,数据原样保留 恢复最终数据,继续禁写 origin、配置和数据验收通过
已授权切入口 旧地址仍不能写 接受选定入口;先读验收 DNS/tunnel/mesh/客户端分别确认
开放写入 继续禁止所有 writer 唯一正式 writer,记录首次写入时间 操作者确认切换,短暂冻结预算允许
观察与退旧 保留恢复材料和禁写状态 观察错误率、job 与新备份恢复 另行批准删除/停用源端内容

域名、tunnel、mesh 或客户端如何切换,按跨 VPS 迁移的入口策略选择实际使用的路径。本机迁移也适用;尤其不能在候选验证前复用正式 tunnel ID,造成两端并发接流量。

在真实客户端完成一次登录、读取和授权的合成写入/读回,核对请求确实到目标。再观察至少覆盖一次关键定时任务、一次备份及恢复演练的窗口。窗口长度按业务频率填写,不用一个固定“等 24 小时”替代判断。记录日志位置、维护 owner、报警接收方式和下一次恢复检查。

回滚分界是目标是否接受过新写入。 在目标仍未写入时,可在确认目标 writer 关闭后把入口恢复至源,重新放开源 writer;冻结期的排队请求仍需处理。目标已经写入后,旧数据已落后,不能仅改回 DNS。先冻结受影响写入、保留两侧数据,再选择在目标 forward recovery,或经对账的反向迁移;细节见回滚与数据边界。

完成报告分开写:源代码/配置已准备、候选已启动、数据恢复已验证、入口已切、目标已接收写入、真实客户端已验收、旧端是否保留。某层未执行就写“未验证”;不要把一个 HTTP 200 写成“迁移全部完成”。

想一想:把本机目录复制到 VPS,页面也返回 200,就能宣布迁移完成吗?

查看答案

还不能。需要确认 Linux 运行兼容性、数据恢复、服务与调度 owner,再按冻结和切入口顺序开放唯一 writer,并从真实客户端验收;200 只说明这次请求得到响应。

读到这里,喝口水吧。回到页首 ↑

在手册里找一找

输入关键词,搜索全部章节与工单。

关键词只在当前浏览器中检索。Esc 关闭

MOONLIGHT NOTE / 随手查词

到词表继续阅读 →