目录
- [Dockerfile 深入](#Dockerfile 深入)
- 镜像原理与分层结构
- 容器运行机制
- 网络深入
- 存储与数据管理
- [Docker Compose 进阶](#Docker Compose 进阶)
- 安全最佳实践
- [CI/CD 生产部署流程](#CI/CD 生产部署流程)
一、Dockerfile 深入
1.1 完整指令参考
dockerfile
# ── 基础 ──────────────────────────────────────────────
FROM node:18-alpine # 基础镜像,推荐用 alpine 变种(体积小)
ARG NODE_ENV=production # 构建时参数,docker build --build-arg 传入
ENV APP_PORT=3000 # 运行时环境变量,容器内可读取
# ── 文件系统 ──────────────────────────────────────────
WORKDIR /app # 设置工作目录(不存在会自动创建)
COPY package*.json ./ # 复制文件(推荐 COPY,不要用 ADD)
ADD archive.tar.gz /data/ # ADD 额外支持自动解压 tar 和远程 URL
RUN npm ci --only=production # 执行命令(每条 RUN 生成一个新层)
# ── 元数据 ────────────────────────────────────────────
LABEL maintainer="you@example.com"
EXPOSE 3000 # 声明端口(文档作用,不会真正开放)
VOLUME ["/app/data"] # 声明挂载点
# ── 用户 ──────────────────────────────────────────────
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser # 切换到非 root 用户运行(安全实践)
# ── 启动 ──────────────────────────────────────────────
ENTRYPOINT ["node"] # 固定入口,不能被 docker run 末尾参数覆盖
CMD ["server.js"] # 默认参数,可被 docker run 末尾参数覆盖
ENTRYPOINT vs CMD 对比:
ENTRYPOINT |
CMD |
|
|---|---|---|
| 作用 | 固定入口命令 | 默认参数 |
| 能否覆盖 | 需要 --entrypoint 才能覆盖 |
docker run 末尾参数直接覆盖 |
| 组合使用 | ENTRYPOINT ["node"] + CMD ["app.js"] → 执行 node app.js |
--- |
| 推荐场景 | 容器作为可执行程序时 | 提供灵活默认参数时 |
1.2 多阶段构建(Multi-stage Build)
架构说明:

多阶段构建将构建环境和运行环境完全分离。整个过程分两个阶段:
阶段 1(Builder) 阶段 2(Runner)
───────────────── ────────────────
FROM node:18(~1GB) FROM node:18-alpine(~50MB)
↓ ↓
npm install COPY --from=builder /app/dist
↓ ↓
npm run build 仅保留运行产物
↓ 最终镜像 ~150MB(减少 85%)
生成 dist/ 产物
(编译器、源码、dev依赖留在这里,
不会出现在最终镜像中)
不用多阶段构建(臃肿):
dockerfile
FROM node:18 # 完整镜像,~1GB
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "dist/server.js"]
# 最终镜像包含所有构建工具,非常臃肿
使用多阶段构建(精简):
dockerfile
# ── 阶段 1:构建 ──────────────────────────────────────
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── 阶段 2:运行 ──────────────────────────────────────
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Go 语言极致优化(最终镜像几乎为零):
dockerfile
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch # 空镜像!
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
# 最终镜像只有几 MB
1.3 .dockerignore
.dockerignore 告诉 Docker 哪些文件不要打包进镜像,类似 .gitignore。
# .dockerignore
node_modules/ # 不复制本地 node_modules,镜像内会重新安装
.git/ # git 历史记录
*.log # 日志文件
.env # 敏感配置,绝对不能打包进镜像!
dist/ # 多阶段构建时本地 dist 不需要
coverage/
.DS_Store
为什么重要:
- 减少构建上下文大小,加快构建速度
- 避免
.env等敏感文件意外打包进镜像
1.4 构建缓存优化
Docker 按层缓存,层不变则复用,不重新执行。关键原则:把变化少的放前面,变化多的放后面。
dockerfile
# ❌ 低效:代码改动 → npm install 重新执行
COPY . .
RUN npm install
# ✅ 高效:利用层缓存
COPY package*.json ./ # ① 先只复制依赖描述文件(改动少)
RUN npm ci # ② 安装依赖(package.json 不变则命中缓存)
COPY . . # ③ 再复制源码(源码改动不影响上面两层)
缓存失效规则:
- 某一层发生变化,其后所有层缓存全部失效
COPY/ADD对比文件内容,文件变了才失效RUN只对比指令字符串,指令不变则命中缓存
二、镜像原理与分层结构
2.1 联合文件系统(UnionFS)------ 架构说明

分层结构示意:
┌────────────────────────────────────────┐
│ 可写层(Container Layer) │ ← 每个容器独享,停止后丢失
├────────────────────────────────────────┤ ← 分界线(以下全部只读)
│ Layer 4: COPY . .(应用代码) │
├────────────────────────────────────────┤
│ Layer 3: RUN npm install(依赖) │
├────────────────────────────────────────┤
│ Layer 2: WORKDIR /app │
├────────────────────────────────────────┤
│ Layer 1: FROM node:18-alpine(基础层) │ ← ~50MB,可被多个镜像共享
└────────────────────────────────────────┘
叠加方向 ↓
工作原理:
- 镜像由多个只读层叠加,每层记录相对上一层的变更(增删改)
- 容器启动时,Docker 在镜像层顶部加一个可写层
- 读文件:从上往下找,找到为止(上层覆盖下层)
- 写文件:写到可写层,不修改下面的只读层(Copy-on-Write)
- 删文件:在可写层写入"删除标记",原文件仍在下层,只是被遮蔽
层共享的价值:
- 两个镜像都基于
node:18-alpine,这层在磁盘只存一份 docker pull时已有的层直接跳过,只下载新增的层
2.2 镜像体积优化技巧
dockerfile
# 技巧 1:选用 alpine / slim 基础镜像
FROM node:18-alpine # ~50MB,而非 node:18 的 ~1GB
# 技巧 2:合并 RUN 指令,减少层数
# ❌ 三层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ 一层,且同步清理缓存
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# 技巧 3:在同一层内完成安装和清理
# ❌ 错误:Layer N 删除的文件在 Layer N-1 仍然存在,镜像体积不变
RUN apt-get install -y build-essential # Layer 2
RUN rm -rf /usr/share/doc # Layer 3,无效
# ✅ 正确:同一层内完成
RUN apt-get install -y build-essential \
&& rm -rf /usr/share/doc /var/lib/apt/lists/*
# 技巧 4:只安装生产依赖
RUN npm ci --only=production
# 技巧 5:使用多阶段构建(见 1.2 节)
2.3 常用镜像管理命令
bash
# 查看镜像层历史(每层大小、命令)
docker history myapp:v1
# 查看镜像详细元数据
docker inspect myapp:v1
# 打新标签
docker tag myapp:v1 myuser/myapp:v1
docker tag myapp:v1 myapp:latest
# 离线传输:导出 / 导入
docker save myapp:v1 | gzip > myapp.tar.gz
docker load < myapp.tar.gz
# 清理
docker image prune # 清理悬空镜像(无标签的旧层)
docker image prune -a # 清理所有未被容器使用的镜像
三、容器运行机制
3.1 容器生命周期------ 状态机说明

完整状态转换:
┌─────────────┐
│ Created │ docker create / docker run
└──────┬──────┘
│ docker start / docker run
↓
┌──── Running ────┐
docker pause │ │ docker stop(SIGTERM → SIGKILL)
↓ │ docker kill(直接 SIGKILL)
Paused ↓
│ Stopped ──→ Removed(docker rm)
docker unpause│ │
└──→ Running ←────┘ docker start / docker restart
(继续)
关键区别:
docker stop:发送SIGTERM,等待应用优雅退出(默认 10 秒),超时再SIGKILLdocker kill:直接发送SIGKILL,立即强制终止,可能丢失数据docker pause:冻结容器进程(SIGSTOP),不释放资源,恢复后继续执行
3.2 容器资源限制
不限制资源的容器可能耗尽宿主机所有 CPU/内存,生产环境必须设置:
bash
# 内存限制(超出会被 OOM Kill)
docker run -m 512m myapp
# CPU 限制(最多使用 1 个核心)
docker run --cpus="1.0" myapp
# 综合生产配置
docker run -d \
--name web \
-m 512m \
--memory-swap 512m \ # 与内存相同 = 禁用 swap
--cpus="0.5" \
--restart unless-stopped \
-p 8080:3000 \
myapp:v1
3.3 容器日志管理
bash
docker logs web # 全部日志
docker logs -f web # 实时跟踪(tail -f)
docker logs --tail 100 web # 最后 100 行
docker logs -t web # 带时间戳
docker logs --since 2h web # 最近 2 小时
docker logs --since "2026-03-01" web
生产环境限制日志大小(防止撑满磁盘):
json
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
3.4 进入容器调试
bash
# 进入容器 shell(最常用)
docker exec -it web bash # bash(ubuntu/debian)
docker exec -it web sh # sh(alpine)
# 以 root 身份进入(容器内用 root 排查问题)
docker exec -it -u root web bash
# 执行单条命令不进入
docker exec web cat /app/config.json
docker exec web ps aux
# 用 netshoot 排查网络问题(共享目标容器网络命名空间)
docker run -it --network container:web nicolaka/netshoot
四、网络深入
4.1 网络驱动类型
| 驱动 | 说明 | 适用场景 |
|---|---|---|
bridge(默认) |
虚拟网桥,容器通过 NAT 访问外网 | 单机多容器 |
host |
共享宿主机网络,无隔离 | 高性能场景(慎用) |
overlay |
跨主机容器通信 | Docker Swarm / 多机集群 |
macvlan |
容器直接获得 MAC 地址 | 需要接入物理网络时 |
none |
无网络,完全隔离 | 离线计算任务 |
4.2 自定义 Bridge 网络------ 架构说明

默认 bridge vs 自定义 bridge 的核心差别:
默认 bridge 网络:
容器 A(172.17.0.2)──→ 只能用 IP 访问容器 B(172.17.0.3)
IP 会变,不可靠
自定义 bridge 网络(推荐):
┌─── app-net(内置 DNS 服务器)───────────────┐
│ │
│ web ──→ db:3306 (用容器名,稳定可靠) │
│ web ──→ cache:6379 │
│ │
└──────────────────────────────────────────────┘
宿主机端口映射:-p 8080:3000(仅 web 对外暴露)
db / cache 不暴露端口,外部无法直接访问(安全)
bash
# 创建自定义网络
docker network create --driver bridge app-net
# 启动容器加入网络
docker run -d --name db --network app-net mysql:8
docker run -d --name cache --network app-net redis:7-alpine
docker run -d --name web --network app-net \
-e DB_HOST=db -e REDIS_HOST=cache \
-p 8080:3000 myapp:v1
# 同一网络内可用容器名直接访问,无需 IP
# 查看网络信息
docker network ls
docker network inspect app-net
# 将已运行容器加入/离开网络
docker network connect app-net existing-container
docker network disconnect app-net existing-container
4.3 端口映射详解
bash
docker run -p 8080:80 nginx # 宿主机 8080 → 容器 80
docker run -p 127.0.0.1:8080:80 nginx # 只绑本地,不对外暴露
docker run -p 80:80 -p 443:443 nginx # 映射多个端口
docker port web # 查看端口映射
生产最佳实践: Web 服务绑定 127.0.0.1,由 nginx 做反向代理对外暴露,数据库等服务完全不映射端口。
五、存储与数据管理
5.1 三种挂载方式------ 架构说明

Container(容器)
│
┌────────┼────────┐
↓ ↓ ↓
Named Bind tmpfs
Volume Mount Mount
│ │ │
Docker 宿主机 宿主机
管理目录 任意目录 内存
│ │ │
持久化 开发同步 临时/敏感
数据库 源码热更新 不写磁盘
| 类型 | 存储位置 | 容器删除后数据 | 推荐用途 |
|---|---|---|---|
| Named Volume | /var/lib/docker/volumes/ |
保留 | 数据库、持久化应用数据 |
| Bind Mount | 宿主机任意目录 | 保留(宿主机上) | 开发时挂载源码 |
| tmpfs | 宿主机内存 | 消失 | 敏感临时数据(不落盘) |
5.2 Named Volume(命名卷)
bash
docker volume create mysql-data
docker volume ls
docker volume inspect mysql-data # 查看实际存储路径
docker run -d \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8
docker volume rm mysql-data # 删除(数据丢失!)
docker volume prune # 清理未使用的 volume
5.3 Bind Mount
bash
# 开发时挂载本地源码(容器内改动实时同步)
docker run -v $(pwd):/app myapp
# 只读挂载(容器不能修改配置文件)
docker run -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx
5.4 数据备份与迁移
bash
# 备份 volume 数据
docker run --rm \
-v mysql-data:/data \
-v $(pwd):/backup \
alpine \
tar czf /backup/mysql-backup.tar.gz -C /data .
# 恢复备份
docker run --rm \
-v mysql-data:/data \
-v $(pwd):/backup \
alpine \
tar xzf /backup/mysql-backup.tar.gz -C /data
六、Docker Compose 进阶
6.1 服务依赖与拓扑------ 架构说明

典型多服务依赖关系:
外部流量
│ :80/:443
↓
nginx(反向代理 / SSL 终止)
│ :3000
↓
web(node.js 应用)
│ DB_HOST=db │ REDIS_HOST=cache
↓ ↓
db(mysql) cache(redis)
│
Volume: db-data(持久化)
depends_on 顺序:db 和 cache 先启动,web 再启动,nginx 最后
healthcheck:等待 db 真正就绪(能响应连接),而非仅仅进程存在
6.2 完整 compose 文件结构
yaml
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile.prod
args:
NODE_ENV: production
ports:
- "8080:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
env_file:
- .env # 从文件加载环境变量(不提交到 git)
depends_on:
db:
condition: service_healthy # 等待 db 健康检查通过再启动
volumes:
- ./logs:/app/logs
networks:
- app-net
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # 从 .env 读取
MYSQL_DATABASE: myapp
volumes:
- db-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- web
networks:
- app-net
networks:
app-net:
driver: bridge
volumes:
db-data:
driver: local
6.3 多环境配置
bash
# 文件结构
docker-compose.yml # 基础配置(所有环境共用)
docker-compose.dev.yml # 开发环境覆盖
docker-compose.prod.yml # 生产环境覆盖
yaml
# docker-compose.dev.yml
services:
web:
build:
target: builder # 使用含调试工具的构建阶段
volumes:
- .:/app # 挂载源码,热更新
environment:
- NODE_ENV=development
command: npm run dev
db:
ports:
- "3306:3306" # 开发时暴露数据库端口,方便本地连接
bash
# 开发
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# 生产
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
6.4 常用 Compose 命令
bash
docker compose up -d --build # 后台启动,强制重新构建
docker compose up -d --scale web=3 # 扩容到 3 个 web 实例
docker compose up -d --no-deps --build web # 只重建并更新 web 服务
docker compose restart web # 重启某服务
docker compose logs -f web # 实时日志
docker compose exec web bash # 进入容器
docker compose exec db mysql -u root -p
docker compose ps # 查看状态
docker compose down # 停止并删除容器和网络(保留 volume)
docker compose down -v # 同上,同时删除 volume(数据丢失!)
七、安全最佳实践
7.1 不以 root 运行容器
dockerfile
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . .
USER appuser
7.2 只读文件系统 + 最小权限
bash
docker run \
--read-only \ # 文件系统只读
--tmpfs /tmp \ # /tmp 允许写(内存)
-v app-data:/app/data \ # 数据目录允许写
--cap-drop ALL \ # 删除所有 Linux capabilities
--cap-add NET_BIND_SERVICE \ # 只允许绑定低位端口
--security-opt no-new-privileges \ # 禁止容器内提权
myapp
7.3 镜像安全扫描
bash
# Trivy(开源,推荐)
trivy image myapp:v1
trivy image --severity HIGH,CRITICAL myapp:v1
# Docker 内置
docker scout cves myapp:v1
7.4 敏感信息管理
绝对不能做:
dockerfile
# ❌ 密钥写进 Dockerfile
ENV DB_PASSWORD=mysecret
# ❌ .env 文件打包进镜像
COPY .env .
# ❌ 私钥放进镜像
COPY private.key /app/certs/
推荐做法:
bash
# 运行时注入环境变量(比构建时写入安全)
docker run -e DB_PASSWORD="$SECRET" myapp
# 生产推荐:使用外部密钥管理系统
# AWS Secrets Manager / HashiCorp Vault / Kubernetes Secrets
# 容器启动时从外部拉取,不落进镜像
八、CI/CD 生产部署流程
8.1 完整流程说明

CI 阶段(代码提交自动触发):
git push
↓
CI 系统触发(GitHub Actions / GitLab CI)
↓
运行测试 + lint 检查
↓
docker build(利用层缓存加速)
↓
镜像安全扫描(Trivy)
↓
docker push → Registry(tag 用 git commit hash)
CD 阶段(手动或自动触发):
从 Registry 拉取新镜像
↓
停止旧容器(docker stop,给 30s 优雅退出)
↓
启动新容器(带资源限制 + 健康检查)
↓
健康检查通过 → 流量切入
↓
(失败 → 自动回滚到上一个 tag)
8.2 GitHub Actions 完整示例
yaml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
myuser/myapp:latest
myuser/myapp:${{ github.sha }}
cache-from: type=gha # 利用 GitHub Actions 缓存加速
cache-to: type=gha,mode=max
8.3 生产容器启动模板
bash
docker run -d \
--name web \
--restart unless-stopped \ # 自动重启
-m 512m \ # 内存上限
--memory-swap 512m \ # 禁用 swap
--cpus="1.0" \ # CPU 上限
--cap-drop ALL \ # 最小权限
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--read-only \ # 只读文件系统
--tmpfs /tmp:rw,size=64m \
--log-opt max-size=10m \ # 日志大小限制
--log-opt max-file=3 \
-p 127.0.0.1:8080:3000 \ # 只绑本地,由 nginx 反代
-v app-data:/app/data \
--health-cmd "curl -f http://localhost:3000/health || exit 1" \
--health-interval 30s \
--health-retries 3 \
myapp:abc1234 # 使用 commit hash,不用 latest
8.4 镜像版本管理策略
bash
# ❌ 生产不用 latest(无法追溯)
docker pull myapp:latest
# ✅ 语义化版本
docker tag myapp:1.2.3 myapp:1.2
docker tag myapp:1.2.3 myapp:1
docker tag myapp:1.2.3 myapp:latest
# ✅ 用 git commit hash(可精确回滚)
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t myapp:${GIT_SHA} .
附录:常见问题排查
bash
# 容器启动失败
docker logs web
# 容器反复重启,查看退出码
docker inspect web --format='{{.State.ExitCode}} {{.State.Error}}'
# 查看磁盘占用
docker system df
# 清理(保留运行中容器和有名字的 volume)
docker system prune
# 网络排查
docker network inspect app-net
docker exec web ping db
docker exec web nslookup db
# 实时监控事件流
docker events
# 查看容器资源占用
docker stats