Docker 容器化
告别"在我机器上能跑"的环境噩梦
🎯 学习目标
学完本章,你将能够:
- 理解 Docker 核心概念:镜像、容器、Volume 和网络
- 编写 Dockerfile 并使用多阶段构建优化镜像
- 使用 Docker Compose 编排多容器应用
- 配置开发环境热重载和生产环境最佳实践
- 发布镜像到 Docker Hub 并管理版本
你有没有想过
为什么代码在本地跑得好好的,部署到服务器就报错?为什么新同事配环境要花一整天?
这些都是环境不一致导致的:Node 版本不同、数据库没装、环境变量没配。
Docker 把应用和它的所有依赖打包成一个容器,确保在任何环境都能一致运行。开发、测试、生产,一个镜像走天下。
核心概念
七个 Docker 关键词
镜像 Image
只读的应用模板。类似安装光盘。
容器 Container
镜像的运行实例。类似运行中的程序。
Dockerfile
构建镜像的指令文件。类似构建脚本。
Registry
镜像仓库。类似 npm registry。
Volume
持久化数据存储。类似外接硬盘。
Network
容器间通信网络。类似局域网。
Compose
多容器编排工具。管理一组容器。
Docker vs 虚拟机
轻量级的进程隔离
| 特性 | Docker 容器 | 虚拟机 |
|---|---|---|
| 启动速度 | 秒级 | 分钟级 |
| 资源占用 | MB 级别 | GB 级别 |
| 性能 | 接近原生 | 有虚拟化开销 |
| 镜像大小 | 几十到几百 MB | 通常几 GB |
| 单机密度 | 数百个 | 几个到几十个 |
这个比喻只是为了帮你建立直观感受:虚拟机像整栋别墅,容器像合租公寓的一个房间。实际上容器共享宿主内核,只是进程级别的隔离。
容器架构
Docker 如何在宿主系统上运行容器

图:Docker 容器架构与运行时
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:展示 Docker 容器架构,包含 Docker Engine、多个容器和客户端 - 左侧是一个圆角矩形"宿主系统",内嵌"Docker Engine"大框 - Docker Engine 内部有三个并排的容器框:Node.js App(含 Express 服务器和应用代码)、PostgreSQL(含数据库进程和数据文件)、Redis(含缓存进程) - 容器之间有"Network"连线,PostgreSQL 到数据文件有"Volume 持久化"虚线 - 右侧是"客户端: docker / docker compose",通过"API 请求"箭头指向 Docker Engine 内的 Docker Daemon - 整体配色:蓝色系为主,容器用浅色背景,连线用深色箭头
安装 Docker
各平台安装方式与国内镜像加速
Docker Desktop (Win/macOS)
下载官方安装包,包含 GUI 设置、Kubernetes 支持。
apt install (Ubuntu)
服务器环境建议直接安装 Docker Engine,新版本使用 docker.io 或 Docker 官方 repo。
验证安装
运行 docker run hello-world 确保环境正确启动,成功即可开始使用。
国内镜像加速
配置加速源解决国际访问问题,如 docker.ucloud.cn 等。
# Ubuntu/Debian 安装 (最新版本方式)
$ curl -fsSL https://get.docker.com | sh
# 或者使用 apt
$ sudo apt-get update
$ sudo apt-get install -y docker-ce docker-ce-cli docker-compose-plugin
# 免去每次加 sudo
$ sudo usermod -aG docker $USER
$ newgrp docker
# 验证
$ docker --version
Docker version 24.x.x
$ docker run hello-world
# 输出 Hello from Docker! 表示成功
# 国内镜像加速配置 /etc/docker/daemon.json
{
"registry-mirrors": [
"https://docker.ucloud.cn",
"https://docker.m.daocloud.io",
"https://<your-id>.mirror.aliyuncs.com"
]
}
# 保存后重启 Docker
$ sudo systemctl restart docker
# 验证加速是否生效
$ docker info | grep -A 5 "Registry Mirrors"
常见问题排查
# 问题1: permission denied
# 原因:当前用户未加入 docker 组
$ sudo usermod -aG docker $USER
$ newgrp docker
# 问题2: 无法连接 Docker Hub
# 检查网络,尝试国内镜像加速
$ docker info | grep -i mirror
# 问题3: 容器无法访问外部网络
# 检查 DNS 配置
$ docker run --rm alpine ping -c 3 8.8.8.8
$ docker run --rm alpine ping -c 3 google.com
Dockerfile 指令速查
12 个最常用的 Dockerfile 指令
| 指令 | 作用 | 示例 |
|---|---|---|
| FROM | 指定基础镜像 | FROM node:20-alpine |
| WORKDIR | 设置工作目录 | WORKDIR /app |
| COPY | 复制文件到容器 | COPY package*.json ./ |
| RUN | 执行命令(构建时) | RUN npm ci |
| ENV | 设置环境变量 | ENV NODE_ENV=production |
| EXPOSE | 声明容器监听端口 | EXPOSE 3000 |
| USER | 切换运行用户 | USER appuser |
| CMD | 容器启动默认命令 | CMD ["node", "dist/index.js"] |
| ENTRYPOINT | 容器入口点(不可覆盖) | ENTRYPOINT ["node"] |
| ARG | 构建时参数 | ARG NODE_VERSION=20 |
| HEALTHCHECK | 健康检查配置 | HEALTHCHECK CMD curl -f http://localhost:3000/health |
| VOLUME | 声明挂载点 | VOLUME /data |
CMD vs ENTRYPOINT:CMD 指定默认命令,可被命令行参数覆盖;通常组合使用 ENTRYPOINT ["node"] + CMD ["dist/index.js"],运行时只替换 CMD 部分。
Part 1: Dockerfile
用多阶段构建打造精简的生产镜像。
多阶段构建
构建和运行分离,镜像体积缩小 80%
# 第一阶段:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
# 第二阶段:运行
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
# 创建非 root 用户
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
最终镜像只包含 dist/ 和 node_modules/,不含源码和构建工具。Alpine 基础镜像仅 ~5MB,加上 Node 应用一般不超过 100MB。
.dockerignore
排除不需要进入镜像的文件
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
*.md
.docker
docker-compose*.yml
Dockerfile*
coverage
.nyc_output
.vscode
.idea
不写 .dockerignore,构建时会把整个项目目录(包括巨大的 node_modules)发给 Docker daemon,导致构建极慢且镜像臃肿。
Part 2: 数据与网络
容器是临时的,数据是永久的,服务是隔离的,通信是必要的。Volume 确保数据不丢。
Volume 数据持久化
容器毁了,数据还在
容器是临时的。容器删除后,内部文件就消失了。但数据库数据、用户上传文件显然不能跟着一起消失。
| 类型 | 命令 | 生命周期 | 适用场景 |
|---|---|---|---|
| 匿名卷 | VOLUME /data | 随容器删除 | 临时缓存 |
| 具名卷 | docker volume create my-vol | 独立于容器 | 数据库数据 |
| Bind Mount | -v $(pwd):/app | 绑定宿主目录 | 开发环境代码同步 |
# 创建具名卷
$ docker volume create pgdata
# 启动容器时挂载
$ docker run -d --name pg \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16-alpine
# 容器删了,数据还在
$ docker rm -f pg
$ docker volume ls # pgdata 仍存在
# 新容器挂载同一个卷,数据自动恢复
$ docker run -d --name pg-new \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16-alpine
容器网络
服务之间如何互相发现对方

