5. 从代码到云端的部署实战
这是知识库中最核心的章节。覆盖从 SSH 连接到服务器、代码传输、Docker 构建与编排、一键启动、数据库运维到增量更新的完整流程。每条命令都附带原理说明,让你不仅会做,还懂为什么。
5.1 SSH:安全接管远程服务器
第一步:密码登录(首次连接)
# 在 Windows PowerShell 中执行
ssh root@211.159.xxx.xxx
原理说明:
ssh:Secure Shell 协议,建立加密的远程连接通道root:Linux 最高管理员账号@:连接符,"以 root 身份登录到后面的地址"- 首次登录会提示:
Are you sure you want to continue connecting?,输入yes接受服务器指纹- 输入密码时屏幕不显示字符:这是安全设计,防止旁人偷窥密码长度
第二步:从密码到密钥(为什么密码登录危险?)
互联网上每天有大量机器人程序自动扫描服务器 22 端口,尝试各种密码组合入侵。密钥对相当于给服务器换了一把"指纹锁":
# 在 Windows PowerShell 中生成密钥对
ssh-keygen -t rsa -b 4096
原理说明:
ssh-keygen生成两个文件:
id_rsa(私钥):你的身份证原件,绝对不能给任何人id_rsa.pub(公钥):可以公开,配置到服务器上- 登录时,SSH 用你的私钥签名一个挑战,服务器用你存的公钥验证
- 别人猜对密码也进不去——服务器只认装了公钥的电脑
-t rsa -b 4096:RSA 算法,4096 位密钥长度,远超民用破解能力
第三步:配置 SSH Config 实现秒连
# 在 C:\Users\你的用户名\.ssh\ 目录下创建 config 文件(无后缀)
# ~/.ssh/config
Host tao
HostName 211.159.xxx.xxx
User root
IdentityFile ~/.ssh/id_rsa
ServerAliveInterval 60
ServerAliveCountMax 10
配置完成后,登录只需一行:
ssh tao
原理说明:
Host tao:自定义别名,代替root@IP长串IdentityFile:指定私钥路径,SSH 不用盲目尝试其他钥匙ServerAliveInterval 60:每隔 60 秒发一个心跳包,防止连接因空闲被防火墙切断ServerAliveCountMax 10:连续 10 次心跳无回应才断开(约 10 分钟无操作才断)- 本质:config 文件让你从"每次敲 IP + 密码"变成"敲两个单词"
5.2 代码传输:把代码送上服务器
方案 A:Git 克隆(标准,推荐)
# 在服务器上执行
mkdir -p ~/ing && cd ~/ing
git clone https://github.com/你的用户名/项目名.git
cd 项目名
更新代码时:
# 本地提交
git add .
git commit -m "feat: 描述你的更新"
git push origin main
# 服务器拉取
ssh tao
cd ~/ing/项目名
git pull origin main
原理说明:
- Git 只上传
.js、.tsx、Dockerfile等源文件(通常几 MB)- 通过
.gitignore排除node_modules(数百 MB)和编译缓存- 这是一种**"轻量化传输,本地化构建"**策略——传图纸而不是传成品
方案 B:使用加速镜像(网络波动时)
# 当 GitHub 连接超时时使用代理
git clone https://ghproxy.net/https://github.com/Ing-la/openrise.git
原理说明:
- 加速站部署在海外与国内交界处,先替你下载 GitHub 文件,再通过优化线路回传
- 局限性:部分加速站对协议处理方式不同,可能导致
PROTOCOL_ERROR
方案 C:SCP "空投"传输(终极方案)
当服务器彻底无法连接 GitHub 时,直接从本地推送代码:
# 在 Windows PowerShell 中执行
# 1. 本地打包(排除不需要的重型文件夹)
tar --exclude="node_modules" --exclude=".next" --exclude=".git" --exclude=".agents" -czvf openrise.tar.gz .
# 2. 使用 SSH 别名上传到服务器
scp openrise.tar.gz tao:~/ing/
# 3. SSH 登录服务器解压
ssh tao
cd ~/ing
mkdir -p openrise
tar -xzvf openrise.tar.gz -C openrise/
原理说明:
- 为什么排除
node_modules? Windows 的依赖包在 Linux 上无法运行,服务器会现场npm install重新安装-C openrise/:解压到指定目录,保持根目录整洁scp:基于 SSH 协议加密传输,因为tao已绑定密钥,无需密码- SCP vs Git:SCP 是全量传输,适合首次或网络极差时;Git 增量传输,适合日常更新
5.3 环境变量配置
两份配置文件的用途
| 文件 | 用途 | 生效场景 |
|---|---|---|
根目录 .env |
Docker Compose 变量源 | docker-compose up 时注入到容器 |
frontend/.env |
Next.js 本地变量 | npm run dev 本地开发时 |
核心规则:
Docker 部署:只需根目录
.env,docker-compose 自动读取注入到容器
本地开发:需要frontend/.env,Next.js 直接读取
根目录 .env 配置(Docker 用)
# 从模板复制
cp .env.example .env
# 编辑配置
nano .env
# 数据库
POSTGRES_USER=openrise
POSTGRES_PASSWORD=你的强密码
POSTGRES_DB=openrise
# 认证
NEXTAUTH_SECRET=your-secret-here # 用 openssl rand -base64 32 生成
NEXTAUTH_URL=http://localhost # 按阶段修改为域名
# 邮件
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM=onboarding@resend.dev # 开发环境;正式上线用 verified@你的域名
# Turnstile(人机验证)
NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x...
TURNSTILE_SECRET_KEY=0x...
# 生成 NEXTAUTH_SECRET
openssl rand -base64 32
生产环境至少修改
| 变量 | 说明 |
|---|---|
POSTGRES_PASSWORD |
设置强密码 |
NEXTAUTH_SECRET |
openssl rand -base64 32 生成 |
NEXTAUTH_URL |
改为 https://你的域名 |
RESEND_API_KEY |
正式 API Key |
关键规则:
.env严禁进入 Git,已在.gitignore中排除- 每次服务器上修改
.env后,必须重启容器才能生效- 修改涉及
NEXT_PUBLIC_前缀的变量,需要--build重新构建(因为这些变量在构建时编译进前端代码)
5.4 Docker 构建与编排深度解析
核心前提:启动 Docker 引擎
# Windows 环境:双击打开 Docker Desktop
# 检查状态:右下角小鲸鱼图标变绿("Docker Desktop is running")
# 服务器环境:检查 Docker 运行状态
sudo systemctl status docker
原理说明:
docker-compose只是一个命令行工具(遥控器)- Docker 引擎才是真正干活的后台进程(电视机)
- 没有引擎,命令将无处执行
docker-compose up -d --build 的连环动作
这是部署中最核心的一行命令。按下回车后,后台经历了一个精密的流水线:
1. 解析蓝图 → 读取 docker-compose.yml
2. 执行构建 → 找到有 build 指令的服务,构建镜像
3. 拉取镜像 → 下载依赖的官方镜像(PostgreSQL、Nginx)
4. 建立网络 → 创建隔离网络 app-network
5. 顺序启动 → DB → Frontend(等待DB就绪)→ Nginx(最后启动)
6. 后台运行 → -d 参数让所有容器进入守护进程
# 一键启动所有服务
docker compose up -d --build
原理说明:
up:Docker Compose 核心命令,读取docker-compose.yml,像指挥官一样命令所有容器启动-d(Detached):后台运行模式。关掉 SSH 窗口后,网站依然 24 小时运行--build:强制重新构建镜像。即使已有镜像缓存,也会检查代码是否变化
什么时候需要 --build?
开发阶段 → 每次都加 --build:
原理说明:
- 镜像具有**"快照性"**——第一次
build时,Docker 把你当时的代码"拍了照片"封进镜像- 如果你改了代码但只运行
docker compose up(不加--build):
- Docker 检查发现"已有 frontend 镜像",直接启动旧镜像
- 刷破浏览器看到的还是旧网页
- Docker 很聪明:如果代码没变,
--build会因缓存瞬间跳过,不浪费时间
| 场景 | 命令 | 原因 |
|---|---|---|
| 改完代码 | up -d --build |
强制封装新代码 |
| 仅重启服务 | up -d(不加 --build) |
代码没变,无需构建 |
| 下班/释放资源 | down |
干净利落,不留残留进程 |
| 仅重启数据库 | restart db |
不动代码,只重启动力系统 |
docker-compose down 的拆除流水线
docker compose down
原理说明:
这不是简单的"关闭程序",而是有顺序的清除:
- SIGTERM:礼貌通知容器"请保存工作准备退出"
- SIGKILL:10 秒后没反应,强制切断电源
- 移除容器:这是
stop和down的本质区别——down把容器实体彻底抹除- 拆除网络:销毁隔离通信通道
- 保留卷数据:除非加
-v参数,否则down绝不删除数据,数据库资产稳如泰山
Dockerfile 多阶段构建原理
# frontend/Dockerfile
# 第一阶段:构建室(Builder)—— 有重型工具
FROM node:20-alpine AS builder
WORKDIR /app
# 【关键优化】先复制 package.json,利用缓存
COPY package.json ./
RUN npm install # 这一步最耗时,但依赖不常变
COPY . . # 后复制源码(经常变)
RUN npm run build # 编译
# 第二阶段:运行室(Runner)—— 极简干净
FROM node:20-alpine AS runner
WORKDIR /app
# 【精髓】只从 builder 搬走成品,1GB 垃圾全部扔掉
COPY --from=builder /app/.next/standalone ./
USER nextjs # 非 root 运行,安全第一
CMD ["node", "server.js"]
原理说明:
为什么需要两个阶段?
- 第一阶段(Builder)安装了所有构建工具(TypeScript 编译器、Webpack 等),体积可能 1GB+
- 第二阶段(Runner)只拷贝编译后的成品,镜像只有几十 MB
分层缓存优化:
COPY package.json ./ # ① 先复制依赖清单 RUN npm install # ② 安装依赖(最耗时) COPY . . # ③ 后复制源码(经常变化)
- 你每天改几百次代码,但可能一个月才加一个新依赖
- Docker 逐层检查:如果
package.json没变 → 直接复用缓存的npm install结果- 效果:第二次构建从"几分钟"缩短到"十几秒"
为什么需要 .dockerignore
# .dockerignore — 防止把本地垃圾塞进镜像
node_modules
.next
.git
*.md
原理说明:
- 就像
.gitignore,但作用对象是 Docker 构建- 不加这个,Docker 会把本地数百 MB 的
node_modules一起打包进镜像- 能让镜像从 2GB 暴减到 200MB
5.5 一键启动与验证
首次部署完整流程
# 在服务器上依次执行
# 1. 进入项目目录
cd ~/ing/openrise
# 2. 配置环境变量(从模板复制并编辑)
cp .env.example .env
nano .env
# ➜ 修改数据库密码、NEXTAUTH_SECRET、NEXTAUTH_URL 等
# 3. 一键启动(首次会下载镜像,约 3-5 分钟)
docker compose up -d --build
# 4. 检查容器状态(所有服务应为 Up)
docker compose ps
# 5. 查看启动日志(确认无错误)
docker compose logs -f frontend
验证清单
| 操作 | 预期结果 |
|---|---|
curl -I http://localhost |
返回 200 OK |
浏览器访问 http://服务器IP |
打开网页 |
http://服务器IP/api/health |
返回 {"status":"ok"} |
常用命令速查
# 查看所有容器状态
docker compose ps
# 查看所有服务日志(实时滚动)
docker compose logs -f
# 查看特定服务日志
docker compose logs -f frontend
# 停止并移除容器(保留数据)
docker compose down
# 完全清理(删除数据卷)
docker compose down -v # ⚠️ 会删除数据库数据!
# 仅停止(不删除容器)
docker compose stop
# 重新构建(无缓存)
docker compose build frontend --no-cache
5.6 生产环境配置修正与"名实同步"
在服务器上修改配置
# 用 vi 修改 .env
vi ~/ing/openrise/.env
vi 基本操作:
按 i → 进入编辑模式(底部显示 -- INSERT --)
编辑完成 → 按 Esc 退出编辑
:wq → 保存并退出
:q! → 不保存退出
修改配置后必须重启容器
核心原理(非常重要):
- Docker 容器是"静态读取"配置的
- 加载时机:只有在容器创建或重启的那一瞬间,Docker 才会去读
.env文件- 容器跑起来后,环境变量就存在于容器内存里了
- 此时在宿主机修改
.env,对已运行的容器没有任何影响
# ✅ 正确做法:修改 .env 后重启容器
docker compose up -d
# ❌ 错误认知:以为 restart 会重新读取 .env
docker compose restart # 有时不会重新读取外部 .env
up -d 为什么不需要先 down?
# ✅ 日常更新的标准命令
docker compose up -d --build
原理说明:
直接
up而不先down,Docker 会执行**"边跑边换"**策略:
- 构建新镜像——此时旧容器依然正常运行,用户还能访问
- 计算变更——发现镜像有更新,标记受影响容器
- 瞬间替换——停止旧容器,启动新容器(通常 1-3 秒)
如果先
down再up,会导致明显的服务中断。除非你要改网络架构或清理数据卷,否则日常更新直接用up -d --build。
| 命令 | 操作流程 | 适用场景 | 风险 |
|---|---|---|---|
up -d --build |
边跑边换 | 日常更新 | 极低 |
down |
先停止再拆除 | 深度维护(改网络、清数据) | 高:服务完全下线 |
stop |
只停止不删除 | 临时维护,省资源 | 中:再次启动快但文件不更新 |
5.7 数据库运维
开启 Prisma Studio 可视化管理
# 服务器上(通过 Docker 容器执行)
docker compose exec frontend npx prisma studio
原理说明:
exec frontend穿透容器壁,在运行中的前端服务容器里执行命令- 浏览器访问
http://服务器IP:5555,像 Excel 一样操作数据库- 注意:Prisma Studio 默认无登录验证,使用完
Ctrl + C关闭,防止数据泄露- 需要在云平台安全组开放 5555 端口
核心运维指令集
# 查看数据库日志(排查连接错误)
docker compose logs -f db
# 直接进入 SQL 命令行
docker compose exec db psql -U openrise -d openrise
# 数据备份(拍快照)
docker compose exec db pg_dump -U openrise > backup.sql
# 本地开发:同步数据库结构
npx prisma db push
# ⚠️ 慎用:重置数据库(清空所有表)
npx prisma migrate reset
数据库迁移自动化
# frontend/entrypoint.sh 中的关键环节
#!/bin/sh
set -e
# 先执行数据库迁移
echo "正在对比数据库版本并执行迁移..."
npx prisma migrate deploy
# 迁移成功后才启动应用
echo "数据库已就绪,正在启动 Next.js 应用..."
exec node server.js
原理说明:
这就是**"原子化升级"**——要么全对,要么不跑:
- 如果数据库迁移失败(如字段冲突),
set -e让脚本立即退出- 容器会保持在 Crash 状态,旧的业务代码永远不会在错误的数据库结构上运行
- 这让你在远端部署时拥有极强的心理保障
5.8 增量更新:迭代的正确姿势
当你的项目进入迭代期(从 1 到 100),核心挑战是"如何高效更新"。
标准更新流程
# 1. 本地修改代码 → 提交 → 推送
git add .
git commit -m "feat: 描述更新内容"
git push origin main
# 2. SSH 登录服务器 → 拉取 → 重启
ssh tao
cd ~/ing/openrise
git pull origin main
docker compose up -d --build
SCP 增量更新(无 Git 时)
如果在无 GitHub 环境下使用 SCP 传输:
# 本地打包
tar -czf openrise.tar.gz . --exclude='node_modules' --exclude='.git' --exclude='.agents'
# 上传
scp openrise.tar.gz tao:~/ing/openrise/
# 服务器上:先洗地再解压(保留 .env)
ssh tao
cd ~/ing/openrise
find . -maxdepth 1 ! -name 'openrise.tar.gz' ! -name '.env' ! -name '.' -exec rm -rf {} +
tar -xzf openrise.tar.gz
docker compose up -d --build
原理说明:
- 先删后解压:防止旧文件残留,确保服务器代码与本地完全一致
- 保留
.env:服务器端的.env包含生产密钥,绝对不能被覆盖- Docker 增量构建:Docker 镜像的分层缓存机制会对比文件哈希值——如果
package.json没变,直接跳过最耗时的npm install- 效果:第二次构建通常只需几十秒
重要原则
- 不要在服务器上直接改代码——所有变动必须始于本地,经由 Git/SCP 投递
- 小步快跑——不要攒一大堆改动才提交,频繁提交+部署降低风险
- 配置安全——
.env不进入 Git,环境变量与代码彻底分离
5.9 常见问题排查
1. 端口被占用
# 修改 docker-compose.yml 中的端口映射
# 例如改为 "8080:80",然后访问 http://服务器IP:8080
2. Frontend 构建失败
# 常见原因:网络问题导致 npm install 失败
# 清除缓存重新构建
docker compose build frontend --no-cache
3. 容器反复重启
# 查看日志定位原因
docker compose logs -f frontend
# 常见错误:"Cannot find module" = 依赖缺失
4. 数据库连接失败
# 检查数据库容器状态
docker compose ps db
docker compose logs db
# 确认 db 容器健康后再启动 frontend
5. 注册/登录异常
# 检查 NEXTAUTH_URL 配置
# Docker 环境下应为 http://localhost(或通过 nginx 的端口)
# 生产环境应为 https://你的域名
6. IP 打不开网页?
90% 的原因是云平台安全组没有放通 80 端口
→ 登录腾讯云/阿里云控制台 → 安全组 → 添加入站规则
→ 放通 80(HTTP)和 443(HTTPS)端口
7. 镜像里包含环境变量吗?
不包含。 变量在容器启动时由 docker-compose 从 .env 文件中注入。这确保了镜像本身是通用的、与环境无关的。
5.10 部署思维总结
Git 传递灵魂(逻辑),Docker 重塑肉身(环境),部署就是一场有序的更替。
本地开发 → Git/SCP → 服务器拉取 → Docker 构建 → 瞬间替换
↓ ↓ ↓ ↓ ↓
写代码 传源码 取到最新 打包镜像 1-3秒切换
用户几乎无感
生产环境的敬畏之心:
- 本地环境是你的实验室,可以爆炸,可以重来
- 生产环境是你的前线,必须通过标准化、工业化的流程(Docker + Git)来保证坚不可摧
- 当你习惯了"推代码即上线",你会发现曾经的 SCP 就像在手动搬砖
← 返回知识库 | → 下一篇:CI/CD 自动化流水线