Docker服务器部署实战

结论: 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 已弃用)。

yaml
# 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 只读挂载。

日常运维命令:

bash
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 的示例,体现多阶段构建和最佳实践:

dockerfile
# ./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 出网绝大多数场景
hostnetwork_mode: host直接用宿主机网络栈,性能最好对网络性能极敏感、端口不冲突
nonenetwork_mode: none无网络离线批处理
自定义 bridgenetworks: 定义支持 DNS 服务名解析、网络隔离生产推荐
internal: truenetworks.backend.internal禁止出网数据库等后端服务

同一 Compose 网络内,容器可直接用服务名互访(如 php 连 mysql:3306),Docker 内嵌 DNS 自动解析。这就是为什么不需要 --link。

3.2 数据持久化

容器文件系统是临时的,容器删除后写入层的数据全部丢失。持久化只有两种正确方式:

bash
# 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 类工具可实现自动重建。

bash
# 查看健康状态
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 日志无大小限制,是生产环境磁盘被撑爆的头号原因。必须限制:

yaml
# 单个服务配置
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 杀掉其他进程(包括数据库)。

yaml
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回收空间又保住数据

自动化更新与清理示例:

bash
# 只清理 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

常见误区 / 排错提示

  1. 把数据只存在容器里:docker compose down(不带 -v)虽然保留卷,但很多人误以为容器内 /data 目录也在,实际上没挂卷的目录会随容器消失。任何需要留存的路径必须挂卷。
  2. docker system prune -a --volumes 一把梭:这会删除所有未被容器引用的卷,包括你刚刚停掉的数据库卷。清理时绝不加 --volumes。
  3. 镜像用 latest 且配了自动更新:某天重启后 MySQL 从 8.4 跳到 9.x,数据字典不兼容直接起不来。生产必须锁版本,升级要在测试环境验证后再手动改 tag。
  4. 容器内时间不对:容器默认使用 UTC 时区,Java 应用或定时任务会出现时间偏差。挂载 -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro,或设环境变量 TZ=Asia/Shanghai。
  5. 端口映射冲突:第二个 MySQL 容器映射 3306:3306 会报 "port is already allocated"。要么改宿主端口 3307:3306,要么不映射、让同网络的容器用服务名访问。