硬核实践:使用 Docker 部署生产级 MySQL

Docker 让 MySQL 的部署变得极其简单,但"简单"背后藏着无数可能踩坑的细节。本文将带你从一条 docker run 命令开始,逐步深入到 Docker Compose 编排、安全加固与日常运维,力争让每一个参数都清晰透明,让每一次部署都安全、可用、高效。


1. 使用 docker run 快速部署

1.1 最简启动

bash 复制代码
docker run --name mysql-test -e MYSQL_ROOT_PASSWORD=root -d mysql:8.0

这条命令会从 Docker Hub 拉取 mysql:8.0 镜像并运行一个容器。但强烈不推荐在生产中这样使用:没有数据持久化,容器删除数据即丢失;密码直接暴露在命令行中,可被 docker inspecthistory 轻易获取。

1.2 生产级 docker run 命令及参数详解

一条面向生产的启动命令应该这样写:

bash 复制代码
docker run -d \
  --name mysql-prod \
  --restart=unless-stopped \
  -p 3306:3306 \
  -v /etc/localtime:/etc/localtime:ro \
  -v mysql-data:/var/lib/mysql \
  -v /opt/mysql/conf:/etc/mysql/conf.d:ro \
  -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password \
  -e MYSQL_DATABASE=app_db \
  -e MYSQL_USER=app_user \
  -e MYSQL_PASSWORD_FILE=/run/secrets/mysql_app_password \
  mysql:8.0 \
  --character-set-server=utf8mb4 \
  --collation-server=utf8mb4_unicode_ci

下面逐一拆解每个参数:

  • -d:后台运行容器。

  • --name mysql-prod:容器名称,便于后续管理。

  • --restart=unless-stopped :重启策略。unless-stopped 表示除非手动 docker stop,否则 Docker 服务重启时容器也会自动启动。生产环境推荐使用 unless-stoppedalways

  • -p 3306:3306 :端口映射 宿主机端口:容器端口。宿主机不一定是 3306,例如可以映射为 -p 3307:3306 避免冲突。如果只允许本地访问,可用 -p 127.0.0.1:3306:3306

  • -v /etc/localtime:/etc/localtime:ro:只读挂载宿主机时区文件,保证容器时间与宿主机一致,避免时区引发的时间错乱。

  • -v mysql-data:/var/lib/mysql :数据卷挂载。mysql-data 是 Docker 命名卷 ,由 Docker 管理,存储在 /var/lib/docker/volumes/ 下。若想显式指定宿主机目录(绑定挂载),可写作 -v /data/mysql:/var/lib/mysql。务必确保持久化。

  • -v /opt/mysql/conf:/etc/mysql/conf.d:ro :挂载自定义 MySQL 配置目录。容器启动时 /etc/mysql/conf.d/ 下的所有 .cnf 文件会被自动加载,且优先于默认配置。使用 :ro 只读挂载更安全。

  • 环境变量(官方镜像的核心入口):

    • MYSQL_ROOT_PASSWORD:直接设置 root 密码。不安全,容易泄露。

    • MYSQL_ROOT_PASSWORD_FILE安全替代方案 ,指定一个包含密码的文件路径。结合 Docker Swarm 的 secrets 或直接使用文件挂载,可避免密码出现在环境变量、命令行和 docker inspect 中。

    • MYSQL_DATABASE:容器启动时自动创建的数据库名。

    • MYSQL_USER / MYSQL_PASSWORD:自动创建的用户及其密码,该用户会被授予对 MYSQL_DATABASE 的全部权限。同样建议使用 MYSQL_PASSWORD_FILE

    • MYSQL_RANDOM_ROOT_PASSWORD:设为 yes 将生成随机 root 密码,并输出到日志(docker logs mysql-prod)。测试环境可用。

    • MYSQL_ONETIME_PASSWORD:为 root 创建一次性密码,首次登录必须修改。强安全场景可配合使用。

  • 镜像后的参数 :所有在 mysql:8.0 之后的参数都会传递给 MySQL 服务进程(mysqld)。这里我们传入了 --character-set-server=utf8mb4--collation-server=utf8mb4_unicode_ci,确保默认字符集为支持 emoji 的 utf8mb4。

