Archived
vault backup: 2026-04-28 21:55:18
This commit is contained in:
+228
-244
@@ -1,10 +1,44 @@
|
||||
# Dockerfile 编写指南
|
||||
|
||||
> Dockerfile 是**镜像构建的蓝图**,每一行指令都会在镜像中生成一层。写好 Dockerfile 的核心目标:**镜像更小、构建更快、启动更安全**。
|
||||
|
||||
---
|
||||
tags:
|
||||
- docker
|
||||
- devops
|
||||
- containerization
|
||||
create time: 2026-04-28 14:30
|
||||
---
|
||||
|
||||
## 一、Dockerfile 基本结构
|
||||
# Dockerfile 编写指南
|
||||
|
||||
> 一份从"能跑"到"跑得好"的 Dockerfile 教程。通过教学式讲解、流程图和实战示例,帮你写出体积小、构建快、启动安全的镜像。
|
||||
|
||||
## 概述
|
||||
|
||||
Dockerfile 是镜像构建的蓝图,每一行指令都会在镜像中生成一层。写好 Dockerfile 的核心目标:**镜像更小、构建更快、启动更安全**。
|
||||
|
||||
> [!tip] 初学者思考题
|
||||
> 为什么 `RUN apt-get install nginx && rm -rf /var/lib/apt/lists/*` 不能拆成两个 RUN?拆开后镜像体积会不会变小?(答案见正文 2.2 节)
|
||||
|
||||
## 正文
|
||||
|
||||
### 一、Dockerfile 基本结构
|
||||
|
||||
一张图理解 Dockerfile 的构建流程:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["FROM 基础镜像"] --> B["WORKDIR 设工作目录"]
|
||||
B --> C["COPY 依赖文件 + RUN 安装"]
|
||||
C --> D["COPY 源代码"]
|
||||
D --> E["RUN 构建产物"]
|
||||
E --> F["EXPOSE 声明端口"]
|
||||
F --> G["CMD 启动命令"]
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#c8e6c9
|
||||
```
|
||||
|
||||
> [!note] 构建流程
|
||||
> 构建过程是**自上而下逐行执行**的,每一步都基于上一步的结果生成新层。层序排列直接影响缓存命中率和镜像体积。
|
||||
|
||||
一个标准的 Node.js 项目 Dockerfile:
|
||||
|
||||
```dockerfile
|
||||
# 1. 基础镜像
|
||||
@@ -16,14 +50,14 @@ LABEL maintainer="yourname@example.com"
|
||||
# 3. 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 4. 复制依赖文件并安装
|
||||
# 4. 先复制依赖文件,利用 Docker 缓存
|
||||
COPY package*.json ./
|
||||
RUN npm ci --production
|
||||
|
||||
# 5. 复制源代码
|
||||
# 5. 再复制源代码
|
||||
COPY . .
|
||||
|
||||
# 6. 构建(开发环境需要,生产环境不需要)
|
||||
# 6. 构建
|
||||
RUN npm run build
|
||||
|
||||
# 7. 暴露端口
|
||||
@@ -35,167 +69,165 @@ CMD ["node", "dist/index.js"]
|
||||
|
||||
---
|
||||
|
||||
## 二、常用指令详解
|
||||
### 二、常用指令详解
|
||||
|
||||
### 2.1 `FROM` — 基础镜像
|
||||
#### 2.1 `FROM` — 基础镜像
|
||||
|
||||
> [!question] 为什么镜像大小差距这么大?
|
||||
> 完整 OS + 工具链 vs 精简版 = 1GB vs 10MB,这就是 `FROM` 选择的影响。
|
||||
|
||||
**选镜像原则:有 Alpine 选 Alpine,有 Slim 选 Slim,没有再选完整版。**
|
||||
|
||||
```dockerfile
|
||||
# 选镜像的通用原则:
|
||||
# 有 Alpine 选 Alpine(体积小)
|
||||
# 有 Slim 选 Slim(平衡体积和功能)
|
||||
# 没有再选完整版本
|
||||
|
||||
FROM node:20-alpine # 推荐:最小体积
|
||||
FROM node:20-slim # 推荐:平衡方案
|
||||
FROM node:20 # 不推荐:包含完整 OS,镜像臃肿
|
||||
FROM node:20-alpine # 推荐:最小体积 ~150MB
|
||||
FROM node:20-slim # 推荐:平衡方案 ~200MB
|
||||
FROM node:20 # 不推荐:完整 OS,镜像臃肿 ~1GB
|
||||
FROM python:3.12-slim-bookworm
|
||||
FROM nginx:alpine
|
||||
FROM golang:1.22-alpine
|
||||
FROM ubuntu:24.04 # 一般作为多阶段构建的第一步
|
||||
```
|
||||
|
||||
**常见基础镜像体积对比:**
|
||||
|
||||
| 镜像 | 大小 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| 镜像类型 | 大小 | 适用场景 |
|
||||
|----------|------|----------|
|
||||
| `alpine` | 5-10MB | 生产环境、追求极致体积 |
|
||||
| `slim` | 100-200MB | 大多数生产场景,兼容性好 |
|
||||
| 完整版 | 1GB+ | 开发环境、需要完整工具链 |
|
||||
|
||||
### 2.2 `RUN` — 执行命令
|
||||
#### 2.2 `RUN` — 执行命令
|
||||
|
||||
> [!warning] 关键概念:Docker 层不可删
|
||||
> `RUN` 产生的层会**永久留在镜像中**,即使后续 `rm` 了也没用。所以更新、安装、清理必须在**同一个 RUN** 里。
|
||||
|
||||
```dockerfile
|
||||
# 合并 RUN 指令减少镜像层数
|
||||
# 不推荐:每行一个 RUN,产生多层
|
||||
# ❌ 不推荐:每行一个 RUN,产生多层,rm 无效
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nginx
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 推荐:合并为一行,并用 && 连接,用 \ 换行
|
||||
# ✅ 推荐:合并为一行,用 && 连接,用 \ 换行
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 注意:RUN 产生的层会**永久留在镜像中**,即使后续 rm 了也没用
|
||||
# 所以更新和安装、清理要放在同一个 RUN 里
|
||||
```
|
||||
|
||||
### 2.3 `COPY` vs `ADD`
|
||||
#### 2.3 `COPY` vs `ADD`
|
||||
|
||||
> [!abstract] 简单原则
|
||||
> **99% 的情况用 COPY。** ADD 的额外功能(自动解压、远程下载)行为不可预测。
|
||||
|
||||
```dockerfile
|
||||
# COPY — 简单复制,推荐优先使用
|
||||
# COPY — 简单复制,优先使用
|
||||
COPY package.json ./
|
||||
COPY . .
|
||||
|
||||
# ADD — 有额外功能但不可预测,不推荐
|
||||
ADD https://example.com/file.tar.gz /tmp/ # 能下载远程文件,但容易出错
|
||||
ADD file.tar.gz /app/ # 能自动解压,但 COPY + RUN tar 更清晰
|
||||
|
||||
# 结论:99% 的情况用 COPY,ADD 只在自动解压 tar 和下载远程文件时有用
|
||||
# ADD — 仅在两种场景有用
|
||||
ADD https://example.com/file.tar.gz /tmp/ # 下载远程文件(但容易出错)
|
||||
ADD file.tar.gz /app/ # 自动解压(COPY + RUN tar 更清晰)
|
||||
```
|
||||
|
||||
### 2.4 `WORKDIR` — 工作目录
|
||||
#### 2.4 `WORKDIR` — 工作目录
|
||||
|
||||
```dockerfile
|
||||
WORKDIR /app # 设置工作目录
|
||||
COPY package.json ./ # 相对于 /app
|
||||
RUN npm install
|
||||
COPY . . # 复制代码到 /app
|
||||
COPY . .
|
||||
|
||||
# WORKDIR 会被后续指令继承,且会自动创建(不需要提前 mkdir)
|
||||
# WORKDIR 会被后续指令继承,且会自动创建
|
||||
WORKDIR /subdir # 现在是 /app/subdir
|
||||
RUN pwd # 输出 /app/subdir
|
||||
```
|
||||
|
||||
### 2.5 `EXPOSE` — 声明端口
|
||||
#### 2.5 `EXPOSE` — 声明端口
|
||||
|
||||
> [!info] EXPOSE 不自动映射端口
|
||||
> `EXPOSE` 只是**文档性质**的声明,实际端口映射需要在 `docker run -p` 或 `docker-compose` 中指定。
|
||||
|
||||
```dockerfile
|
||||
# 声明容器运行时监听的端口(文档性质,不自动映射)
|
||||
EXPOSE 3000
|
||||
EXPOSE 80 443
|
||||
|
||||
# 实际端口映射需要在 docker run -p 或 docker-compose 中指定
|
||||
```
|
||||
|
||||
### 2.6 `ENV` — 环境变量
|
||||
#### 2.6 `ENV` — 环境变量
|
||||
|
||||
```dockerfile
|
||||
ENV NODE_ENV=production
|
||||
ENV APP_PORT=3000
|
||||
ENV DB_HOST=localhost
|
||||
|
||||
# 容器启动时可以覆盖:
|
||||
# docker run -e DB_HOST=192.168.1.100 my-app
|
||||
# 容器启动时可以覆盖:docker run -e DB_HOST=192.168.1.100 my-app
|
||||
```
|
||||
|
||||
### 2.7 `CMD` vs `ENTRYPOINT`
|
||||
#### 2.7 `CMD` vs `ENTRYPOINT`
|
||||
|
||||
| 指令 | 特点 | 可被 docker run 覆盖? |
|
||||
|------|------|----------------------|
|
||||
| `CMD` | 默认命令,最灵活 | ✅ 完全覆盖 |
|
||||
| `ENTRYPOINT` | 入口点,固定执行 | ⚠️ 需用 `--entrypoint` 强制覆盖 |
|
||||
|
||||
```dockerfile
|
||||
# CMD — 容器启动时执行的默认命令(可被覆盖)
|
||||
# CMD — 大多数情况用这个就够了
|
||||
CMD ["node", "dist/index.js"]
|
||||
# 运行 docker run my-app echo "hello" 会覆盖 CMD
|
||||
|
||||
# ENTRYPOINT — 入口点,不可被 docker run 覆盖(但可用 --entrypoint 强制覆盖)
|
||||
# ENTRYPOINT + CMD — 入口固定,参数可配
|
||||
ENTRYPOINT ["node"]
|
||||
CMD ["dist/index.js"] # 作为 ENTRYPOINT 的参数
|
||||
|
||||
# 实际开发中,大多数情况用 CMD 就够了
|
||||
CMD ["dist/index.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、多阶段构建(关键优化手段)
|
||||
### 三、多阶段构建(核心优化手段)
|
||||
|
||||
### 3.1 为什么需要多阶段构建
|
||||
> [!abstract] 一句话理解多阶段构建
|
||||
> **先用大镜像构建 → 再把产物复制到最小镜像中运行。** 构建工具不留到最终镜像。
|
||||
|
||||
前端项目用 Next.js/Vite + TypeScript 构建,需要 Node.js 全套环境。但运行只需要一个编译后的静态文件。多阶段构建可以**把构建工具留在第一阶段,最终镜像只放产物**。
|
||||
构建流程图:
|
||||
|
||||
### 3.2 前端多阶段构建(Node + TypeScript)
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Phase1["第一阶段:构建(大镜像)"]
|
||||
A["node:20-alpine"] --> B["安装全套依赖 + devDependencies"]
|
||||
B --> C["TypeScript / Maven / Go 编译"]
|
||||
C --> D["生成 dist/ / target/*.jar"]
|
||||
end
|
||||
|
||||
subgraph Phase2["第二阶段:运行(最小镜像)"]
|
||||
E["node:20-alpine / JRE Alpine / Alpine"] --> F["只装 production 依赖"]
|
||||
F --> G["COPY --from=builder 复制产物"]
|
||||
G --> H["启动应用"]
|
||||
end
|
||||
|
||||
Phase1 -->|COPY --from=builder| Phase2
|
||||
|
||||
style Phase1 fill:#fff3e0
|
||||
style Phase2 fill:#e8f5e9
|
||||
```
|
||||
|
||||
#### 3.1 前端多阶段构建(Node + TypeScript)
|
||||
|
||||
```dockerfile
|
||||
# ========== 第一阶段:构建 ==========
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ① 先复制依赖文件,利用 Docker 缓存
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# ② 再复制源码(代码改了才重新构建)
|
||||
COPY . .
|
||||
|
||||
# ③ 构建
|
||||
RUN npm run build
|
||||
RUN npm ci # ① 先装依赖,利用缓存
|
||||
COPY . . # ② 再复制源码,代码改了才重建
|
||||
RUN npm run build # ③ 编译
|
||||
|
||||
# ========== 第二阶段:运行 ==========
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# 安装 tini(处理僵尸进程,可选但推荐)
|
||||
RUN apk add --no-cache tini
|
||||
|
||||
RUN apk add --no-cache tini # tini 处理僵尸进程(推荐)
|
||||
WORKDIR /app
|
||||
|
||||
# 仅复制 production 依赖
|
||||
COPY package*.json ./
|
||||
RUN npm ci --production
|
||||
|
||||
# 从构建阶段复制产物
|
||||
RUN npm ci --production # 只装 production 依赖
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# 使用 tini 作为 PID 1
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
**核心思路:先用大镜像构建 → 再把产物复制到最小镜像中运行。**
|
||||
|
||||
### 3.3 Java 多阶段构建
|
||||
#### 3.2 Java 多阶段构建
|
||||
|
||||
```dockerfile
|
||||
# 第一阶段:Maven 构建
|
||||
@@ -206,7 +238,7 @@ RUN mvn dependency:go-offline -B
|
||||
COPY src ./src
|
||||
RUN mvn package -DskipTests
|
||||
|
||||
# 第二阶段:运行
|
||||
# 第二阶段:运行(JRE 镜像,无 Maven)
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
@@ -214,7 +246,7 @@ EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
```
|
||||
|
||||
### 3.4 Go 多阶段构建
|
||||
#### 3.3 Go 多阶段构建
|
||||
|
||||
```dockerfile
|
||||
# 第一阶段:编译
|
||||
@@ -225,7 +257,7 @@ RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /app/server
|
||||
|
||||
# 第二阶段:运行
|
||||
# 第二阶段:运行(纯 Alpine,仅几 MB)
|
||||
FROM alpine:3.19
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /app/server /app/server
|
||||
@@ -235,9 +267,10 @@ CMD ["/app/server"]
|
||||
|
||||
---
|
||||
|
||||
## 四、.dockerignore 文件
|
||||
### 四、.dockerignore 文件
|
||||
|
||||
> **和 `.gitignore` 一样重要!** 不写 `.dockerignore` 会导致不必要的文件被复制到镜像中,增大体积、暴露敏感信息。
|
||||
> [!danger] 和 .gitignore 一样重要!
|
||||
> 不写 `.dockerignore` 会把不必要的文件塞进镜像,增大体积、暴露密钥。
|
||||
|
||||
```dockerignore
|
||||
# 依赖
|
||||
@@ -248,26 +281,23 @@ npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# 构建产物(别把 build 直接拷进去,让容器自己 build)
|
||||
# 构建产物(别把 build 拷进去,让容器自己 build)
|
||||
dist
|
||||
build
|
||||
*.local
|
||||
|
||||
# 编辑器
|
||||
# 编辑器和文档
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
|
||||
# Docker 自身
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# 文档和测试
|
||||
README.md
|
||||
LICENSE
|
||||
tests
|
||||
__tests__
|
||||
|
||||
# Docker 自身文件
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# 环境变量(千万不要拷进去!)
|
||||
.env
|
||||
@@ -277,9 +307,9 @@ __tests__
|
||||
|
||||
---
|
||||
|
||||
## 五、最佳实践清单
|
||||
### 五、最佳实践清单
|
||||
|
||||
### ✅ 应该做
|
||||
#### ✅ 应该做
|
||||
|
||||
| 实践 | 说明 |
|
||||
|------|------|
|
||||
@@ -292,7 +322,7 @@ __tests__
|
||||
| 非 root 用户运行 | 安全考虑 |
|
||||
| 用 `CMD ["exec格式"]` | 避免 shell 包装问题 |
|
||||
|
||||
### ❌ 不应该做
|
||||
#### ❌ 不应该做
|
||||
|
||||
| 实践 | 问题 |
|
||||
|------|------|
|
||||
@@ -305,58 +335,117 @@ __tests__
|
||||
|
||||
---
|
||||
|
||||
## 六、非 Root 用户示例
|
||||
### 六、安全加固
|
||||
|
||||
#### 6.1 使用非 root 用户
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
# 利用 --chown 设置文件所有者,避免切换用户后权限问题
|
||||
COPY --chown=node:node package*.json ./
|
||||
RUN npm ci
|
||||
COPY --chown=node:node . .
|
||||
|
||||
COPY . .
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -S appgroup && \
|
||||
adduser -S appuser -G appgroup
|
||||
|
||||
# 切换用户
|
||||
USER appuser
|
||||
# 切换到非 root 用户
|
||||
USER node
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
#### 6.2 不要安装 build 依赖到生产镜像
|
||||
|
||||
```dockerfile
|
||||
# ❌ 错误:devDependencies 也被装进镜像
|
||||
RUN npm install
|
||||
|
||||
# ✅ 正确:多阶段构建
|
||||
FROM node:20-alpine AS builder
|
||||
RUN npm install # 包含 devDependencies
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
RUN npm ci --production # 只装 production 依赖
|
||||
COPY --from=builder /app/dist ./dist
|
||||
```
|
||||
|
||||
#### 6.3 密钥管理原则
|
||||
|
||||
```dockerfile
|
||||
# ❌ 绝对不要硬编码密码
|
||||
RUN echo "password=123456" > /app/config.env
|
||||
|
||||
# ❌ 不要在构建时传递密钥(会留在镜像层中)
|
||||
# docker build --build-arg DB_PASSWORD=secret .
|
||||
|
||||
# ✅ 运行时传入环境变量
|
||||
# docker run -e DB_PASSWORD=secret my-app
|
||||
|
||||
# ✅ 生产环境用 Kubernetes Secrets / AWS Secrets Manager
|
||||
```
|
||||
|
||||
#### 6.4 扫描镜像漏洞
|
||||
|
||||
```bash
|
||||
# 使用 trivy 扫描镜像
|
||||
trivy image my-app:v1.0
|
||||
|
||||
# 只输出高危及以上
|
||||
trivy image --severity HIGH,CRITICAL my-app:v1.0
|
||||
|
||||
# 直接扫描 Dockerfile(不用构建镜像)
|
||||
trivy config Dockerfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、常见问题排查
|
||||
### 七、构建缓存优化
|
||||
|
||||
### 7.1 镜像太大
|
||||
> [!tip] 缓存命中规则
|
||||
> Docker 从上到下逐层检查缓存。**任何一行变更,后续所有层全部失效重构建。** 所以要把经常变的内容放下面。
|
||||
|
||||
```dockerfile
|
||||
# ❌ 错误:代码改动 → npm install 也重新执行
|
||||
COPY . .
|
||||
RUN npm install
|
||||
|
||||
# ✅ 正确:先复制不变的依赖文件
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
```
|
||||
|
||||
构建缓存策略流程图:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["package.json 未变"] -->|"缓存命中"| B["跳过 npm install"]
|
||||
C["package.json 变更"] -->|"缓存失效"| D["重新 npm install"]
|
||||
E["源码变更"] --> B
|
||||
B --> F["最终镜像"]
|
||||
D --> F
|
||||
|
||||
style A fill:#c8e6c9
|
||||
style C fill:#fff3e0
|
||||
style E fill:#fff3e0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 八、常见问题排查
|
||||
|
||||
#### 8.1 镜像太大
|
||||
|
||||
```bash
|
||||
# 分析镜像各层大小
|
||||
docker history my-app:latest
|
||||
|
||||
# 检查是否有未清理的缓存
|
||||
# 解决:使用 alpine/slim 基础镜像 + 多阶段构建 + .dockerignore
|
||||
# 解决:alpine 基础镜像 + 多阶段构建 + .dockerignore
|
||||
```
|
||||
|
||||
### 7.2 构建缓存失效
|
||||
|
||||
```dockerfile
|
||||
# 错误:代码改动导致 npm install 也重新执行
|
||||
COPY . .
|
||||
RUN npm install
|
||||
|
||||
# 正确:依赖文件不变时跳过 npm install
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
```
|
||||
|
||||
### 7.3 容器启动后立即退出
|
||||
#### 8.2 容器启动后立即退出
|
||||
|
||||
```bash
|
||||
# 查看日志
|
||||
@@ -364,128 +453,23 @@ docker logs <容器名>
|
||||
|
||||
# 常见原因:
|
||||
# 1. CMD/ENTRYPOINT 写错了
|
||||
# 2. 主进程不是 PID 1(用 tini 或 dumb-init 处理)
|
||||
# 2. 主进程不是 PID 1(用 tini 或 dumb-init)
|
||||
# 3. 应用启动失败(检查日志)
|
||||
```
|
||||
|
||||
### 7.4 端口无法访问
|
||||
#### 8.3 端口无法访问
|
||||
|
||||
```bash
|
||||
# 1. 检查 docker run -p 是否映射
|
||||
docker run -p 3000:3000 ...
|
||||
|
||||
# 2. 检查容器内应用是否监听 0.0.0.0(不是 localhost)
|
||||
# 2. 容器内应用监听 0.0.0.0(不是 localhost)
|
||||
# 3. 检查防火墙
|
||||
|
||||
# 4. 用 docker-compose 时,确认 services 定义正确
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、Docker 安全加固
|
||||
## 关联笔记
|
||||
|
||||
### 7.1 使用非 root 用户
|
||||
|
||||
```dockerfile
|
||||
# 最简方式(Node 官方镜像已内置 node 用户)
|
||||
FROM node:20-alpine
|
||||
USER node
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
# 手动创建用户(通用模式)
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --chown=node:node package*.json ./
|
||||
RUN npm ci
|
||||
COPY --chown=node:node . .
|
||||
USER node
|
||||
CMD ["node", "dist/index.js"]
|
||||
```
|
||||
|
||||
> `--chown=user:group` 在 COPY/RUN 时设置文件所有者,**避免 root 拥有文件**后切换到非 root 用户导致权限问题。
|
||||
|
||||
### 7.2 不要安装 build 依赖到生产镜像
|
||||
|
||||
```dockerfile
|
||||
# 错误:所有依赖都装进去了
|
||||
FROM node:20-alpine
|
||||
RUN npm install # devDependencies 也被安装了
|
||||
|
||||
# 正确:生产环境只装 dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 更正确:多阶段构建
|
||||
FROM node:20-alpine AS builder
|
||||
RUN npm install # 包含 devDependencies
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
RUN npm ci --only=production
|
||||
COPY --from=builder /app/dist ./dist
|
||||
```
|
||||
|
||||
### 7.3 扫描镜像漏洞
|
||||
|
||||
```bash
|
||||
# 使用 trivy(推荐)
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
aquasec/trivy image nginx:latest
|
||||
|
||||
# 扫描指定镜像
|
||||
trivy image my-app:v1.0
|
||||
|
||||
# 只输出高危及以上
|
||||
trivy image --severity HIGH,CRITICAL my-app:v1.0
|
||||
|
||||
# 扫描 Dockerfile 本身(不用构建镜像)
|
||||
trivy config Dockerfile
|
||||
```
|
||||
|
||||
### 7.4 密钥管理原则
|
||||
|
||||
```dockerfile
|
||||
# ❌ 绝对不要硬编码密码
|
||||
RUN echo "password=123456" > /app/config.env
|
||||
|
||||
# ❌ 不要在构建时传递密钥(会留在镜像层中)
|
||||
docker build --build-arg DB_PASSWORD=secret .
|
||||
|
||||
# ✅ 运行时传入环境变量
|
||||
docker run -e DB_PASSWORD=secret my-app
|
||||
|
||||
# ✅ 或使用 Docker secrets(Swarm 模式)
|
||||
# ✅ 生产环境用 Kubernetes Secrets / AWS Secrets Manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、实战:Node.js + Express 完整 Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY . .
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
RUN addgroup -S app && adduser -S app -G app
|
||||
USER app
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/server.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> 下一步:理解 Dockerfile 后,学习 [[docker-compose]] 来编排多容器应用。
|
||||
- [[docker-compose]] — 用 Compose 编排多容器应用
|
||||
- [[Docker常用命令]] — 日常 Docker 操作速查
|
||||
|
||||
Reference in New Issue
Block a user