Docker 容器化部署规范
2026/3/20大约 5 分钟
Docker 容器化部署规范
提示
容器化是现代部署的标准方案。正确使用 Docker 可以实现环境一致性、快速部署、资源隔离。
核心原则
- 一容器一进程:每个容器只运行一个主进程
- 无状态设计:容器可随时销毁重建,数据外置
- 最小镜像:使用精简基础镜像,减少攻击面
- 非 root 运行:容器内进程使用普通用户
目录结构规范
/opt/docker/
├── apps/ # 应用容器数据
│ ├── blog/
│ │ ├── docker-compose.yml
│ │ ├── .env
│ │ └── data/ # 挂载数据
│ └── api/
│ └── ...
├── services/ # 基础服务
│ ├── nginx-proxy/
│ ├── postgres/
│ └── redis/
└── volumes/ # 共享卷(可选)
├── postgres-data/
└── redis-data/
Dockerfile 最佳实践
Node.js 应用
# 使用官方镜像,指定精确版本
FROM node:20-alpine AS builder
# 设置工作目录
WORKDIR /app
# 先复制依赖文件(利用缓存)
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建
RUN npm run build
# ========== 生产镜像 ==========
FROM node:20-alpine
# 安装必要工具
RUN apk add --no-cache tini
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 -G nodejs
WORKDIR /app
# 从构建阶段复制
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./
# 切换到非 root 用户
USER nodejs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# 使用 tini 作为 init 进程
ENTRYPOINT ["/sbin/tini", "--"]
# 启动命令
CMD ["node", "dist/server.js"]
Python 应用
FROM python:3.11-slim AS builder
WORKDIR /app
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# ========== 生产镜像 ==========
FROM python:3.11-slim
# 创建非 root 用户
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /app
# 复制依赖
COPY /root/.local /home/appuser/.local
# 复制代码
COPY . .
# 设置 PATH
ENV PATH=/home/appuser/.local/bin:$PATH
USER appuser
EXPOSE 8000
HEALTHCHECK \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
多阶段构建要点
# 阶段 1: 构建
FROM node:20-alpine AS builder
# ... 编译、打包
# 阶段 2: 生产
FROM node:20-alpine
# 只复制需要的文件
COPY /app/dist ./dist
Docker Compose 规范
基础模板
# docker-compose.yml
version: "3.8"
services:
app:
image: myapp:${VERSION:-latest}
build:
context: .
dockerfile: Dockerfile
container_name: myapp
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000" # 只绑定本地
environment:
- NODE_ENV=production
env_file:
- .env
volumes:
- ./data/uploads:/app/uploads
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
networks:
app-network:
driver: bridge
完整应用栈示例
# docker-compose.yml
version: "3.8"
services:
# ========== 应用服务 ==========
api:
image: myapi:${VERSION:-latest}
build:
context: ./api
container_name: api
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:${DB_PASSWORD}@postgres:5432/myapp
- REDIS_URL=redis://redis:6379
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# ========== 数据库 ==========
postgres:
image: postgres:15-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
# ========== 缓存 ==========
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- backend
# ========== 定时任务 ==========
worker:
image: myapi:${VERSION:-latest}
container_name: worker
restart: unless-stopped
command: ["node", "dist/worker.js"]
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:${DB_PASSWORD}@postgres:5432/myapp
- REDIS_URL=redis://redis:6379
env_file:
- .env
depends_on:
- postgres
- redis
networks:
- backend
volumes:
postgres-data:
redis-data:
networks:
backend:
driver: bridge
环境变量文件
# .env(不要提交到 Git!)
VERSION=1.0.0
DB_PASSWORD=your-secure-password
SECRET_KEY=your-secret-key
# .env.example(提交到 Git,作为模板)
VERSION=latest
DB_PASSWORD=change-me
SECRET_KEY=change-me
常用命令
基础操作
# 启动服务
docker compose up -d
# 停止服务
docker compose down
# 重新构建并启动
docker compose up -d --build
# 查看日志
docker compose logs -f
docker compose logs -f api
# 查看状态
docker compose ps
# 进入容器
docker compose exec api sh
# 重启单个服务
docker compose restart api
镜像管理
# 构建镜像
docker build -t myapp:1.0.0 .
# 打标签
docker tag myapp:1.0.0 myapp:latest
# 推送到仓库
docker push registry.example.com/myapp:1.0.0
# 清理无用镜像
docker image prune -a
# 查看镜像大小
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
数据卷管理
# 列出所有卷
docker volume ls
# 查看卷详情
docker volume inspect postgres-data
# 备份卷
docker run --rm -v postgres-data:/data -v $(pwd):/backup alpine \
tar czf /backup/postgres-backup.tar.gz -C /data .
# 恢复卷
docker run --rm -v postgres-data:/data -v $(pwd):/backup alpine \
tar xzf /backup/postgres-backup.tar.gz -C /data
# 删除未使用的卷
docker volume prune
网络管理
# 列出网络
docker network ls
# 查看网络详情
docker network inspect app_backend
# 查看容器 IP
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_name
安全最佳实践
1. 使用非 root 用户
# 创建用户
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
# 切换用户
USER appuser
2. 只暴露必要端口
# ❌ 错误:暴露到所有接口
ports:
- "3000:3000"
# ✅ 正确:只暴露到本地
ports:
- "127.0.0.1:3000:3000"
3. 使用只读文件系统
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/run
4. 限制资源
services:
app:
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
5. 扫描镜像漏洞
# 使用 Trivy 扫描
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:latest
# 或使用 Docker Scout
docker scout cves myapp:latest
6. 不要在镜像中存储密钥
# ❌ 错误
ENV SECRET_KEY=hardcoded-secret
COPY .env /app/.env
# ✅ 正确:运行时注入
# docker run -e SECRET_KEY=xxx
# 或使用 env_file
生产部署流程
标准部署流程
#!/bin/bash
# deploy.sh
set -e
APP_DIR="/opt/docker/apps/myapp"
VERSION=${1:-latest}
cd $APP_DIR
# 1. 拉取新镜像
echo "=== 拉取镜像 ==="
docker compose pull
# 2. 备份当前版本号
echo "=== 备份版本 ==="
cp .env .env.bak
# 3. 更新版本
echo "=== 更新版本: $VERSION ==="
sed -i "s/VERSION=.*/VERSION=$VERSION/" .env
# 4. 滚动更新
echo "=== 滚动更新 ==="
docker compose up -d --no-deps --build api
# 5. 等待健康检查
echo "=== 等待健康检查 ==="
sleep 10
docker compose ps
# 6. 清理旧镜像
echo "=== 清理旧镜像 ==="
docker image prune -f
echo "=== 部署完成 ==="
回滚流程
#!/bin/bash
# rollback.sh
set -e
APP_DIR="/opt/docker/apps/myapp"
cd $APP_DIR
# 恢复上一版本
cp .env.bak .env
# 重新部署
docker compose up -d --no-deps api
echo "=== 回滚完成 ==="
日志管理
配置日志驱动
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
集中式日志(推荐)
services:
app:
logging:
driver: "syslog"
options:
syslog-address: "tcp://logserver:514"
tag: "myapp"
常见错误
❌ 错误做法
# 1. 使用 latest 标签
FROM node:latest
# 2. 以 root 运行
# 没有 USER 指令
# 3. 复制所有文件(包括 node_modules)
COPY . .
# 4. 在 Dockerfile 中硬编码密钥
ENV DB_PASSWORD=secret123
✅ 正确做法
# 1. 使用精确版本
FROM node:20.10-alpine
# 2. 创建并使用普通用户
USER nodejs
# 3. 使用 .dockerignore,分层复制
COPY package*.json ./
RUN npm ci
COPY . .
# 4. 运行时注入密钥
# docker run -e DB_PASSWORD=xxx
.dockerignore 示例
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose*.yml
README.md
.DS_Store
*.log
coverage
.nyc_output