图:Docker Compose 容器网络拓扑
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:展示 docker-compose 创建的 bridge 网络,包含三个服务容器 - 顶部是"宿主",通过端口映射 3000:3000 和 5432:5432 连接到下方网络 - 下方是一个虚线框标注"docker-compose 统一网络 (bridge)" - 网络内部有三个并排的圆角矩形:app:3000、db:5432、redis:6379 - app 到 db 有箭头标注"通过服务名 db:5432 连接" - app 到 redis 有箭头标注"通过服务名 redis:6379 连接" - 整体配色:绿色系为主,网络框用虚线,容器用浅色背景
| 驱动 | 说明 | 适用场景 |
|---|---|---|
| bridge | 默认驱动,容器通过虚拟网络通信 | 单机多容器网络 |
| host | 容器直接使用宿主机网络协议栈 | 高性能需求 |
| none | 无网络接口 | 离线批处理 |
# Compose 默认创建统一 bridge 网络
# 容器间通过"服务名"互相访问,无需知道 IP
services:
app:
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
REDIS_URL: redis://redis:6379
这个比喻只是为了帮你建立直观感受:容器网络就像办公室电话分机,每个容器有一个"分机号"(服务名),拨打分机就能接通对应容器。实际上 Docker 通过内置 DNS 实现服务名到容器 IP 的解析。
自定义网络
隔离不同项目的容器
# 创建自定义网络
$ docker network create project-a-net
$ docker network create project-b-net
# 容器连接到指定网络
$ docker run -d --name app-a --network project-a-net nginx
$ docker run -d --name app-b --network project-b-net nginx
# app-a 和 app-b 无法互相通信,实现项目间隔离
自定义网络可以提供自动 DNS 解析,容器通过服务名即可互相访问,无需使用 IP 地址。不同网络的容器默认隔离。
Part 3: Compose 编排
一个文件定义整套基础设施。
docker-compose.yml
Node + Postgres + Redis,一个文件搞定
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://user:pass@db:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
volumes:
pgdata:
redis_data:
networks:
backend:
开发环境覆盖
同一个项目,不同环境不同配置
# docker-compose.override.yml(开发自动加载)
services:
app:
command: npm run dev
volumes:
- .:/app # 挂载源码(热重载)
- /app/node_modules # 不覆盖容器依赖
environment:
- NODE_ENV=development
ports:
- "3000:3000"
- "9229:9229" # 调试端口
$ docker compose up -d # 启动所有服务
$ docker compose logs -f app # 查看 app 日志
$ docker compose exec db psql -U user -d myapp
$ docker compose down # 停止所有服务
Docker Hub 镜像发布
构建、打标签、推送,让镜像进入公共仓库
# 1. 注册 Docker Hub 账号并登录
$ docker login
Username: your-username
Password: ********
# 2. 构建本地镜像
$ docker build -t your-username/my-node-app:v1.0 .
# 3. 推送到 Docker Hub
$ docker push your-username/my-node-app:v1.0
# 4. 任何人都可以拉取并运行
$ docker run -d -p 3000:3000 your-username/my-node-app:v1.0
私有仓库:企业内部可用 Harbor、GitLab Registry 或云商容器镜像服务托管私有镜像,避免代码泄露并加速内网拉取。
镜像版本管理策略
使用 semantic versioning(Major.Minor.Patch)方式标注镜像版本,例如 v1.0.0、v1.1.0、v1.1.1。同时维护 latest 标签指向最新稳定版。
生产最佳实践
安全与效率并重
| 实践 | 说明 | 收益 |
|---|---|---|
| Alpine 基础镜像 | 最小化攻击面,镜像通常 <100MB | FROM node:20-alpine |
| 非 root 用户运行 | 降低容器内权限 | USER appuser |
| 多阶段构建 | 不包含构建工具和源码 | FROM ... AS builder |
| .dockerignore | 排除 node_modules、.git 等 | 加速构建,减小上下文 |
| 镜像安全扫描 | 定期检测 CVE 漏洞 | docker scout cve my-app |
镜像安全扫描
# 使用 Docker Scout 扫描
$ docker scout cve my-node-app:latest
# 使用 Trivy 扫描(第三方工具)
$ trivy image my-node-app:latest
# 扫描后修复: 升级基础镜像或修补 CVE 源码
经过这些最佳实践,你的镜像会更小、更安全、更适合生产部署。
排错:容器问题
docker ps 看不到。
原因:应用启动报错(如连不上数据库),进程退出导致容器停止。
修复:用
docker logs my-app 查看退出日志;确保 depends_on 配了 condition: service_healthy。
# 查看退出容器的日志
$ docker ps -a # 看所有容器(含已停止的)
$ docker logs my-app # 查看崩溃原因
$ docker compose logs app # Compose 方式查看
试一试
练习一
任务:为你的 Node.js 应用编写一个多阶段 Dockerfile,要求使用 Alpine 基础镜像、非 root 用户运行。
参考答案
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
RUN adduser -D appuser
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
练习二
任务:编写 docker-compose.yml,包含 Node.js 应用和 Redis 两个服务,应用通过环境变量连接 Redis。
参考答案
services:
app:
build: .
ports: ["3000:3000"]
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
练习三
任务:创建一个具名 volume 存储 PostgreSQL 数据,验证容器销毁后数据不丢。
参考答案
# 1. 创建具名卷
$ docker volume create pgdata
# 2. 启动 PostgreSQL 容器
$ docker run -d --name pg-test \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=test123 \
postgres:16-alpine
# 3. 写入数据
$ docker exec -it pg-test psql -U postgres -c "CREATE TABLE test (id INT);"
$ docker exec -it pg-test psql -U postgres -c "INSERT INTO test VALUES (1);"
# 4. 销毁容器
$ docker rm -f pg-test
# 5. 重新启动,验证数据还在
$ docker run -d --name pg-test2 \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=test123 \
postgres:16-alpine
$ docker exec -it pg-test2 psql -U postgres -c "SELECT * FROM test;"
# 输出: 1 —— 数据完好无损
练习四(生产构建)
任务:使用 docker build 构建你的 Node.js 应用镜像,并用 docker run 启动容器,验证 3000 端口可访问。
参考答案
# 构建镜像
$ docker build -t my-node-app:v1.0 .
# 启动容器
$ docker run -d -p 3000:3000 --name my-app my-node-app:v1.0
# 验证访问
$ curl http://localhost:3000
# 应该返回 HTTP 200
# 查看日志
$ docker logs -f my-app
📝 要点总结
本章核心知识点
镜像构建
多阶段构建分离编译和运行环境,大幅缩小镜像体积
数据持久化
Volume 让容器数据不随容器销毁而丢失,Bind Mount 方便开发
Compose 编排
一个 yml 文件定义多服务应用,一键启动完整开发环境
生产实践
非 root 用户 + 健康检查 + 资源限制 = 安全的生产容器
下一章
Docker 让应用可以一致地运行在任何环境。下一章我们学习 CI/CD——让代码推送到 main 分支后自动构建、测试、部署。