预计阅读时间:14 分钟 | 难度:进阶 ⭐⭐⭐
回想第二课的手工部署流程——你要手工安装 Nginx、Node.js、数据库,配置环境变量、权限、日志……如果换一台服务器,这些步骤全部重来。
更糟的是"我电脑上跑得好好的"问题:开发环境和生产环境不一致(OS 版本不同、依赖版本不同),导致部署后各种报错。
Docker 解决了这两个痛点:
把应用 + 所有依赖打包成一个镜像,在任何安装了 Docker 的机器上都能以容器的形式运行——环境一致性问题从此消失。
| 概念 | 类比 | 说明 |
|---|---|---|
| Dockerfile | 蛋糕🍰的"食谱" | 包含一系列指令(FROM、RUN、COPY 等),描述如何构建镜像 |
| 镜像 (Image) | 冷冻蛋糕🍰 | 只读模板,包含文件系统和配置。可以从 Dockerfile 构建,或从 Registry 拉取 |
| 容器 (Container) | 切好的蛋糕🍰🍴 | 镜像的运行实例。可以启动、停止、删除。容器之间相互隔离 |
| 数据卷 (Volume) | 冰箱🧊 | 容器删除后数据会消失。Volume 把数据持久化到宿主机,独立于容器生命周期 |
在项目根目录创建 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"]
COPY . . 写在 RUN npm install 之前——每次修改源码都会导致安装依赖的缓存层失效。package.json 安装依赖,再复制源码。这是 Docker 官方最佳实践 明确推荐的层缓存策略。
# 构建镜像(注意最后的 . )
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
实际项目中很少只有一个容器——至少需要 Web 服务 + 数据库。Docker Compose 让你在 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
容器一旦删除,内部所有数据都会消失。这就是为什么数据库必须使用 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 compose down 后所有数据全部丢失!Docker 的默认网络模式是 bridge,同一台机器上、同一个 docker-compose.yml 下的服务可以通过服务名互相访问(例如 app 容器里用 db:3306 连接数据库)。
| 网络模式 | 说明 | 适用场景 |
|---|---|---|
| bridge | 默认。容器间通过 IP/服务名通信,与宿主机隔离 | 单机多容器 |
| host | 容器直接用宿主机的网络栈 | 需要极低延迟的场景(如 Redis) |
| none | 容器无网络 | 仅计算任务,不需要网络 |
Docker 容器默认通过 bridge 网络与外界隔离。要让外部访问容器内的服务,必须在 docker run 或 docker-compose.yml 中映射端口:
# -p 宿主机端口:容器端口
docker run -d -p 8080:3000 myapp:latest
# 用 docker compose 时定义在 ports 字段
ports:
- "8080:3000"
USER 指令切换到非 root 用户docker scout 或 trivy.env 文件或 Docker Secrets# 多阶段构建示例
# 第一阶段:构建
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;"]
Q1: Dockerfile 中应如何安排指令顺序以最大化缓存利用?
Q2: docker compose down 后,你的数据库数据会怎样?
Q3: 在 docker-compose.yml 中,app 服务如何连接数据库?
手工部署和 Docker 部署你都学会了。但每次改代码都要手动 SSH 连上去拉代码、构建、重启?这一课我们用 GitHub Actions 实现"git push 即部署"。