Docker部署血泪史
前置知识:
是个啥:
它是一个利用集装箱思维,解决环境不一致问题的工具,比虚拟机更清亮。
它的核心玩法就是从仓库中拉取镜像,然后运行成容器
好处:
-
开发更加的轻松,不用再一行一行的去配置环境了
-
部署更加的高效,一条命令就能立马上线
-
环境更加的干净,容器和主机之间不会互相的干扰污染

镜像:相当于程序的模板,包含了运行程序所需的所有环境和依赖
容器:容器是镜像的运行实例,相当于把程序放入容器中运行,实现环境隔离
仓库:用于存储和分发镜像
工作的原理:

和虚拟机的区别:

一、6.2第一次部署:从 0 到 1 的地狱开局
第一次部署的时候,我天真地以为只要git clone + docker compose up就能搞定一切。结果现实给了我一记响亮的耳光,整整花了半天时间,踩了好几个坑才把项目跑起来。
坑 1:pnpm install 依赖安装失败
报错:ERR_PNPM_IGNORED_BUILDS · Ignored build scripts: sharp, unrs-resolver
原因:新版 pnpm 为了安全,默认拦截了 sharp、unrs-resolver 等依赖的后置构建脚本。
解决方案:
- 在
frontend/pnpm-workspace.yaml里放行构建脚本:
packages:
- .
allowBuilds:
sharp: true
unrs-resolver: true
- 修改
frontend/Dockerfile,打包时同步复制该配置文件:
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
坑 2:环境变量协议头重复拼接
错误配置:NEXT_PUBLIC_UPLOAD_PROXY_URL=http:http://192.168.10.8
原因:配置的时候手滑,多写了一个http:。
后果:前端所有上传请求都变成了无效地址。
修复:改成正确的http://192.168.10.8:18080
坑 3:80 端口被占用
报错:address already in use · failed to bind host port 0.0.0.0:80
原因:服务器上已经有其他服务占了 80 端口。
解决方案:
- 修改
docker-compose.yml端口映射:
ports:
- "18080:80"
- 同步修改.env 里的两个全局地址:
NEXT_PUBLIC_API_BASE_URL=http://192.168.10.8:18080
NEXT_PUBLIC_UPLOAD_PROXY_URL=http://192.168.10.8:18080
坑 4:8080 端口也被占用
刚解决完 80 端口的问题,发现 8080 也被占了。行吧,那就直接用 18080 作为对外端口,一劳永逸。
坑 5:Backend 容器无限重启
报错:MODULE_NOT_FOUND · /app/dist/config/prisma.js
原因:Prisma 生成的文件在src/generated/prisma,但 TS 编译后运行目录是dist,找不到这个目录。
修复:在后端 Dockerfile 的 build 步骤后面加一行:
RUN pnpm build
# 新增:拷贝prisma生成目录至dist
RUN mkdir -p dist/generated && cp -R src/generated/prisma dist/generated/prisma
坑 6:历史图片全部无法显示
现象:页面上所有旧图片都是裂的,地址还是http://localhost:3001/uploads/xxx。
原因:数据库里存的是完整的本地域名,部署到服务器后当然访问不了。
优化:数据库只存相对路径,不存完整域名!然后重新部署,但是考虑这种方案不好,于是多方对比:
之前:
| 环节 | 数据形式 |
|---|---|
| 浏览器中 | File 对象(包含二进制) |
| HTTP 传输 | multipart/form-data 编码的二进制流 |
| 后端接收 | multer 解析为 req.file(Buffer + 元信息) |
| 存入磁盘 | fs.writeFile 写成真实的 .jpg/.png 文件 |
| 存入数据库 | 只存路径字符串,如 /uploads/avatars/avatar_xxx.jpg |
| 后续访问 | 通过 express.static('/uploads') 以静态文件方式返回 |
经过对比发现:
1. 本地磁盘存储(你原表格的标准流程)
| 环节 | 数据形式 |
|---|---|
| 浏览器中 | File 对象(包含二进制文件内容) |
| HTTP 传输 | multipart/form-data 编码的二进制流 |
| 后端接收 | multer 中间件解析为 req.file(包含 Buffer 二进制 + 文件名 / 大小等元信息) |
| 持久化存储 | fs.writeFile 将 Buffer 写入服务器本地磁盘,生成 .jpg/.png 等真实文件 |
| 存入数据库 | 只存相对路径字符串,如 /uploads/avatars/avatar_123.jpg(最佳实践) |
| 后续访问 | 后端通过 express.static('/uploads') 托管静态文件,浏览器直接请求路径 |
2. 云存储(以阿里云 OSS 为例)
| 环节 | 数据形式 |
|---|---|
| 浏览器中 | File 对象(包含二进制文件内容)与本地完全一致 |
| HTTP 传输 | multipart/form-data 编码的二进制流 与本地完全一致 |
| 后端接收 | multer 中间件解析为 req.file(Buffer + 元信息)与本地完全一致 |
| 持久化存储 | 调用云存储 SDK(如 ali-oss)将 Buffer 直接上传至 OSS,返回文件 Object Key 或完整 URL |
| 存入数据库 | 推荐只存 Object Key(如 avatars/avatar_123.jpg),不存完整域名 |
| 后续访问 | 直接通过 OSS 原生 URL 或 CDN 加速地址访问,无需后端提供静态文件服务 |
二、核心维度详细对比
| 对比维度 | 本地磁盘存储 | 云存储(OSS) | 关键差异点 |
|---|---|---|---|
| 链路复杂度 | 简单,纯后端本地操作 | 多一步云存储 API 调用 | 前 3 个环节完全相同,差异从 “持久化” 开始 |
| 存储成本 | 服务器硬盘成本(一次性投入) 1TB≈500 元 / 永久 | 按量付费:0.12 元 / GB / 月(标准存储) 下行流量:0.5 元 / GB | 小流量场景本地更便宜 大流量 / 大容量场景云存储更划算 |
| 可靠性 | 低,依赖服务器硬盘 单盘损坏数据永久丢失 需手动备份 | 极高,OSS 默认 12 个 9 的数据可靠性 自动多副本容灾 支持跨区域备份 | 云存储从根本上解决了数据丢失问题 |
| 扩展性 | 差,硬盘满了需要手动扩容 单服务器带宽有限 | 无限扩容,无需关心容量 自带弹性带宽,支持百万级并发 | 云存储天生适合业务快速增长 |
| 访问性能 | 受限于服务器带宽和地理位置 跨地域访问慢 | 自带全球 CDN 加速 就近节点访问,延迟低 | 有全国 / 全球用户时,云存储访问速度快 10 倍以上 |
| 运维成本 | 高,需要处理: ・硬盘扩容 ・数据备份 ・静态文件服务优化 ・带宽监控 | 几乎为 0,所有运维由云厂商负责 | 云存储解放了大量运维精力 |
| 安全性 | 依赖服务器安全配置 容易被爬虫遍历 需自己实现防盗链 | 自带完善的安全体系: ・细粒度权限控制 ・防盗链 ・访问日志 ・数据加密 | 云存储的安全性远高于自建 |
| 部署复杂度 | 低,无需额外配置 但需注意: ・文件权限 ・磁盘挂载 ・静态文件路由 | 中等,需要处理: ・SDK 集成 ・AccessKey 配置 ・权限策略 ・DNS 解析(你踩过的坑) | 本地部署简单,但后续麻烦多 云存储前期麻烦,后续一劳永逸 |
| 数据迁移难度 | 极高,需要拷贝大量文件 迁移时服务会中断 | 极低,只需修改数据库中的域名前缀 或直接使用 CDN 无缝切换 | 云存储让数据迁移变得几乎无感知 |
| 功能丰富度 | 只有基础的文件读写 | 自带: ・图片处理(裁剪 / 压缩 / 水印) ・视频转码 ・生命周期管理 ・直传签名 | 云存储提供了大量开箱即用的功能 |
改正:
核心思路 :前端不变,后端接收文件后上传到阿里云 OSS,数据库存储 OSS 的完整 URL。
- 新图片 → 存储到 OSS,数据库存完整 URL(如 https://bucket.oss-cn-xxx.aliyuncs.com/avatars/xxx.jpg )
- 旧图片 → 保持本地 /uploads 静态服务不变,确保兼容
二、改了图片上传的方式,开始第二次部署
本以为轻车熟路,结果又踩新坑。有了第一次的经验,我以为第二次部署会很顺利。没想到,这次遇到的都是更隐蔽、更恶心的坑。
坑 1:前端接口全部报错,页面解析失败
现象:后端进程正常(日志显示运行在 4001 端口,Redis 连接成功),但前端所有接口都报 URL 错误、网页解析失败。
排查过程:
-
先看浏览器控制台,发现请求地址是
http://192.168.10.8.18080 -
哦!端口分隔符
:被我写成了.!
根因:前端环境变量NEXT_PUBLIC_API_BASE_URL书写错误。
修复:把http://192.168.10.8.18080改成http://192.168.10.8:18080
坑 2:头像上传超时,OSS 请求失败
报错:AxiosError: timeout of 5000ms exceeded,后端日志getaddrinfo EAI_AGAIN codestory.oss-cn-beijing.aliyuncs.com
原因:Docker 容器默认没有配置 DNS,无法解析阿里云 OSS 的域名。
修复:在docker-compose.yml或docker-compose.override.yml里给 backend 服务加 DNS 配置:
dns:
- 223.5.5.5
- 8.8.8.8
坑 3:图片上传成功但前端无法显示
现象:文件上传成功,直接访问 OSS 原始地址能打开,但前端显示 403 Forbidden。
原因:阿里云 OSS 默认开启了「阻止公共访问」策略,图片没有公开读取权限。
修复:去 OSS 控制台关闭「阻止公共访问」,开启文件公共读权限。
坑 4:头像还是无法显示,Next 图片代理 500 错误
现象:OSS 原始地址能访问,但 Next.js 的图片代理地址/_next/image?url=...返回 500。
排查过程:
-
先看前端容器日志,发现还是域名解析失败
-
哦!我只给后端配了 DNS,前端容器也需要解析 OSS 域名啊!
修复:给 frontend 服务也加上同样的 DNS 配置。
三、面试官最爱问:你是怎么排查部署问题的?
“我就是瞎试出来的”,这可不是,我有系统性排查思路。
我会按照从外到内、从前端到后端、从网络到权限的顺序逐层排查,核心思路是先定位问题发生在哪个环节,再深入解决:
-
先看容器状态:用
docker compose ps看有没有容器在重启或者退出 -
再看日志:用
docker compose logs --tail=100 服务名找具体的报错信息 -
测试网络连通性:
-
用
curl -I 地址测试 URL 是否能访问 -
用
nslookup 域名测试域名解析是否正常 -
进入容器内部执行同样的命令,区分是宿主机问题还是容器问题
-
-
检查配置文件:重点看环境变量、端口映射、DNS 配置
-
检查权限:比如 OSS 的访问权限、文件系统的读写权限
部署前我会先想清楚这 10 个问题:
-
前端怎么 build?怎么 start?
-
后端怎么 build?怎么 start?
-
后端需要哪些环境变量?
-
数据库是在本机容器里,还是远程?
-
用户最终访问哪个地址?
-
Nginx 要把哪些路径转给前端 / 后端?
-
上传文件要不要持久化?
-
哪些配置不能提交到 Git?
四、标准化部署流程:从此告别手忙脚乱
把踩过的坑变成标准化流程,下次部署就不会再犯同样的错误了。
首次部署完整步骤
登录服务器:
ssh yuanjing@192.168.10.8 -p 22
进入项目目录:

mkdir -p ~/apps
cd ~/apps
git clone -b develop https://github.com/yuanjingteam/CodeStory.git CodeStory
cd CodeStory
配置环境变量:
cp .env.production.example .env
nano .env
生产环境参考配置:
DATABASE_URL=postgresql://数据库账号:数据库密码@老师数据库IP:5432/数据库名
REDIS_URL=redis://redis:6379
NODE_ENV=production
PORT=4001
JWT_SECRET=你的JWT密钥
JWT_EXPIRES_IN=7d
EMAIL_USER=你的邮箱
EMAIL_PASS=你的邮箱授权码
NEXT_PUBLIC_API_BASE_URL=http://192.168.10.8:18080
NEXT_PUBLIC_UPLOAD_PROXY_URL=http://192.168.10.8:18080
构建并启动容器:
# 方式1:分步全量无缓存构建+启动
docker compose build --no-cache
docker compose up -d
# 方式2:单命令一键构建启动
docker compose up -d --build
查看容器运行状态:
docker compose ps
正常运行状态:
codestory-redis Up
codestory-backend Up
codestory-frontend Up
codestory-nginx Up
验证:访问http://192.168.10.8:18080,检查所有功能是否正常
代码更新部署步骤
✅ 正确步骤:
git pull
docker compose up -d --build
❌ 禁止操作:
禁止执行cp .env.production.example .env,避免覆盖服务器已适配好的环境变量配置。
局部重部署规范
| 修改内容 | 执行命令 | 说明 |
|---|---|---|
| 前端业务代码 | docker compose build --no-cache frontend && docker compose up -d | 前端打包时注入 NEXT_PUBLIC_环境变量,改代码需重建 |
| .env 中 NEXT_PUBLIC_开头变量 | 同上 | 编译阶段固化进前端产物,修改必须重新构建前端 |
| 后端业务代码 | docker compose build --no-cache backend && docker compose up -d | |
| 后端非前端类环境变量(数据库 / 邮箱 / JWT) | 优先:docker compose up -d不生效: docker compose build --no-cache backend && docker compose up -d | 后端运行时读取环境变量,多数场景无需重新 build |
五、长效避坑指南
1. 所有重要配置一定要备份
cp .env .env.server.backup
cp docker-compose.yml docker-compose.server.backup.yml
cp -r nginx nginx.server.backup
万一配置被覆盖了,直接用备份恢复就行:
cp docker-compose.server.backup.yml docker-compose.yml
2. 配置与代码分离
-
把服务器私有配置加入
.gitignore,防止提交到 Git -
用
docker-compose.override.yml保存服务器专属配置(比如 DNS),和通用代码隔离
3. 每次部署前必做检查
# 检查前端环境变量
grep NEXT_PUBLIC .env
# 检查OSS配置
grep OSS_ .env
# 检查DNS配置
docker compose config | grep -A 5 dns
4. 通用部署原则
-
数据库只存相对路径,不存完整域名
-
端口占用优先顺延更换,同步修改所有相关配置
-
最重要的是:保证服务器上的
.env和docker-compose.override.yml不被上传 / 拉取代码覆盖
六、部署常用指令速查
文件操作
# 查看文件
cat .env
nl -ba docker-compose.yml # 带行号
less docker-compose.yml # 分页查看(q退出)
# 编辑文件
nano .env # Ctrl+O保存,Enter确认,Ctrl+X退出
# 复制/备份文件
cp .env .env.server.backup
cp -r nginx nginx.server.backup # 复制整个文件夹
# 查看文件是否存在
ls -la
ls -la .env docker-compose.yml
# 搜索配置
grep NEXT_PUBLIC .env
grep -A 6 dns docker-compose.yml # 匹配行后面再显示6行
Docker 常用
| 命令 | 一句话说明 | 什么时候用 |
|---|---|---|
docker compose up -d | 后台启动所有服务 | 改了数据库 / JWT / 邮箱等后端环境变量后 |
docker compose up -d --build | 重新构建所有镜像再启动 | git pull 拉了新代码后 |
docker compose up -d --build frontend | 只重新构建前端再启动 | 只改了前端代码或 NEXT_PUBLIC_* 变量 |
docker compose up -d --force-recreate frontend | 不重新构建,只强制重启前端容器 | 改了 docker-compose.yml 配置(如 DNS)后 |
docker compose ps | 查看所有服务运行状态 | 部署完第一个执行,看有没有容器挂了 |
docker compose logs -f backend | 实时滚动看后端日志 | 排查正在发生的问题(如上传超时) |
docker compose logs --tail=100 frontend | 看前端最后 100 行日志 | 查历史报错(如图片 500) |
docker compose config | 查看最终生效的完整配置 | 排查配置是否正确(如 DNS 有没有加上) |
docker compose exec backend xxx | 在后端容器里执行命令 | 排查容器内部问题(如域名解析是否正常) |
docker compose up -d
docker compose up -d --build
docker compose up -d --build frontend
docker compose up -d --force-recreate frontend
docker compose ps
docker compose logs -f backend # 实时查看日志
docker compose logs --tail=100 frontend
docker compose config
# 进入容器执行命令
docker compose exec backend nslookup codestory.oss-cn-beijing.aliyuncs.com
网络测试
# 测试域名解析
nslookup codestory.oss-cn-beijing.aliyuncs.com
docker compose exec backend nslookup codestory.oss-cn-beijing.aliyuncs.com
# 测试URL是否能访问
curl -I "https://codestory.oss-cn-beijing.aliyuncs.com/uploads/xxx.jpg"
curl -I "http://127.0.0.1:18080/"
# 测试OSS连接
docker compose exec backend wget -S --spider https://codestory.oss-cn-beijing.aliyuncs.com
Git 常用
# 查看改动
git status --short
git ls-files .env docker-compose.override.yml
# 添加忽略规则
nano .gitignore
# 提交代码
git add .gitignore docker-compose.yml backend/Dockerfile frontend/Dockerfile
git commit -m "fix docker deployment config"
git push
# 撤销暂存,不删除文件
git restore --staged .
七、写在最后
部署从来都不是简单的敲几个命令,而是对整个系统架构的理解和验证。每一次踩坑都是一次成长,当你把所有坑都踩过一遍,你就会成为团队里那个实现过部署的人。
希望这篇文章能让你少走弯路,部署顺利!