1.3 进入容器与连接 MySQL

进入容器 shell

bash 复制代码
docker exec -it mysql

容器内默认提供 mysql 客户端,可直接连接:

bash 复制代码
mysql -

如果宿主机安装了 mysql 客户端,也可直接通过暴露的端口连接:

bash 复制代码
mysql -h 127.0.0.1 -P 3306 -u root -p

1.4 首次安全配置

若使用了 MYSQL_RANDOM_ROOT_PASSWORD 或未设置密码,root 密码会打印在日志中:

bash 复制代码
docker logs mysql-prod 2>&1 | grep "GENERATED ROOT PASSWORD"

登录后立即修改 root 密码:

bash 复制代码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_strong_password';
FLUSH PRIVILEGES;

若需允许 root 远程连接(生产环境强烈不推荐),需更改 host:

bash 复制代码
UPDATE mysql.user SET host = '%' WHERE user = 'root';

2. 高级配置与性能调优

2.1 通过挂载配置文件自定义 MySQL

创建宿主机配置文件 /opt/mysql/conf/my.cnf,挂载到 /etc/mysql/conf.d/。例如:

bash 复制代码
[mysqld]
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
max_connections = 500
sql_mode = STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ENGINE_SUBSTITUTION
default-time-zone = '+08:00'

[client]
default-character-set = utf8mb4

注意 :MySQL 8.0 对配置要求更严格,sql_mode 等参数须与版本匹配。配置修改后重启容器即可生效。

2.2 开启查询日志

调试时可临时开启慢查询日志或通用查询日志,通过命令行参数传递:

bash 复制代码
docker run ... mysql:8.0 \
  --general-log=1 \
  --general-log-file=/var/log/mysql/general.log \
  --slow-query-log=1 \
  --slow-query-log-file=/var/log/mysql/slow.log \
  --long-query-time=2

但生产环境建议仅在配置文件中按需开启,并使用数据卷持久化日志目录。

2.3 时区设置

除了挂载 /etc/localtime,也可在配置中指明:

bash 复制代码
default-time-zone 

同时确保 TZ 环境变量或 localtime 挂载正确,避免 now() 时间偏差。


3. Docker Compose 部署方案

3.1 单实例 docker-compose.yml

Compose 可统一管理多个服务、网络和卷,让部署声明化、可重复。以下是一个生产级单实例模板:

bash 复制代码
version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: mysql-prod
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE: app_db
      MYSQL_USER: app_user
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_app_password
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./mysql/conf:/etc/mysql/conf.d:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - backend
    secrets:
      - mysql_root_password
      - mysql_app_password
    command:
      - "--character-set-server=utf8mb4"
      - "--collation-server=utf8mb4_unicode_ci"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$(cat /run/secrets/mysql_root_password)"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 60s

volumes:
  mysql-data:

networks:
  backend:

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password.txt
  mysql_app_password:
    file: ./secrets/mysql_app_password.txt
  • secrets :Docker Compose 支持从文件读取密码,文件内容即为密码(需确保文件权限为 600,不要提交到版本控制)。在服务内通过 /run/secrets/<secret_name> 访问,与环境变量 _FILE 搭配,实现密码与配置分离。

  • healthcheck :利用 mysqladmin ping 检测 MySQL 是否可用。这里用 $$ 转义 $ 来执行 shell 命令读取密码文件。start_period 给初始启动留足时间。

  • command :覆盖默认 CMD,传入 mysqld 参数,写法与 docker run 尾部参数完全一致。

启动与停止:

bash 复制代码
docker-compose up -d
docker-compose down

3.2 使用 .env 环境变量文件

Compose 支持 .env 文件定义变量,避免在 yaml 中硬编码镜像版本或路径:

bash 复制代码
MYSQL_VERSION=8.0
HOST_PORT=3306

yaml 中引用:image: mysql:${MYSQL_VERSION},端口:"${HOST_PORT}:3306"

3.3 多实例与主从复制

可在同一个 Compose 文件中定义多个 MySQL 服务,实现主从复制或读写分离。例如定义 mysql-mastermysql-slave,并利用不同的 server-id、配置文件和数据卷。此处不再展开,但其核心仍是参数与卷的管理。


