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 容器架构与运行时

安装 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
注意:Windows 版本需要 WSL2 后端,macOS 版本支持 Apple Silicon 和 x86 镜像。建议测试完毕再进展后续章节。

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
关键场景:数据库容器。PostgreSQL、MySQL、MongoDB 等数据库容器必须使用具名卷挂载数据目录,否则容器一删数据全丢。

容器网络

服务之间如何互相发现对方

图:Docker Compose 容器网络拓扑
驱动说明适用场景
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 基础镜像最小化攻击面,镜像通常 <100MBFROM 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 源码

经过这些最佳实践,你的镜像会更小、更安全、更适合生产部署。

资源限制:不要让容器使用无限的资源;日志记录应使用 json-file 驱动并限制大小,避免日志写满硬盘空间。

排错:容器问题

症状:容器启动后立刻退出,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 分支后自动构建、测试、部署。

1 / X