技术栈 :Vue 3 + Python/Gunicorn + MySQL + Nginx + Docker Compose
部署环境 :阿里云 Ubuntu
阅读时长:约 8 分钟
前言
最近在阿里云上用 Docker Compose 部署一个前后端分离项目,前端 Vue 打包后丢给 Nginx,后端是 Python + Gunicorn,数据库 MySQL。
本以为是个常规操作,结果连踩三个坑,容器反复 Exited (1),排查了大半天。复盘下来发现这几个问题其实都很典型------尤其是第二个和第三个,属于对 Docker 运行机制理解不到位导致的架构性错误。
记录下来,希望能帮到同样在摸索容器化部署的同学。
坑一:构建产物「人间蒸发」
报错信息
bash
cp: can't stat '/app/dist/*': No such file or directory
现象
执行 docker compose up -d,前端镜像构建阶段直接报错,Nginx 启动失败。
根因
Vue 项目在容器内执行 npm run build 时静默失败了。可能的原因包括:
- 网络超时导致依赖没装全
- Node 版本和项目不兼容
- 代码本身有构建错误(TypeScript 类型报错、ESLint 严格模式拦截等)
构建没成功,/app/dist 目录压根没生成。到了 Dockerfile 的下一个阶段,COPY 或 cp 找不到源文件,直接炸了。
坑在哪? Docker 默认会用缓存,如果之前有一次成功构建的缓存层,后续修改了代码但没清缓存,npm install 那一步可能直接跳过,导致新代码跑在旧依赖上,报错信息还特别迷惑。
解法
shell
# 强制重建,不用缓存,完整输出日志
docker compose build --no-cache frontend
# 盯住输出里的这些关键词
# "ERROR"、"npm ERR!"、"Failed to compile"
看到完整的构建日志后,问题通常一目了然。修完代码再重新构建即可。
坑二:把「构建任务」当成了「服务」
报错信息
vbnet
Container yunji-frontend-builder Exited (1)
service "frontend" didn't complete successfully: exit 1
现象
docker compose build 显示 ✔ Image Built,看起来镜像构建成功了。但 docker compose up 之后容器状态却是 Exited (1)。
更诡异的是,容器名字叫 yunji-frontend-builder------一个前端服务为什么叫 builder?
根因
这是一个对 Docker Compose 运行机制的根本性误解。
当时的 docker-compose.yml 大概长这样(简化版):
yaml
services:
frontend:
build: ./frontend
container_name: yunji-frontend-builder
command: cp -r /app/dist/* /shared/html/
volumes:
- shared-html:/shared/html
nginx:
image: nginx:alpine
volumes:
- shared-html:/usr/share/nginx/html
ports:
- "80:80"
volumes:
shared-html:
思路是:frontend 容器负责构建并把产物复制到共享卷,nginx 容器从共享卷读取文件。
问题在于:Docker Compose 里的每个 service 都是一个长期运行的守护进程。 cp 命令执行完就退出了,进程结束,Docker 检测到主进程退出,判定服务失败,返回 exit 1。
这就好比你雇了个人来当前台,结果他把文件放桌上就走了------老板(Docker)觉得他旷工了。
解法
不要在 docker-compose.yml 里用 command 跑一次性任务。前端服务应该直接运行 Nginx 守护进程。
坑三:架构冗余------一个前端拆成俩容器
现象
docker compose ps -a 一看,出现了两个跟前端相关的容器:
yunji-frontend-builderyunji-nginx
它们通过 volumes 交互:builder 把文件写进共享卷,nginx 从共享卷读。
根因
没用上 Docker 最核心的特性之一------多阶段构建(Multi-stage Build)。
多阶段构建的思路是:在一个 Dockerfile 里定义多个阶段,前面的阶段负责编译/构建,后面的阶段只复制产物,最终镜像里不包含构建工具链,既干净又小。
如果不用多阶段构建,硬要把"构建"和"运行"拆成两个容器,就得靠共享卷来传递文件。这不仅增加了复杂度,还引入了时序依赖(builder 还没跑完,nginx 就启动了怎么办?)、命名混乱、调试困难等一系列问题。
解法
用标准的多阶段构建 Dockerfile,一个服务搞定:
frontend/Dockerfile(修正后):
sql
# ---- 阶段 1:构建 ----
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- 阶段 2:运行 ----
FROM nginx:alpine
# 直接从上一阶段复制产物,不需要共享卷
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose.yml(修正后):
yaml
services:
frontend:
build: ./frontend
image: jizhang-frontend:latest # 明确指定镜像名
container_name: yunji-frontend # 规范命名,不要叫 builder
ports:
- "80:80"
depends_on:
- backend
# 不写 command,让 Dockerfile 的 CMD 生效
# 不挂载 volumes 覆盖 /usr/share/nginx/html
backend:
build: ./backend
image: jizhang-backend:latest
container_name: yunji-backend
ports:
- "8000:8000"
depends_on:
- db
db:
image: mysql:8.0
container_name: yunji-db
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: jizhang
volumes:
- db-data:/var/lib/mysql
volumes:
db-data:
对比修正前后,改动的核心就三点:
- 删掉独立的 nginx 服务------前端自己就是 Nginx
- 删掉
command: cp ...------构建产物在 Dockerfile 里就打包好了 - 删掉共享卷------不需要容器间传文件了
排查命令速查表
遇到类似问题时,按这个顺序排查:
构建失败(dist 目录缺失):
shell
# 强制重建,完整日志
docker compose build --no-cache frontend
# 关注 "ERROR" 和 "npm ERR!"
容器启动后立即退出:
bash
# 查看所有容器状态(包括已停止的)
docker compose ps -a
# 查看容器日志
docker compose logs frontend
# 进容器检查文件是否存在
docker compose run --rm frontend ls -la /usr/share/nginx/html
架构/配置问题:
perl
# 检查 compose 配置结构
cat docker-compose.yml | grep -A 10 "services:"
# 检查本地镜像列表
docker images | grep jizhang
踩坑总结
| 坑 | 表象 | 本质 |
|---|---|---|
| 构建产物缺失 | cp: can't stat '/app/dist/*' |
构建静默失败 + 缓存干扰 |
| 容器秒退 | Exited (1) |
把一次性命令当守护进程 |
| 架构冗余 | 两个容器 + 共享卷 | 没用多阶段构建 |
回头看这三个问题,其实都指向同一个认知盲区:Docker 不只是"把东西装进容器",它有自己的一套关于进程生命周期和镜像分层的设计哲学。 不理解这些,就容易把传统的部署思维(先编译、再拷贝、再启动)原封不动地搬进容器里,造成各种水土不服。
多阶段构建是解决前端容器化部署的标准答案。一个 Dockerfile,两个 FROM,干净利落。
分享 AI、Docker、全栈开发的实战经验,把技术讲成人话。