第13章:生产环境部署实战
本章目标:通过真实案例,演示如何在生产环境中部署和管理容器化应用。
13.1 生产环境架构设计
13.1.1 典型生产架构
┌─────────────────────────────────────────────────────────────┐
│ 生产环境架构 │
│ │
│ 用户 → 负载均衡器 → Nginx 反向代理 → 应用服务 → 数据库 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 负载均衡层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Nginx/LB │ │ Nginx/LB │ (主备/双活) │ │
│ │ └──────┬───────┘ └──────┬───────┘ │ │
│ └─────────┼─────────────────┼────────────────────────┘ │
│ │ │ │
│ ┌─────────┼─────────────────┼────────────────────────┐ │
│ │ ▼ ▼ 应用服务层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ App ×3 │ │ App ×3 │ (多副本) │ │
│ │ └──────┬───────┘ └──────┬───────┘ │ │
│ └─────────┼─────────────────┼────────────────────────┘ │
│ │ │ │
│ ┌─────────┼─────────────────┼────────────────────────┐ │
│ │ ▼ ▼ 数据层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ MySQL主从 │ │ Redis集群 │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
13.1.2 生产环境清单
生产环境检查清单:
□ 基础设施
├── □ 服务器配置(CPU/内存/磁盘)
├── □ 网络配置(VLAN/防火墙)
├── □ 域名和 SSL 证书
└── □ 备份策略
□ Docker 环境
├── □ Docker 版本和配置
├── □ 镜像仓库
├── □ 日志配置
└── □ 监控告警
□ 应用部署
├── □ Dockerfile 优化
├── □ docker-compose 配置
├── □ 环境变量管理
└── □ 健康检查配置
□ 数据安全
├── □ 数据库持久化
├── □ 备份恢复策略
├── □ 密钥管理
└── □ SSL/TLS 加密
□ 运维保障
├── □ 日志收集
├── □ 监控告警
├── □ 自动扩缩容
└── □ 灰度发布
13.2 Nginx + PHP + MySQL + Redis 全栈部署
13.2.1 项目结构
project/
├── docker-compose.yml # 主配置
├── docker-compose.prod.yml # 生产环境配置
├── .env # 环境变量
├── .env.prod # 生产环境变量
├── nginx/
│ ├── nginx.conf # Nginx 主配置
│ ├── conf.d/
│ │ └── default.conf # 站点配置
│ └── ssl/ # SSL 证书
├── php/
│ ├── Dockerfile # PHP 镜像
│ ├── php.ini # PHP 配置
│ �── opcache.ini # OPcache 配置
│ └── conf.d/
│ └── app.ini # 应用配置
├── mysql/
│ ├── my.cnf # MySQL 配置
│ └── initdb/ # 初始化脚本
├── redis/
│ └── redis.conf # Redis 配置
├── app/ # 应用代码
│ ├── src/
│ ├── public/
│ └── composer.json
└── logs/ # 日志目录
13.2.2 Docker Compose 配置
yaml
# docker-compose.prod.yml
version: '3.8'
services:
# Nginx 反向代理
nginx:
image: nginx:1.25-alpine
container_name: prod-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./app/public:/app/public:ro
- nginx-logs:/var/log/nginx
networks:
- frontend
depends_on:
- php
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 512M
# PHP-FPM 应用服务
php:
build:
context: ./php
dockerfile: Dockerfile
container_name: prod-php
volumes:
- ./app:/app
- ./php/php.ini:/usr/local/etc/php/php.ini:ro
- ./php/opcache.ini:/usr/local/etc/php/conf.d/opcache.ini:ro
environment:
- APP_ENV=production
- DB_HOST=mysql
- DB_PORT=3306
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
networks:
- frontend
- backend
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "php-fpm-healthcheck"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 1G
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: prod-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/conf.d/custom.cnf:ro
- ./mysql/initdb:/docker-entrypoint-initdb.d
ports:
- "127.0.0.1:3306:3306" # 仅本地访问
networks:
- backend
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 2G
# Redis 缓存
redis:
image: redis:7-alpine
container_name: prod-redis
command: redis-server /etc/redis/redis.conf
volumes:
- redis-data:/data
- ./redis/redis.conf:/etc/redis/redis.conf:ro
ports:
- "127.0.0.1:6379:6379" # 仅本地访问
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1'
memory: 512M
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 内部网络,不对外暴露
volumes:
mysql-data:
driver: local
redis-data:
driver: local
nginx-logs:
driver: local
13.2.3 Nginx 配置
nginx
# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
include /etc/nginx/conf.d/*.conf;
}
nginx
# nginx/conf.d/default.conf
upstream php-upstream {
server php:9000;
}
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /app/public;
index index.php index.html;
# 健康检查端点
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# PHP 处理
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-upstream;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
13.2.4 PHP 配置
dockerfile
# php/Dockerfile
FROM php:8.2-fpm-alpine
# 安装系统依赖
RUN apk add --no-cache \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
libzip-dev \
icu-dev \
oniguruma-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo \
pdo_mysql \
mysqli \
bcmath \
gd \
zip \
intl \
mbstring \
opcache \
pcntl
# 安装 Redis 扩展
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis
# 清理
RUN apk del --purge $PHPIZE_DEPS
# 复制配置文件
COPY php.ini /usr/local/etc/php/php.ini
COPY opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# 设置工作目录
WORKDIR /app
# 安装 Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
EXPOSE 9000
CMD ["php-fpm"]
ini
; php/php.ini
[PHP]
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
max_input_time = 300
date.timezone = Asia/Shanghai
[Session]
session.save_handler = redis
session.save_path = "tcp://redis:6379?auth=your_password"
session.gc_maxlifetime = 1440
ini
; php/opcache.ini
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.save_comments=1
opcache.fast_shutdown=1
opcache.enable_cli=0
13.2.5 MySQL 配置
ini
# mysql/my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-storage-engine = InnoDB
# 性能优化
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# 连接配置
max_connections = 200
wait_timeout = 600
interactive_timeout = 600
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
# 二进制日志(用于主从复制和备份)
log-bin = mysql-bin
binlog_expire_logs_seconds = 604800
max_binlog_size = 100M
# 安全配置
sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
13.3 灰度发布策略
13.3.1 蓝绿部署
yaml
# 蓝绿部署示例
version: '3.8'
services:
# 蓝环境(当前版本)
app-blue:
image: myapp:v1.0
container_name: app-blue
networks:
- app-network
deploy:
replicas: 3
# 绿环境(新版本)
app-green:
image: myapp:v2.0
container_name: app-green
networks:
- app-network
deploy:
replicas: 3
# Nginx 负载均衡(切换流量)
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
networks:
- app-network
nginx
# nginx/nginx.conf - 蓝绿切换
upstream app {
# 蓝环境(当前)
server app-blue:8080;
# 绿环境(新版本)- 切换时注释/取消注释
# server app-green:8080;
}
13.3.2 金丝雀发布
yaml
# 金丝雀发布示例
version: '3.8'
services:
# 稳定版本(90% 流量)
app-stable:
image: myapp:v1.0
deploy:
replicas: 9
# 金丝雀版本(10% 流量)
app-canary:
image: myapp:v2.0
deploy:
replicas: 1
13.3.3 滚动更新
bash
# Docker Swarm 滚动更新
docker service update \
--image myapp:v2.0 \
--update-parallelism 1 \
--update-delay 30s \
--update-failure-action rollback \
myapp-service
# 参数说明:
# --update-parallelism 1 每次更新1个实例
# --update-delay 30s 每次更新间隔30秒
# --update-failure-action rollback 失败时自动回滚
13.4 自动扩缩容
13.4.1 Docker Swarm 自动扩缩容
bash
# 设置服务自动扩缩容
docker service scale myapp=3
# 根据负载手动扩缩容
docker service scale myapp=5
docker service scale myapp=2
13.4.2 结合 Prometheus 的自动扩缩容
yaml
# prometheus-rules.yml
groups:
- name: scaling-rules
rules:
- record: app:cpu:avg
expr: avg(rate(container_cpu_usage_seconds_total{image="myapp"}[5m]))
- alert: ScaleUp
expr: app:cpu:avg > 0.7
for: 5m
labels:
action: scale-up
- alert: ScaleDown
expr: app:cpu:avg < 0.3
for: 10m
labels:
action: scale-down
13.5 数据备份与恢复
13.5.1 MySQL 备份策略
bash
# 全量备份
docker exec prod-mysql mysqldump -uroot -p${DB_ROOT_PASSWORD} \
--all-databases --single-transaction --routines --triggers \
| gzip > /backup/mysql/full_$(date +%Y%m%d_%H%M%S).sql.gz
# 增量备份(基于 binlog)
docker exec prod-mysql mysqlbinlog --read-from-remote-server \
--host=master-host --user=repl --password=xxx \
mysql-bin.000001 > /backup/mysql/incremental.sql
# 恢复
gunzip < /backup/mysql/full_20240115_030000.sql.gz | \
docker exec -i prod-mysql mysql -uroot -p${DB_ROOT_PASSWORD}
13.5.2 自动化备份脚本
bash
#!/bin/bash
# backup.sh - 自动化备份脚本
set -e
# 配置
BACKUP_DIR="/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD}"
# 创建备份目录
mkdir -p ${BACKUP_DIR}
# MySQL 全量备份
echo "[$(date)] 开始 MySQL 备份..."
docker exec prod-mysql mysqldump -uroot -p${DB_ROOT_PASSWORD} \
--all-databases --single-transaction --routines --triggers \
| gzip > ${BACKUP_DIR}/full_${DATE}.sql.gz
# 验证备份文件
if [ -f "${BACKUP_DIR}/full_${DATE}.sql.gz" ]; then
SIZE=$(du -h "${BACKUP_DIR}/full_${DATE}.sql.gz" | cut -f1)
echo "[$(date)] 备份完成: full_${DATE}.sql.gz (${SIZE})"
else
echo "[$(date)] 备份失败!"
exit 1
fi
# 清理过期备份
echo "[$(date)] 清理 ${RETENTION_DAYS} 天前的备份..."
find ${BACKUP_DIR} -name "full_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
# 上传到远程存储(可选)
# aws s3 sync ${BACKUP_DIR} s3://my-backup-bucket/mysql/
echo "[$(date)] 备份任务完成"
bash
# 添加到 crontab(每天凌晨3点执行)
# crontab -e
0 3 * * * /path/to/backup.sh >> /var/log/mysql-backup.log 2>&1
13.6 动手实验
实验 13.1:部署完整生产环境
bash
# 1. 创建项目结构
mkdir -p ~/production/{nginx/{conf.d,ssl},php,mysql/{conf.d,initdb},redis,app/public,logs}
# 2. 创建 docker-compose.prod.yml(使用上面的配置)
# 3. 创建配置文件(使用上面的配置)
# 4. 创建 .env 文件
cat > .env << 'EOF'
DB_NAME=myapp
DB_USER=appuser
DB_PASSWORD=apppass123
DB_ROOT_PASSWORD=rootpass123
REDIS_PASSWORD=redis123
EOF
# 5. 启动生产环境
docker compose -f docker-compose.prod.yml up -d
# 6. 查看服务状态
docker compose -f docker-compose.prod.yml ps
# 7. 查看日志
docker compose -f docker-compose.prod.yml logs -f
# 8. 测试访问
curl -k https://localhost
实验 13.2:灰度发布
bash
# 1. 部署 v1.0 版本
docker compose up -d
# 2. 验证 v1.0
curl http://localhost
# 输出: Hello from v1.0
# 3. 启动 v2.0 版本(金丝雀)
docker compose up -d --scale app-v2=1
# 4. 逐步切换流量
# 修改 Nginx 配置,添加 v2.0 后端
# 5. 验证
curl http://localhost
# 部分请求返回 v1.0,部分返回 v2.0
# 6. 完全切换到 v2.0
# 修改 Nginx 配置,移除 v1.0 后端
# 7. 停止 v1.0
docker compose stop app-v1
13.7 本章小结
| 生产要素 | 关键配置 |
|---|---|
| 负载均衡 | Nginx 反向代理、多副本 |
| 数据持久化 | 命名卷、定期备份 |
| 安全加固 | 非 root 用户、只读文件系统 |
| 监控告警 | Prometheus + Grafana |
| 日志管理 | ELK/Loki 日志收集 |
| 灰度发布 | 蓝绿/金丝雀/滚动更新 |
| 备份恢复 | 自动化备份脚本 |
13.8 课后练习
- 实践题:部署完整的 Nginx + PHP + MySQL + Redis 生产环境。
- 进阶题:配置灰度发布策略,实现 v1.0 到 v2.0 的平滑升级。
- 运维题:编写自动化备份脚本,并添加到 crontab。
📖 下一章:Docker 与 CI/CD ------ 实现自动化构建和部署