🐳 第三课:Docker 容器化部署实战

预计阅读时间:14 分钟  |  难度:进阶 ⭐⭐⭐

📋 本课目标:理解 Docker 的核心概念(镜像、容器、Dockerfile、数据卷),能为你自己的应用编写 Dockerfile 和 docker-compose.yml,实现"一次构建,到处运行"。

1 为什么需要容器化?

回想第二课的手工部署流程——你要手工安装 Nginx、Node.js、数据库,配置环境变量、权限、日志……如果换一台服务器,这些步骤全部重来。

更糟的是"我电脑上跑得好好的"问题:开发环境和生产环境不一致(OS 版本不同、依赖版本不同),导致部署后各种报错。

Docker 解决了这两个痛点:

你的应用 + 依赖/环境 + 配置文件 📦 Docker 镜像 🖥️ 任何服务器

把应用 + 所有依赖打包成一个镜像,在任何安装了 Docker 的机器上都能以容器的形式运行——环境一致性问题从此消失。

2 核心概念:镜像 vs 容器 vs Dockerfile

概念类比说明
Dockerfile蛋糕🍰的"食谱"包含一系列指令(FROM、RUN、COPY 等),描述如何构建镜像
镜像 (Image)冷冻蛋糕🍰只读模板,包含文件系统和配置。可以从 Dockerfile 构建,或从 Registry 拉取
容器 (Container)切好的蛋糕🍰🍴 镜像的运行实例。可以启动、停止、删除。容器之间相互隔离
数据卷 (Volume)冰箱🧊容器删除后数据会消失。Volume 把数据持久化到宿主机,独立于容器生命周期

3 编写 Dockerfile:以 Node.js 应用为例

3.1 从零开始写 Dockerfile

在项目根目录创建 Dockerfile(无扩展名):

# ===== Dockerfile — Node.js 应用 =====

# 1️⃣ 选一个基础镜像
FROM node:18-alpine

# 2️⃣ 设置工作目录
WORKDIR /app

# 3️⃣ 先复制 package.json(利用 Docker 缓存层)
COPY package*.json ./

# 4️⃣ 安装生产依赖
RUN npm ci --only=production

# 5️⃣ 复制应用源码
COPY . .

# 6️⃣ 暴露端口
EXPOSE 3000

# 7️⃣ 启动命令
CMD ["node", "index.js"]
FROM node:18-alpine 基础镜像(约 120MB) ⬇️ COPY package*.json + RUN npm ci 依赖层(变化较少) ⬇️ COPY . . 源码层(变化频繁) ⬇️ CMD ["node", "index.js"] 运行时层
⚠️ 常见误区:COPY . . 写在 RUN npm install 之前——每次修改源码都会导致安装依赖的缓存层失效。
正解:先复制 package.json 安装依赖,再复制源码。这是 Docker 官方最佳实践 明确推荐的层缓存策略。

3.2 构建和运行

# 构建镜像(注意最后的 . )
docker build -t myapp:latest .

# 查看本地镜像列表
docker images

# 运行容器(前台模式,按 Ctrl+C 停止)
docker run -p 3000:3000 myapp:latest

# 运行容器(后台守护模式)
docker run -d --name myapp-container -p 3000:3000 myapp:latest

# 查看运行中的容器
docker ps

# 查看日志
docker logs -f myapp-container

# 停止和删除容器
docker stop myapp-container
docker rm myapp-container

4 Docker Compose:一键启动多服务

实际项目中很少只有一个容器——至少需要 Web 服务 + 数据库。Docker Compose 让你在 docker-compose.yml 中定义所有服务,一条命令启动整个栈。

4.1 编写 docker-compose.yml

# docker-compose.yml
version: "3.8"

