Skip to content
Go back

Docker部署血泪史

Published:  at  7:00 AM

Docker部署血泪史


前置知识:

是个啥:

它是一个利用集装箱思维,解决环境不一致问题的工具,比虚拟机更清亮。

它的核心玩法就是从仓库中拉取镜像,然后运行成容器

好处:

  1. 开发更加的轻松,不用再一行一行的去配置环境了

  2. 部署更加的高效,一条命令就能立马上线

  3. 环境更加的干净,容器和主机之间不会互相的干扰污染

image.png

镜像:相当于程序的模板,包含了运行程序所需的所有环境和依赖

容器:容器是镜像的运行实例,相当于把程序放入容器中运行,实现环境隔离

仓库:用于存储和分发镜像

工作的原理:

image.png

和虚拟机的区别:

image.png


一、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 等依赖的后置构建脚本。

解决方案:

  1. frontend/pnpm-workspace.yaml里放行构建脚本:
packages:
  - .
allowBuilds:
  sharp: true
  unrs-resolver: true
  1. 修改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 端口。

解决方案:

  1. 修改docker-compose.yml端口映射:
ports:
  - "18080:80"
  1. 同步修改.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 错误、网页解析失败。

排查过程:

  1. 先看浏览器控制台,发现请求地址是http://192.168.10.8.18080

  2. 哦!端口分隔符:被我写成了.

根因:前端环境变量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.ymldocker-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。

排查过程:

  1. 先看前端容器日志,发现还是域名解析失败

  2. 哦!我只给后端配了 DNS,前端容器也需要解析 OSS 域名啊!

修复:给 frontend 服务也加上同样的 DNS 配置。

三、面试官最爱问:你是怎么排查部署问题的?

 “我就是瞎试出来的”,这可不是,我有系统性排查思路。

我会按照从外到内、从前端到后端、从网络到权限的顺序逐层排查,核心思路是先定位问题发生在哪个环节,再深入解决:

  1. 先看容器状态:用docker compose ps看有没有容器在重启或者退出

  2. 再看日志:用docker compose logs --tail=100 服务名找具体的报错信息

  3. 测试网络连通性:

    • curl -I 地址测试 URL 是否能访问

    • nslookup 域名测试域名解析是否正常

    • 进入容器内部执行同样的命令,区分是宿主机问题还是容器问题

  4. 检查配置文件:重点看环境变量、端口映射、DNS 配置

  5. 检查权限:比如 OSS 的访问权限、文件系统的读写权限

部署前我会先想清楚这 10 个问题:

  1. 前端怎么 build?怎么 start?

  2. 后端怎么 build?怎么 start?

  3. 后端需要哪些环境变量?

  4. 数据库是在本机容器里,还是远程?

  5. 用户最终访问哪个地址?

  6. Nginx 要把哪些路径转给前端 / 后端?

  7. 上传文件要不要持久化?

  8. 哪些配置不能提交到 Git?

四、标准化部署流程:从此告别手忙脚乱

把踩过的坑变成标准化流程,下次部署就不会再犯同样的错误了。

首次部署完整步骤

登录服务器:

ssh yuanjing@192.168.10.8 -p 22

进入项目目录:

image.png

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. 配置与代码分离

3. 每次部署前必做检查

# 检查前端环境变量
grep NEXT_PUBLIC .env

# 检查OSS配置
grep OSS_ .env

# 检查DNS配置
docker compose config | grep -A 5 dns

4. 通用部署原则

六、部署常用指令速查

文件操作

# 查看文件
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 .

七、写在最后

部署从来都不是简单的敲几个命令,而是对整个系统架构的理解和验证。每一次踩坑都是一次成长,当你把所有坑都踩过一遍,你就会成为团队里那个实现过部署的人。

希望这篇文章能让你少走弯路,部署顺利!


Suggest Changes
Share this post on:

Next Post
双Token认证原理以及实现的步骤