4. 安全加固实践

数据库安全无小事,以下措施务必在生产落地:

  1. 绝不使用明文密码环境变量 :优先使用 _FILE 系列变量配合 Docker secrets 或 Kubernetes secrets。

  2. 删除匿名用户和测试库:官方镜像会自动执行初始化脚本,但如果使用自定义初始化,应手动执行:

    bash 复制代码
    DELETE FROM mysql.user WHERE User='';
    DROP DATABASE IF EXISTS test;
    DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
    FLUSH PRIVILEGES;
  3. 限制 root 远程登录 :确保 root 仅能从 localhost 连接。查看权限:

    bash 复制代码
    SELECT host, user FROM mysql.user;

    若存在 root@'%',执行:

    bash 复制代码
    DROP USER 'root'@'%';

    或修改为只能本地登录:

    bash 复制代码
    RENAME USER 
  4. 创建专用应用账户并细粒度授权:不要将 root 账号提供给应用。

    bash 复制代码
    CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_pass';
    GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'%';
    FLUSH PRIVILEGES;

    如果应用与 MySQL 在同一 Docker 网络,可限制 @'%' 为具体的容器 IP 或子网,例如 @'172.16.238.%'

  5. 网络隔离 :Compose 中创建独立 backend 网络,不对外暴露端口(ports 留空),仅让应用容器通过服务名访问。若需外部管理,可通过 SSH 隧道或 VPN 连接。

  6. TLS 加密 :对跨机房或非信任网络,应配置 MySQL 的 SSL。证书可挂载到容器并启用 require_secure_transport=ON


5. 日常运维与问题排查

5.1 数据备份与恢复

逻辑备份(跨版本兼容):

bash 复制代码
docker exec mysql-prod mysqldump -u root -p$ROOT_PASSWORD --single-transaction --all-databases > backup.sql

恢复

bash 复制代码
docker exec -i mysql-prod mysql -u root -p$ROOT_PASSWORD < backup.sql

物理备份 :直接备份数据卷。因 MySQL 运行时文件可能不一致,最好先停止容器或确保使用 FLUSH TABLES WITH READ LOCK。使用卷备份工具如 docker run --rm -v mysql-data:/data -v $(pwd):/backup alpine tar czf /backup/mysql-data.tar.gz -C /data .

5.2 查看日志

bash 复制代码
docker logs -f --tail 100 mysql-prod

若 MySQL 启动失败,日志是最直接的诊断源,常见错误如数据目录非空、权限错误、配置参数不支持等。

5.3 升级 MySQL 镜像

升级次要版本通常只需拉取新镜像,然后重建容器:

bash 复制代码
docker pull mysql:8.0.34
docker-compose down
docker-compose up -d

数据卷保持不变。升级主版本(如 5.7 → 8.0)必须预先进行兼容性检查,并在停服后使用 mysqldump 逻辑迁移或借助 mysql_upgrade(但 8.0 开始 mysql_upgrade 不再必要,但需注意数据字典升级会由容器自动完成)。

5.4 常见问题速查

问题现象 可能原因与解决
容器反复重启 docker logs 查看,多为配置错误或数据目录权限问题,检查挂载卷所有权(MySQL 用户 uid 通常为 999)
端口冲突 修改宿主机映射端口 -p 3307:3306,或停用占用端口的服务
Access denied for root 密码错误或被重置。使用 --skip-grant-tables 模式进入修复(有风险)
中文乱码 未正确设置字符集,务必在启动参数或配置中指定 utf8mb4
数据卷占用空间过大 定期清理 binlog,设置 expire_logs_days,优化表空间
mbind: Operation not permitted 警告 通常无害,可添加 --security-opt seccomp=unconfined 或调整内核参数,但非必要不推荐随意放权

结语

Docker 给了我们一把快速部署 MySQL 的瑞士军刀,但真正让它安全、高效、可维护的,是那些隐藏在参数背后的细节:密码管理、数据卷策略、网络隔离、健康检查与声明式配置。希望这篇文章能够成为你手中的参考手册,让每一次敲下 docker rundocker-compose up 时,都充满信心。