面向开发者的 Docker:容器、Compose 与真实工作流
一份关于 Docker 核心概念、Dockerfile 最佳实践以及使用 Docker Compose 运行多服务开发环境的实用指南。
"在我机器上明明是好的"——这句话已经引发了无数次部署故障。Docker 通过将应用及其所有依赖打包进一个容器来解决这个问题——这是一个隔离的、可复现的环境,无论是在你的笔记本上还是在生产环境中,运行结果完全一致。
容器与虚拟机的对比
| 虚拟机 | 容器 | |
|---|---|---|
| 隔离方式 | 完整操作系统 | 进程级别 |
| 启动时间 | 分钟级 | 秒级 |
| 体积 | GB 级 | MB 级 |
| 开销 | 高(需要 hypervisor) | 接近零 |
| 适用场景 | 完整 OS 隔离 | 应用打包 |
容器共享宿主机内核,但隔离了文件系统、进程和网络。这正是它们如此轻量的原因。
Docker 核心概念
镜像(Image) — 容器的只读蓝图,可以将其理解为类的定义。
容器(Container) — 镜像的运行实例,可以将其理解为由类创建的对象。
仓库(Registry) — 镜像的存储和分发系统。Docker Hub 是默认的公共仓库;GitHub Container Registry 和 AWS ECR 也是常见选择。
数据卷(Volume) — 在容器重启后仍然保留的持久化存储。没有挂载数据卷时,写入容器内部的数据会在容器停止后丢失。
编写一份优质的 Dockerfile
# 使用具体版本号——生产环境中永远不要用 "latest"
FROM node:20-alpine
# 设置工作目录
WORKDIR /app
# 先复制依赖文件(利用层缓存)
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 复制应用代码
COPY . .
# 构建应用
RUN npm run build
# 以非 root 用户运行以提高安全性
USER node
# 声明端口(不会主动发布)
EXPOSE 3000
# 使用 exec 格式以正确处理信号
CMD ["node", "server.js"]
层缓存:快速构建的关键
Dockerfile 中的每条指令都会创建一个层。Docker 会缓存这些层——如果某一层及其前置层没有发生变化,Docker 会直接复用缓存。
先复制依赖文件,再复制应用代码。 依赖的变更频率远低于代码本身。如果一次性复制所有文件,index.js 中哪怕只改动一行,也会导致依赖安装缓存失效,从而触发完整的 npm install。
多阶段构建
使用多阶段构建来保持生产镜像的精简:
# 第一阶段:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 第二阶段:生产环境(不含开发依赖和源码)
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
最终镜像只包含构建产物——不含源代码、测试文件或开发依赖。
使用 Docker Compose 进行本地开发
Docker Compose 用于编排多容器应用。一个 docker-compose.yml 文件即可定义整个本地开发栈:
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
使用我们的 Docker Compose Generator 快速生成项目脚手架——选择所需服务,几秒内即可获得一份生产就绪的 docker-compose.yml。
Compose 常用命令
# 在后台启动所有服务
docker compose up -d
# 查看日志(跟踪模式)
docker compose logs -f app
# 在运行中的容器内执行命令
docker compose exec app sh
# 停止并移除容器(保留数据卷)
docker compose down
# 停止并移除容器及数据卷
docker compose down -v
# 修改 Dockerfile 后重新构建镜像
docker compose up -d --build
环境变量与密钥管理
永远不要在 Dockerfile 或 Compose 文件中硬编码凭据。请使用 .env 文件:
# .env(记得添加到 .gitignore!)
DATABASE_URL=postgresql://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379
JWT_SECRET=your-super-secret-key
Compose 会自动从项目目录加载 .env 文件。在 YAML 中使用 ${VARIABLE_NAME} 的方式引用变量。
使用我们的 Env Generator 快速生成带有合理默认值的 .env 文件,以及供团队使用的 .env.example 模板。
Docker 网络
Compose 会为你的服务栈自动创建一个默认网络。各服务可以通过服务名称作为主机名相互访问——这就是为什么应用使用 db 作为数据库主机名,而不是 localhost。
app → 连接到 "db:5432"
app → 连接到 "redis:6379"
只有通过 ports: 显式映射的端口,才能从宿主机访问。
健康检查与启动顺序
depends_on 只等待容器启动,而不是等待其就绪。数据库容器可能在几秒内启动,但 Postgres 可能还需要额外几秒才能接受连接。请使用 healthcheck + condition: service_healthy 来确保服务真正就绪后再继续。
生产环境注意事项
- 使用具体的镜像标签 — 例如
node:20.11.1-alpine,而非node:latest。 - 扫描镜像漏洞 —
docker scout cves myapp:latest。 - 设置资源限制 — 防止单个容器耗尽其他容器的资源。
- 尽量使用只读文件系统 — 在 Compose 中设置
read_only: true。 - 不要以 root 运行 — 添加
USER node或对应的用户指令。 - 配置 Nginx 作为应用的反向代理 — 使用我们的 Nginx Config Generator 获取经过实战验证的配置。
快速参考
# 镜像操作
docker images # 列出本地镜像
docker pull nginx:alpine # 下载镜像
docker rmi myimage # 删除镜像
# 容器操作
docker ps # 查看运行中的容器
docker ps -a # 查看所有容器
docker stop <id> # 优雅停止容器
docker rm <id> # 删除已停止的容器
docker logs <id> -f # 实时查看日志
# 清理操作
docker system prune # 删除所有未使用的资源
docker volume prune # 删除未使用的数据卷
Docker 彻底消除了一整类与环境相关的 bug,并让新开发者的上手过程变得极为轻松。掌握这里介绍的基础知识,你将为后续学习 Kubernetes、CI/CD 流水线和云端部署打下坚实的基础。