services:
  app:
    build: . # 使用当前目录的 Dockerfile
    ports:
      - "3000:3000" # 宿主机端口:容器端口
    environment:
      - NODE_ENV=production
      - DB_HOST=db
      - DB_USER=myuser
      - DB_PASSWORD=${DB_PASSWORD} # 从 .env 文件读取
    depends_on:
      - db
    volumes:
      - app_logs:/app/logs # 持久化日志

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - certbot_data:/etc/letsencrypt
    depends_on:
      - app

  db:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=myapp
      - MYSQL_USER=myuser
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql # 数据库数据持久化

volumes:
  mysql_data:
  app_logs:
  certbot_data:
# 启动所有服务(-d 后台运行)
docker compose up -d

# 查看服务状态
docker compose ps

# 查看日志(指定服务)
docker compose logs -f app

# 重启指定服务
docker compose restart app

# 停止并删除所有容器和网络
docker compose down

# 重新构建并启动(代码变更后常用)
docker compose up -d --build

5 数据持久化:Volume 的核心作用

容器一旦删除,内部所有数据都会消失。这就是为什么数据库必须使用 Volume(数据卷):

两种持久化方式

Volume由 Docker 管理,存储在 /var/lib/docker/volumes/。推荐用于数据库等不需要手动查看的数据。
Bind Mount直接映射宿主机目录。适合需要手动编辑的配置文件和开发时的热重载。
# Volume 用法:docker-compose 中已经示范了
# volumes: mysql_data:/var/lib/mysql

# Bind Mount 用法(开发时热重载)
docker run -d -p 3000:3000 -v $(pwd):/app myapp:dev

# 查看和管理 Volume
docker volume ls
docker volume inspect mysql_data
⚠️ 致命误区:Docker 部署 MySQL/PostgreSQL 没有使用 Volume。执行 docker compose down 后所有数据全部丢失!
预防措施:无论如何都要为数据库定义 volume。在 docker-compose.yml 的 volumes 节声明,并在 db 服务的 volumes 列表中引用。
参考:Docker Volumes 文档

6 网络:容器之间怎么通信?

Docker 的默认网络模式是 bridge,同一台机器上、同一个 docker-compose.yml 下的服务可以通过服务名互相访问(例如 app 容器里用 db:3306 连接数据库)。

网络模式说明适用场景
bridge默认。容器间通过 IP/服务名通信,与宿主机隔离单机多容器
host容器直接用宿主机的网络栈需要极低延迟的场景(如 Redis)
none容器无网络仅计算任务,不需要网络

6.1 防火墙与端口映射

Docker 容器默认通过 bridge 网络与外界隔离。要让外部访问容器内的服务,必须在 docker rundocker-compose.yml 中映射端口:

# -p 宿主机端口:容器端口
docker run -d -p 8080:3000 myapp:latest

# 用 docker compose 时定义在 ports 字段
ports:
  - "8080:3000"

7 Docker 安全最佳实践

  1. 不要以 root 运行容器——Dockerfile 中使用 USER 指令切换到非 root 用户
  2. 使用 Alpine 版本的基础镜像——更小的镜像 = 更小的攻击面
  3. 定期扫描镜像漏洞——docker scouttrivy
  4. 不要硬编码密钥——使用 .env 文件或 Docker Secrets
  5. 使用多阶段构建——构建环境和运行环境分离,减小生产镜像体积
# 多阶段构建示例

# 第一阶段:构建
FROM node:18 AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

# 第二阶段:运行(用 Nginx 托管构建产物)
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

8 小测验

Q1: Dockerfile 中应如何安排指令顺序以最大化缓存利用?

Q2: docker compose down 后,你的数据库数据会怎样?

Q3: 在 docker-compose.yml 中,app 服务如何连接数据库?

📖 推荐官方文档

📌 第四课预告

第四课:CI/CD 自动化部署流水线

手工部署和 Docker 部署你都学会了。但每次改代码都要手动 SSH 连上去拉代码、构建、重启?这一课我们用 GitHub Actions 实现"git push 即部署"。