结论: Docker 服务器部署的实战主线是"一个 docker-compose.yml 描述整套服务"。核心要点:数据放命名卷或绑定目录、镜像 tag 锁死具体版本、配置 healthcheck 与 restart: unless-stopped、用 deploy.resources 或 mem_limit 限制资源、用 logging 驱动控制日志体积、生产环境只暴露必要端口并用 Nginx 统一入口。
引言:从"能跑起来"到"敢放到生产"
docker run -d -p 80:80 nginx 三秒钟就能把服务跑起来,但这离"生产可用"差得远。真正的容器化生产部署要回答几个问题:数据存在哪、容器挂了会不会自动拉起、日志会不会把磁盘撑爆、一个容器吃光内存会不会拖垮整机、如何做到零停机更新。
Docker 的价值不在"跑起来快",而在"环境可复现"。把整套服务写成一份 Compose 文件后,换服务器、扩容新节点、灾难恢复都变成"拷文件 + 一条命令"。本篇按实际生产部署的顺序展开:Compose 编排 → Dockerfile 构建 → 网络与卷 → 健康检查 → 日志 → 资源限制 → 生产检查清单。
一、Compose 编排一套 LNMP 生产环境
Docker Compose 用一个 YAML 文件定义多容器应用,是中小规模部署最实用的工具。下面是一份接近生产标准的配置(截至 2026 年,Compose v2 已内置于 docker compose,旧版 docker-compose 已弃用)。
# docker-compose.yml
name: myapp
services:
nginx:
image: nginx:1.27-alpine
container_name: myapp-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
- static-files:/var/www/static:ro
depends_on:
php:
condition: service_healthy
networks:
- frontend
- backend
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
php:
build:
context: ./php
dockerfile: Dockerfile
container_name: myapp-php
expose:
- "9000"
volumes:
- app-code:/var/www/html
- static-files:/var/www/static
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.5"
memory: 1G
mysql:
image: mysql:8.4
container_name: myapp-mysql
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_pw
MYSQL_DATABASE: appdb
MYSQL_USER: appuser
MYSQL_PASSWORD_FILE: /run/secrets/mysql_user_pw
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d:ro
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--innodb-buffer-pool-size=1G
--max-connections=300
secrets:
- mysql_root_pw
- mysql_user_pw
networks:
- backend
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p$(cat /run/secrets/mysql_root_pw)"]
interval: 30s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: myapp-redis
command: redis-server --appendonly yes --requirepass_file /run/secrets/redis_pw --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
secrets:
- redis_pw
networks:
- backend
restart: unless-stopped
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 内部网络,容器无法访问外网,数据库更安全
volumes:
mysql-data:
redis-data:
app-code:
static-files:
secrets:
mysql_root_pw:
file: ./secrets/mysql_root_pw.txt
mysql_user_pw:
file: ./secrets/mysql_user_pw.txt
redis_pw:
file: ./secrets/redis_pw.txt这份配置体现的关键实践:
- 网络分层:
frontend面向公网,backend设internal: true——数据库和 Redis 无法主动访问外网,也无法从外网直接访问。 - 敏感信息用
secrets(以文件形式挂载到/run/secrets/),而不是明文写environment,避免docker inspect泄露密码。 depends_on配合condition: service_healthy,确保 Nginx 等 PHP-FPM 真正就绪才启动。- 静态文件用独立卷在 Nginx 与 PHP 间共享,Nginx 只读挂载。
日常运维命令:
docker compose up -d # 后台启动
docker compose ps # 查看状态(含健康状态)
docker compose logs -f --tail=200 php # 跟踪日志
docker compose exec mysql bash # 进入容器
docker compose up -d --force-recreate nginx # 强制重建单个服务
docker compose pull && docker compose up -d # 更新镜像(注意 tag 已锁定时才会真正更新)
docker compose down # 停止并移除容器(卷保留!)
docker compose down -v # 危险:连同卷一起删除,数据丢失二、Dockerfile:把应用打成镜像
Dockerfile 的质量直接决定镜像大小、构建速度和安全性。以下是 PHP-FPM 的示例,体现多阶段构建和最佳实践:
# ./php/Dockerfile
FROM php:8.3-fpm-alpine AS builder
# 安装编译依赖(构建阶段)
RUN apk add --no-cache $PHPIZE_DEPS libzip-dev libpng-dev \
&& docker-php-ext-configure zip \
&& docker-php-ext-install -j$(nproc) pdo_mysql gd opcache zip
# ---- 运行阶段 ----
FROM php:8.3-fpm-alpine
COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
RUN apk add --no-cache libzip libpng \
&& rm -rf /var/cache/apk/*
COPY opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY www.conf /usr/local/etc/php-fpm.d/www.conf
COPY src/ /var/www/html/
# 使用非 root 用户运行
RUN addgroup -g 1000 -S app && adduser -u 1000 -S app -G app \
&& chown -R app:app /var/www/html
USER app
WORKDIR /var/www/html
EXPOSE 9000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ["/usr/local/bin/php-fpm-healthcheck"]
CMD ["php-fpm"]Dockerfile 要点清单:
- 多阶段构建:编译依赖留在 builder 阶段,运行镜像只留运行时库,体积可缩小一半以上。
- 合并
RUN并清理缓存:每条RUN都会生成一个镜像层,apk add后要rm -rf /var/cache/apk/*才能生效。 .dockerignore必须配置:排除.git、node_modules、日志和本地配置,否则构建上下文巨大且可能把密钥打进镜像。- 不要用 root 运行:
USER app可限制容器内权限。 - 基础镜像优先
-alpine变体(体积约 1/5),但注意 musl libc 与某些扩展的兼容性问题。 - 镜像 tag 锁死版本(
php:8.3-fpm-alpine),不用latest。
三、网络与卷:容器化最容易踩坑的两块
3.1 网络模式对比
| 模式 | 命令/配置 | 特点 | 适用 |
|---|---|---|---|
bridge(默认) | network_mode: bridge | 容器间通过服务名互访,NAT 出网 | 绝大多数场景 |
host | network_mode: host | 直接用宿主机网络栈,性能最好 | 对网络性能极敏感、端口不冲突 |
none | network_mode: none | 无网络 | 离线批处理 |
| 自定义 bridge | networks: 定义 | 支持 DNS 服务名解析、网络隔离 | 生产推荐 |
internal: true | networks.backend.internal | 禁止出网 | 数据库等后端服务 |
同一 Compose 网络内,容器可直接用服务名互访(如 php 连 mysql:3306),Docker 内嵌 DNS 自动解析。这就是为什么不需要 --link。
3.2 数据持久化
容器文件系统是临时的,容器删除后写入层的数据全部丢失。持久化只有两种正确方式:
# 1. 命名卷(推荐,Docker 管理生命周期,便于备份迁移)
docker volume create mysql-data
docker run -d -v mysql-data:/var/lib/mysql mysql:8.4
# 2. 绑定挂载(适合配置文件、代码目录,便于宿主机直接编辑)
docker run -d -v /data/app:/var/www/html myapp:1.0
# 查看卷的实际位置与容量占用
docker volume inspect mysql-data
docker system df -v
# 备份卷:起一个临时容器挂载源卷和当前目录打包
docker run --rm -v mysql-data:/from -v "$PWD":/to alpine \
tar czf /to/mysql-$(date +%F).tar.gz -C /from .
# 恢复
docker run --rm -v mysql-data:/to -v "$PWD":/from alpine \
sh -c "rm -rf /to/* && tar xzf /from/mysql-2026-01-01.tar.gz -C /to"要点清单:
- 数据库用命名卷,配置文件用绑定挂载(
:ro只读更安全)。 - 卷的备份不能靠"拷贝容器",必须起临时容器打包或用
docker compose down后备份卷目录。 - 定期检查
docker system df,未使用的镜像、容器、构建缓存会悄悄吃掉几十 GB。
四、健康检查、日志与资源限制
4.1 健康检查(healthcheck)
健康检查让 Docker 知道容器是否"真的可用",而不只是"进程还在"。它比单纯 restart: always 更智能——配合 autoheal 类工具可实现自动重建。
# 查看健康状态
docker inspect --format '{{.State.Health.Status}}' myapp-mysql
# starting / healthy / unhealthy
# 查看最近几次健康检查结果
docker inspect --format '{{json .State.Health.Log}}' myapp-mysql | python3 -m json.tool注意:healthcheck 只标记状态,不会自动重启容器。若要自动恢复,可配合 autoheal 容器(监听 unhealthy 事件并 docker restart),或在 Compose 中依赖 depends_on: condition: service_healthy 控制启动顺序。
4.2 日志驱动
默认 json-file 日志无大小限制,是生产环境磁盘被撑爆的头号原因。必须限制:
# 单个服务配置
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
# 全局默认(/etc/docker/daemon.json)
{
"log-driver": "json-file",
"log-opts": { "max-size": "20m", "max-file": "5" },
"live-restore": true
}| 驱动 | 特点 | 适用 |
|---|---|---|
json-file | 默认,docker logs 可用,需限大小 | 单机、中小规模 |
local | 高效本地格式,自动轮转 | 单机推荐替代 json-file |
journald | 交给 systemd-journald | 已用 journald 统一管理 |
syslog / fluentd / loki | 集中式日志 | 多机、需要检索分析 |
4.3 资源限制
不设限制的容器可能吃光整机内存,触发 OOM 杀掉其他进程(包括数据库)。
services:
app:
deploy:
resources:
limits:
cpus: "2.0" # 最多使用 2 个核
memory: 2G # 硬限制,超过触发容器内 OOM
reservations:
memory: 512M # 软预留或用命令行:docker run -d --memory=2g --cpus=2 --memory-swap=2g myapp:1.0。注意 --memory-swap 与 --memory 相等表示禁用 swap。
五、生产环境注意事项清单
| 项目 | 做法 | 原因 |
|---|---|---|
| 镜像版本 | 锁死 tag(mysql:8.4),不用 latest | 避免自动升级导致不兼容 |
| 数据持久化 | 命名卷 + 定期异地备份 | 容器删除不影响数据 |
| 重启策略 | restart: unless-stopped + Docker 开机自启 | 机器重启后服务自动恢复 |
| 端口暴露 | 只映射 80/443,数据库只用 expose | 减少攻击面 |
| 运行用户 | 容器内 USER 非 root | 降低逃逸风险 |
| 日志 | 限制 max-size / max-file | 防止磁盘撑爆 |
| 资源 | 设置 memory / cpus 限制 | 防止单容器拖垮整机 |
| 密钥 | 用 secrets 或环境变量文件,不进镜像 | 避免 docker inspect 泄露 |
| 镜像来源 | 官方镜像或自建私有仓库,扫描漏洞 | 供应链安全 |
| 更新 | docker compose pull + 灰度更新,保留回滚版本 | 出问题能快速回退 |
| 清理 | 定期 docker system prune,但不要加 -a --volumes | 回收空间又保住数据 |
自动化更新与清理示例:
# 只清理 24 小时前的悬空镜像与构建缓存,保留在用资源
docker image prune -f --filter "until=24h"
docker builder prune -f --filter "until=72h"
# 定时检查磁盘与容器状态(crontab -e)
0 3 * * * /usr/bin/docker system df >> /var/log/docker-df.log 2>&1常见误区 / 排错提示
- 把数据只存在容器里:
docker compose down(不带-v)虽然保留卷,但很多人误以为容器内/data目录也在,实际上没挂卷的目录会随容器消失。任何需要留存的路径必须挂卷。 docker system prune -a --volumes一把梭:这会删除所有未被容器引用的卷,包括你刚刚停掉的数据库卷。清理时绝不加--volumes。- 镜像用
latest且配了自动更新:某天重启后 MySQL 从 8.4 跳到 9.x,数据字典不兼容直接起不来。生产必须锁版本,升级要在测试环境验证后再手动改 tag。 - 容器内时间不对:容器默认使用 UTC 时区,Java 应用或定时任务会出现时间偏差。挂载
-v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro,或设环境变量TZ=Asia/Shanghai。 - 端口映射冲突:第二个 MySQL 容器映射
3306:3306会报 "port is already allocated"。要么改宿主端口3307:3306,要么不映射、让同网络的容器用服务名访问。
企业QQ咨询




