文章

Docker部署深度解析

解决"在我机器上能跑"的终极武器:镜像与容器是什么、多阶段构建怎么把 1GB 镜像压到 30MB、Docker Compose 怎么一键拉起前后端全家桶,以及 CMD/ENTRYPOINT 等易错点。

Docker部署深度解析

一句话概括

Docker 把应用连同它的运行环境打包成一个镜像,到任何装了 Docker 的机器上跑出来都一模一样——彻底干掉”我本地是好的”这种甩锅。对前端来说,它是把 Vue/React 构建产物、Node 服务、Nginx、Redis 一股脑编排起来的部署利器。

面试不考你背命令,而考镜像分层与缓存、多阶段构建瘦身、容器和虚拟机的本质区别、CMD 与 ENTRYPOINT 怎么配合。讲清这几个,你就不只是”用过 docker-compose up”。

核心知识点

1. 镜像、容器、Dockerfile:三个核心概念

  • 镜像(Image):只读模板,包含代码 + 运行时 + 依赖,相当于”装机盘”
  • 容器(Container):镜像跑起来的实例,互相隔离,一个镜像可起多个容器
  • Dockerfile:构建镜像的”配方”,逐行指令叠成镜像层

最小可用:把一个静态站点丢进 Nginx 镜像。

1
2
3
4
5
FROM nginx:stable-alpine
COPY ./dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
docker build -t my-app:v1 .            # 构建
docker run -d -p 8080:80 --name app my-app:v1   # 跑容器
docker ps                                # 看运行状态
docker stop app && docker rm app         # 停并删

2. 多阶段构建:把 1GB 镜像压到 30MB

前端构建需要 Node + node_modules,但运行时只需要静态文件。多阶段构建用 AS builder 专门编译,再把产物拷进干净的小镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ===== 构建阶段 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci                  # 构建需要 vite/tsc 等 devDependencies,必须全量装
COPY . .
RUN npm run build

# ===== 运行阶段(只留产物) =====
FROM nginx:stable-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

⚠️ 常见错误:在构建阶段写 npm ci --omit=dev(原 --only=production)——那样 devDependencies 装不上,vite/tsc 直接缺失、构建失败。构建阶段要全量装,瘦身靠”换运行阶段”,不是靠构建阶段少装。

对比单阶段多阶段
最终镜像1GB+(含 node_modules)~30MB(仅 Nginx + 静态文件)
安全风险高(工具一堆)低(只留运行时)
缓存利用差好(依赖层可复用)

3. Node 后端容器化:非 root + 信号转发

后端容器要跑得稳,两个关键:用非 root 用户(被攻破也拿不到宿主 root)、用 tini 当 PID 1(正确转发 SIGTERM,docker stop 才能优雅退出)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev          # 运行只需生产依赖

FROM node:20-alpine
RUN apk add --no-cache tini    # 轻量 init,处理信号
RUN addgroup -S nodejs && adduser -S nodejs   # 建非 root 用户
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
USER nodejs                    # 降权运行
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]

4. Docker Compose:一键拉起全家桶

多服务(前端 + 后端 + Redis + 数据库)用 Compose 一个文件编排。注意 Compose v2 已把 version 字段标记为 obsolete(会告警),现代写法直接省略,按实际用到的功能来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# docker-compose.yml(Compose v2:无需 version 字段)
services:
  frontend:
    build: ./frontend
    ports: ["80:80"]
    depends_on: [api]
    restart: unless-stopped

  api:
    build: ./backend
    expose: ["3000"]
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
    depends_on: [redis]
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes: ["redis_data:/data"]
    restart: unless-stopped

volumes:
  redis_data:
1
2
3
4
docker compose up -d --build   # 构建并后台启动
docker compose ps              # 看状态
docker compose logs -f api     # 跟 api 日志
docker compose down            # 停并删容器/网络

5. 镜像分层与缓存:指令顺序有讲究

镜像是一层层叠的,每层是上一层的增量。Docker 会缓存没变的层——所以不常变的放前面、常变的放后面,最大化复用:

1
2
3
4
COPY package.json package-lock.json ./   # 依赖文件先拷
RUN npm ci                                # 只有依赖变了才重装
COPY . .                                  # 源码常变,放后面
RUN npm run build

配 .dockerignore 排除 node_modules、.git、.env 等,避免它们被塞进构建上下文拖慢速度。

其实你每天都在用

  • 本地起一套依赖:docker compose up 一键拉起 MySQL + Redis,不用本机装环境
  • 统一团队环境:新人 clone 下来直接跑,告别”你环境没配好”
  • 前端构建产物托管:多阶段构建把 dist 丢进 Nginx 镜像,部署即静态服务
  • CI/CD 出镜像:Git 推上去 → 流水线 docker build + push 到镜像仓库
  • 多项目隔离:同一台机器跑多个前端站点,各占各的容器互不打架
  • 本地调试后端:把本地 Node 服务连到容器里的 Redis,环境一致
  • 回滚版本:镜像打 v1.2.3 标签,出问题直接 docker run 旧标签秒回退

常见误解(FAQ)

❌ 误区一:”Docker 容器就是轻量虚拟机”

本质不同。虚拟机跑完整 Guest OS,靠 Hypervisor 虚拟化硬件,GB 级、分钟级启动;容器共享宿主内核,只隔离进程/网络/文件系统(namespace + cgroups),MB 级、秒级启动。所以容器是”进程级隔离”,不是”操作系统级隔离”,性能损耗几乎为零。

❌ 误区二:”CMD 和 ENTRYPOINT 随便用一个就行”

分工明确:RUN 是构建时执行(装依赖、编译);CMD 是容器启动时默认命令,可被 docker run 后面的参数覆盖;ENTRYPOINT 是固定入口,一般不覆盖。常见组合是 ENTRYPOINT 定死启动器、CMD 给默认参数:

1
2
ENTRYPOINT ["/sbin/tini", "--"]   # 固定:用 tini 兜底信号
CMD ["node", "dist/server.js"]    # 可被 docker run 镜像 node other.js 覆盖

❌ 误区三:”镜像越小越好,所以 npm install 用 –no-cache”

两个坑:① npm ci 根本没有 --no-cache 选项(那是 docker build --no-cache 的),写上去会报错;正确是用 npm ci(基于 lockfile、干净可复现)代替 npm install。② 真正瘦身靠多阶段构建 + alpine 基础镜像 + 运行阶段 --omit=dev,而不是在构建阶段少装依赖——构建阶段少装会导致编译失败。

❌ 误区四:”docker-compose.yml 顶部必须写 version: ‘3.8’“

那是 Compose v1 的写法。Compose v2 已把 version 标记为 obsolete(运行会告警,但兼容),现代 Compose 文件直接省略 version,按实际功能声明即可。留着不报错,但属于过时写法,新项目建议去掉。

一句话总结

Docker 的价值就一句:把”环境”也变成了可版本化、可复制的代码——多阶段构建负责瘦身、Compose 负责编排、CMD/ENTRYPOINT 负责定义行为,吃透这三点,”在我机器上能跑”这句话就可以从你的词汇表里删掉了。

本文由作者按照 CC BY 4.0 进行授权