不少团队折腾 Docker 镜像时,都有一个绕不开的痛点:MySQL 官方镜像确实一行docker pull mysql:8.4就能用,可真到了生产环境,时区不对、字符集不合规、插件版本不匹配、内网拉不到镜像、安全审计要求逐层追溯,官方镜像那套黑盒根本应付不来。我自己也接过类似需求——基于 Ubuntu 和 CentOS 两套基础镜像,分别构建出 MySQL 8.4 自定义镜像,整个过程中踩了不少坑,也沉淀了一套相对稳定的构建方案。
这篇文章就把完整过程拆开讲清楚:从基础镜像怎么选、Dockerfile 怎么写、初始化脚本怎么设计,到构建完怎么验证、遇到问题怎么排查,全部按实操来。适合刚接触 Dockerfile、想用 MySQL 8.4 定制镜像的读者,也适合正在做内部数据库镜像基线化的运维同学参考。
1. 为什么非要自己构建 MySQL 镜像
1.1 官方镜像好用,但总有你想改的地方
官方mysql:8.4镜像最大的优点是省事,但这同时也意味着你接受了一套别人定好的默认值。
第一个问题是时区。官方镜像默认 UTC,国内业务库如果不改,日志时间、NOW()函数返回的时间全部差 8 小时。每次启动容器都要记得加-e TZ=Asia/Shanghai,但还得保证镜像里装了 tzdata,否则环境变量白设。与其每次启动时碰运气,不如在镜像里把时区直接固化。
第二个问题是字符集和排序规则。MySQL 8.x 默认字符集虽然是utf8mb4,但排序规则是utf8mb4_0900_ai_ci,而老业务很多习惯用utf8mb4_general_ci,或者需要在连接层强制utf8mb4。官方镜像的默认配置不会替你考虑这些,必须自己写my.cnf注入。
第三个问题是认证插件。MySQL 8.4 中mysql_native_password插件默认被禁用,默认认证插件变成了caching_sha2_password。如果你的业务里还有老版本 PHP、老版 JDBC 驱动,连不上就是连不上。这个兼容性开关也得靠自定义配置去打开。
更现实的场景是内网环境。很多公司生产网和公网隔离,docker pull根本拉不了 Docker Hub。你需要在一台能联网的构建机上把基础镜像和 MySQL 安装包准备好,然后在内网用docker load导入,再用 Dockerfile 构建出标准化的业务镜像。这种情况下,自己掌控整个构建链路反而成了硬性需求。
1.2 Ubuntu 与 CentOS,选谁当底座
基础镜像的选型本质上是选运行时环境。Ubuntu 和 CentOS 在包管理器、glibc 版本、系统初始化方式上都有差异,直接影响到 MySQL 安装方式和你后续维护的心态。
| 对比项 | Ubuntu 24.04 LTS | CentOS Stream 9 / Rocky Linux 9 |
|---|---|---|
| 包管理器 | apt / dpkg | dnf / yum |
| glibc 版本 | 2.39 | 2.34 |
| MySQL 官方支持 | 有 APT 仓库,也可用通用 TAR 包 | 有 YUM 仓库,也可用通用 TAR 包 |
| 镜像体积 | 约 80MB 起步 | 约 160MB 起步 |
| 适用团队 | 习惯 Debian 系、追求镜像精简 | 习惯 RHEL 系、已有 RPM 运维基线 |
这里特别提醒一个坑:不要用 CentOS 7 当 MySQL 8.4 的底座。MySQL 8.4 的官方 Linux 通用包要求 glibc 2.28 以上,而 CentOS 7 的 glibc 是 2.17。就算你强行装上,启动时大概率会碰到类似version 'GLIBC_2.28' not found的报错,而且这种底层依赖问题基本没有优雅解法。如果团队里有历史包袱、必须用 CentOS 7,建议老老实实跑 MySQL 5.7 或者 8.0 早期版本,不要跟 8.4 死磕。
我这次的方案是 Ubuntu 24.04 和 CentOS Stream 9 各出一套。前者给容器化和微服务团队用,后者给传统运维体系用,两套镜像的 MySQL 版本、配置基线完全一致,只是底层系统不同。
1.3 构建前的需求清单
动手写 Dockerfile 之前,先把需求列清楚,否则构建到一半才想起来要加配置,又要重新来一遍。我的需求清单如下:
- MySQL 版本固定为 8.4.x(具体小版本写死,避免
latest漂移) - 数据目录固定为
/var/lib/mysql,方便挂载持久化 - 时区固定为
Asia/Shanghai - 字符集和排序规则固定为
utf8mb4/utf8mb4_general_ci - 默认开启
binlog(业务要求),日志目录独立 - root 用户密码通过环境变量或初始化 SQL 注入
- 容器内以非 root 用户运行 MySQL 进程
- 内置健康检查脚本,方便编排系统探活
需求清单的本质是把“变量”和“常量”分开。常量固化在镜像里,变量留给运行时通过环境变量或挂载文件注入。这样同一套镜像可以反复复用,而不是每个项目都重新构建一个。
2. Dockerfile 核心指令拆解
2.1 从纯基础镜像开始,还是用官方仓库?
两种路线:一种是在基础镜像里直接配 MySQL 官方 APT/YUM 仓库,然后apt install mysql-server;另一种是下载 MySQL 官方 Generic Linux TAR 包,解压后自己部署。
用官方仓库的好处是安装方便、依赖自动解决、后续升级也简单。缺点是构建时依赖外部网络,一旦仓库地址变更或者内网无法访问,整个构建就断了。而且 APT/YUM 仓库安装出来的目录结构和官方 TAR 包不完全一样,不同发行版之间差异更大。
我更倾向于 TAR 包方式:在一台可以联网的机器上下载好mysql-8.4.x-linux-glibc2.28-x86_64.tar.xz,然后通过COPY放进构建上下文。这样做的好处有三个:
- 构建过程可控,不依赖外部仓库在线状态
- Ubuntu 和 CentOS 两套镜像用同一个二进制包,版本绝对一致
- 便于离线环境复现,只要把 TAR 包和 Dockerfile 一起归档就行
TAR 包方式唯一的代价是你要自己处理依赖库,不过 MySQL 需要的依赖不多,后面会讲到。
2.2 FROM、ENV、ARG 的设计思路
FROM是 Dockerfile 的第一条指令,直接决定了整个镜像的软件生态。基础镜像有两种选法:一种是ubuntu:24.04、centos:stream9这种纯粹的操作系统镜像,另一种是带运行时的镜像比如eclipse-temurin。MySQL 场景选纯系统镜像就对了,没必要引入额外的运行环境。
ARG和ENV容易搞混。ARG只在构建阶段有效,用于传递构建参数;ENV会写入镜像环境变量,容器运行时依然存在。比如:
ARG MYSQL_VERSION=8.4.3 ENV MYSQL_HOME=/opt/mysqlMYSQL_VERSION只在RUN里用来拼接路径,不需要留在镜像里,所以用ARG。MYSQL_HOME后面启动脚本和客户端命令都要用,必须用ENV固化下来,运行时也能读取。
还要注意ENV的持久化问题。有人在RUN里执行export PATH=/opt/mysql/bin:$PATH,以为镜像里就永久生效了,其实每次RUN都是新的 shell,export 只在那一条指令里有效。想让 PATH 永久生效,必须写ENV PATH=/opt/mysql/bin:$PATH。
2.3 COPY 指令与初始化脚本机制
Dockerfile 里的COPY看起来简单,但它决定了构建上下文的大小和镜像层的多少。我习惯把配置文件和初始化脚本集中放在一个assets目录下,然后用COPY assets/ /tmp/assets/一次性拷进去,后面由一个RUN统一排版到目标位置。
MySQL 初始化脚本要重点设计。我的做法是:把需要执行的 SQL(建用户、改密码、建库)写成.sql文件,放到镜像的/docker-entrypoint-initdb.d/目录。容器首次启动时,入口脚本检测到数据目录为空,就会先执行mysqld --initialize-insecure初始化数据目录,然后启动临时实例并遍历执行initdb.d下的 SQL 脚本,最后切换到正式启动流程。这个机制学的是官方镜像的思路,但实现完全由自己控制,可定制性更强。
if [ ! -d "$MYSQL_DATA_DIR/mysql" ]; then mysqld --initialize-insecure --user=mysql --datadir="$MYSQL_DATA_DIR" fi--initialize-insecure会生成一个无密码的 root 用户,方便后续脚本无交互执行 SQL。如果直接用--initialize,系统会生成一个临时密码并写到日志里,脚本里再想去自动执行 SQL 就麻烦了,得先解析日志提取密码。所以自动化初始化场景选--initialize-insecure更合适,但一定要注意:这个无密码状态只存在于初始化阶段,初始化完成后必须立刻设置密码。
2.4 数据目录、权限与 USER
MySQL 非常强调目录权限。如果容器里直接用 root 用户启动mysqld,MySQL 会拒绝启动或报警告。所以镜像里需要创建mysql系统用户,并把数据目录、日志目录的属主改掉。
RUN groupadd -r mysql && useradd -r -g mysql -s /sbin/nologin mysql \ && mkdir -p /var/lib/mysql /var/log/mysql /var/run/mysqld \ && chown -R mysql:mysql /var/lib/mysql /var/log/mysql /var/run/mysqld这里有个细节:/var/run/mysqld是 socket 文件所在的目录,MySQL 运行时要往里面写文件。如果不提前创建并授权,启动时会报Can't create/write to file '/var/run/mysqld/mysqld.sock'。这个目录很容易被忽略,我第一版镜像就栽在这里。
Dockerfile 最后通过USER mysql切换用户,这是容器安全的基本要求。但要注意,USER mysql之后,ENTRYPOINT和CMD都会以mysql身份执行,所以入口脚本里如果需要 root 权限做操作(比如chown挂载卷),就会失败。解决思路是:入口脚本开头检测当前用户,如果是 root 就先把权限整理好,再通过gosu或su切换到 mysql 用户执行mysqld。
3. 完整实操:Ubuntu 与 CentOS 双版本构建
3.1 先准备 MySQL 安装包和配置资产
确认好版本号之后,先去 MySQL 官网下载 Generic Linux TAR 包。以 8.4.3 为例,文件名一般是mysql-8.4.3-linux-glibc2.28-x86_64.tar.xz。下载完放到构建目录下,和 Dockerfile 同级的assets目录里。
然后准备my.cnf,这是 MySQL 行为定制的核心。我的配置如下:
[mysqld] user = mysql port = 3306 bind-address = 0.0.0.0 socket = /var/run/mysqld/mysqld.sock pid-file = /var/run/mysqld/mysqld.pid datadir = /var/lib/mysql log-error = /var/log/mysql/error.log character-set-server = utf8mb4 collation-server = utf8mb4_general_ci default-time-zone = '+08:00' server-id = 1 log-bin = /var/log/mysql/mysql-bin binlog_format = ROW expire_logs_days = 7 max_connections = 500 innodb_buffer_pool_size = 1G innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 skip-name-resolve mysql_native_password = ON [client] socket = /var/run/mysqld/mysqld.sock default-character-set = utf8mb4用default-time-zone = '+08:00'而不是TZ环境变量,是为了让 MySQL 内部时区也统一,避免日志时间和业务时间对不上。skip-name-resolve是让 MySQL 不反查客户端域名,能减少连接延迟,但如果客户端 IP 变化频繁,需要谨慎使用。
mysql_native_password = ON是特意加的。8.4 默认禁用这个插件,但有些老客户端只支持它。等确认业务侧全部升级完成后,这行可以删掉,让镜像回到更安全的caching_sha2_password状态。
初始化 SQL 脚本init.sql:
ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; CREATE USER IF NOT EXISTS 'app'@'%' IDENTIFIED BY '${MYSQL_APP_PASSWORD}'; GRANT ALL PRIVILEGES ON `appdb`.* TO 'app'@'%'; FLUSH PRIVILEGES;实际生产中,脚本里的密码不应该直接写死,而是通过环境变量在容器运行时动态传入。所以更好的做法是写一个 shell 脚本,用环境变量拼接 SQL,再交给 mysql 执行。后面入口脚本里会体现。
3.2 Ubuntu 版 Dockerfile
FROM ubuntu:24.04 ARG MYSQL_VERSION=8.4.3 ARG MYSQL_TARBALL=mysql-${MYSQL_VERSION}-linux-glibc2.28-x86_64.tar.xz ENV MYSQL_HOME=/opt/mysql \ MYSQL_DATA_DIR=/var/lib/mysql \ MYSQL_LOG_DIR=/var/log/mysql \ MYSQL_RUN_DIR=/var/run/mysqld \ LANG=C.UTF-8 RUN apt-get update && apt-get install -y --no-install-recommends \ libaio1 \ libnuma1 \ libncurses6 \ tzdata \ curl \ gosu \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt COPY assets/${MYSQL_TARBALL} /tmp/ RUN tar -xJf /tmp/${MYSQL_TARBALL} -C /opt/ \ && mv /opt/mysql-${MYSQL_VERSION}-linux-glibc2.28-x86_64 /opt/mysql \ && rm -f /tmp/${MYSQL_TARBALL} RUN groupadd -r mysql && useradd -r -g mysql -s /sbin/nologin mysql \ && mkdir -p ${MYSQL_DATA_DIR} ${MYSQL_LOG_DIR} ${MYSQL_RUN_DIR} \ && chown -R mysql:mysql ${MYSQL_DATA_DIR} ${MYSQL_LOG_DIR} ${MYSQL_RUN_DIR} ENV PATH=${MYSQL_HOME}/bin:${PATH} COPY assets/my.cnf /etc/my.cnf COPY assets/docker-entrypoint.sh /usr/local/bin/ COPY assets/init/ /docker-entrypoint-initdb.d/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh EXPOSE 3306 ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld"]Ubuntu 的底层依赖在libaio1和libnuma1这两个包上体现得很明显。缺少libaio1时,MySQL 会直接报error while loading shared libraries: libaio.so.1。如果你的 Ubuntu 基础镜像版本较老,可能还需要libncurses5,但 24.04 里已经不需要了,这里保留libncurses6是为了保证mysql客户端工具正常。
3.3 CentOS Stream 9 版 Dockerfile
FROM centos:stream9 ARG MYSQL_VERSION=8.4.3 ARG MYSQL_TARBALL=mysql-${MYSQL_VERSION}-linux-glibc2.28-x86_64.tar.xz ENV MYSQL_HOME=/opt/mysql \ MYSQL_DATA_DIR=/var/lib/mysql \ MYSQL_LOG_DIR=/var/log/mysql \ MYSQL_RUN_DIR=/var/run/mysqld RUN dnf install -y \ libaio \ libnsl \ ncurses-libs \ tzdata \ curl \ gosu \ && dnf clean all WORKDIR /opt COPY assets/${MYSQL_TARBALL} /tmp/ RUN tar -xJf /tmp/${MYSQL_TARBALL} -C /opt/ \ && mv /opt/mysql-${MYSQL_VERSION}-linux-glibc2.28-x86_64 /opt/mysql \ && rm -f /tmp/${MYSQL_TARBALL} RUN groupadd -r mysql && useradd -r -g mysql -s /sbin/nologin mysql \ && mkdir -p ${MYSQL_DATA_DIR} ${MYSQL_LOG_DIR} ${MYSQL_RUN_DIR} \ && chown -R mysql:mysql ${MYSQL_DATA_DIR} ${MYSQL_LOG_DIR} ${MYSQL_RUN_DIR} ENV PATH=${MYSQL_HOME}/bin:${PATH} COPY assets/my.cnf /etc/my.cnf COPY assets/docker-entrypoint.sh /usr/local/bin/ COPY assets/init/ /docker-entrypoint-initdb.d/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh EXPOSE 3306 ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld"]CentOS Stream 9 的包名和 Ubuntu 略有区别,libaio在 RHEL 系里就叫这个名字,没有数字后缀。libnsl是可选的,某些 MySQL 工具会引用到,装上省心。
这里还要再强调一次:不要拿 CentOS 7 硬跑这个 Dockerfile。即使你把dnf换成yum,MySQL 8.4 的二进制在 CentOS 7 上依然会因为 glibc 版本过低起不来。如果团队确实只能用 CentOS 7,要么换 MySQL 版本,要么考虑写两层镜像:用 CentOS 7 做底层,但 MySQL 进程跑在一个独立的、基于较新发行版的容器里,通过网络互通。这种拆法复杂度高,不建议新手尝试。
3.4 入口脚本:初始化与启动的桥梁
入口脚本是整个镜像的大脑,负责数据目录初始化、SQL 注入、进程权限切换和最终启动。我的脚本结构如下:
#!/bin/bash set -e MYSQL_HOME=${MYSQL_HOME:-/opt/mysql} MYSQL_DATA_DIR=${MYSQL_DATA_DIR:-/var/lib/mysql} MYSQL_LOG_DIR=${MYSQL_LOG_DIR:-/var/log/mysql} MYSQL_RUN_DIR=${MYSQL_RUN_DIR:-/var/run/mysqld} if [ "$(id -u)" = "0" ]; then mkdir -p "$MYSQL_DATA_DIR" "$MYSQL_LOG_DIR" "$MYSQL_RUN_DIR" chown -R mysql:mysql "$MYSQL_DATA_DIR" "$MYSQL_LOG_DIR" "$MYSQL_RUN_DIR" exec gosu mysql "$0" "$@" fi if [ ! -d "$MYSQL_DATA_DIR/mysql" ]; then echo "Initializing MySQL data directory..." mysqld --initialize-insecure --user=mysql --datadir="$MYSQL_DATA_DIR" echo "Starting temporary server for initialization..." mysqld --daemonize --skip-networking --socket="$MYSQL_RUN_DIR/mysqld.sock" for f in /docker-entrypoint-initdb.d/*; do case "$f" in *.sql) echo "Executing $f" mysql --socket="$MYSQL_RUN_DIR/mysqld.sock" -uroot < "$f" ;; *.sh) echo "Executing $f" bash "$f" ;; esac done mysqladmin --socket="$MYSQL_RUN_DIR/mysqld.sock" -uroot shutdown fi exec "$@"这个脚本里有几个关键决策。第一,如果以 root 进入,先整理目录权限,再通过gosu降权执行自身。第二,数据目录为空时,用--initialize-insecure初始化,然后--daemonize启动一个临时实例,执行完初始化脚本后立刻shutdown。第三,初始化完的正式启动直接exec "$@",也就是执行mysqld,让它作为 PID 1 进程在前台运行。
关于--daemonize,这是在初始化脚本场景下比较顺手的方式。临时实例不需要保持前台,执行完 SQL 后关闭即可。正式实例一定不要加daemonize,否则容器会因为没有前台进程而退出。
3.5 构建、启动与功能验证
构建和启动命令如下:
docker build -t mysql-8.4:ubuntu -f Dockerfile.ubuntu ./ docker build -t mysql-8.4:centos -f Dockerfile.centos ./ docker run -d --name mysql-test \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD='Root@123456' \ -e MYSQL_APP_PASSWORD='App@123456' \ -v /data/mysql:/var/lib/mysql \ mysql-8.4:ubuntu注意,这里的MYSQL_ROOT_PASSWORD和MYSQL_APP_PASSWORD不会自动被入口脚本使用,除非你在init/目录下的.sh脚本里读取它们并生成 SQL。所以我的init/目录里放的是一个init-user.sh:
#!/bin/bash if [ -n "$MYSQL_ROOT_PASSWORD" ]; then mysql -uroot <<EOF ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; EOF fi if [ -n "$MYSQL_APP_PASSWORD" ]; then mysql -uroot <<EOF CREATE USER IF NOT EXISTS 'app'@'%' IDENTIFIED BY '${MYSQL_APP_PASSWORD}'; GRANT ALL PRIVILEGES ON appdb.* TO 'app'@'%'; FLUSH PRIVILEGES; EOF fi启动完成后,验证是否正常工作:
docker exec -it mysql-test mysql -uroot -p'Root@123456' -e "SELECT VERSION();" docker exec -it mysql-test mysql -uroot -p'Root@123456' -e "SHOW VARIABLES LIKE 'character_set_server';" docker exec -it mysql-test mysql -uroot -p'Root@123456' -e "SHOW VARIABLES LIKE 'time_zone';"如果输出里的版本是 8.4.x、字符集是utf8mb4、时区是+08:00,那镜像的核心需求就全部满足了。接下来再测一下持久化:手动往数据库里写点数据,然后docker restart mysql-test,确认数据还在,说明数据目录挂载和磁盘写入都正常。
4. 构建与运行中的高频问题排查
4.1 启动失败:缺少动态库
这是 Ubuntu 和 CentOS 上最容易踩的坑。镜像构建完,一启动容器就退出,docker logs里出现:
mysqld: error while loading shared libraries: libaio.so.1: cannot open shared object file原因就是 MySQL 官方 TAR 包依赖了这些动态库,但基础镜像默认没装。Ubuntu 上执行apt-get install -y libaio1 libnuma1,CentOS 上执行dnf install -y libaio就能解决。这里一定要在 Dockerfile 里显式安装,不要试图把宿主机的库文件拷进镜像,不同发行版的库路径和依赖关系不一样,硬拷容易引入更多问题。
排查技巧:构建时先不急着写完整 Dockerfile,可以进入基础镜像手动解压 TAR 包,然后直接跑一下mysqld --version,看报错缺什么就装什么。这一步提前做,能省很多构建迭代时间。
4.2 挂载卷后权限不对
第一次用-v /data/mysql:/var/lib/mysql启动容器,发现 MySQL 报:
[ERROR] InnoDB: Operating system error number 13 in a file operation. [ERROR] InnoDB: The error means mysqld does not have the access rights to the directory.原因是宿主机新建的/data/mysql目录属主是 root,容器里的mysql用户没有写权限。入口脚本里虽然有chown逻辑,但它只针对于 root 启动的情况,而且如果容器以mysql用户直接启动(比如用了docker run --user mysql),chown根本执行不了。
解法有两个:要么在宿主机上先执行chown 999:999 /data/mysql(999 通常是mysql用户的 UID),要么在入口脚本里检测到目录权限不对时,用 root 先修正再降权。我个人倾向于在编排层就解决,比如 docker-compose 里写一个初始化容器来准备目录权限,避免业务容器需要 root 能力。
4.3 初始化时区与字符集没生效
构建镜像时已经设置了时区和字符集,但容器启动后查询发现还是 UTC 和utf8mb4_0900_ai_ci。这个问题十有八九是my.cnf没被读到。
MySQL 读取配置文件的顺序是/etc/my.cnf、/etc/mysql/my.cnf、$MYSQL_HOME/my.cnf,还有启动命令行参数。如果你的 Dockerfile 把my.cnf放到/etc/my.cnf但启动时用了mysqld --initialize-insecure,初始化进程也许没问题,但后续正式启动时如果CMD里带了额外参数,可能会覆盖配置文件里的部分设置。另一个常见情况是:character-set-server和collation-server写到了[mysqld]段,但如果版本或拼写有误,MySQL 会静默忽略。
验证方式很直接:
docker exec -it mysql-test my_print_defaults mysqld这个命令会打印 mysqld 实际读取到的所有配置项。如果输出里没有你的配置,说明文件路径或文件权限有问题。
4.4 8.4 默认认证插件导致老客户端连不上
MySQL 8.4 的行为变化比较大,最明显的就是默认认证插件从mysql_native_password切到了caching_sha2_password。如果你手里的客户端是 8.0 之前的老版本,连接时会报:
Authentication plugin 'caching_sha2_password' cannot be loaded或者在日志里看到:
Authentication method 'caching_sha2_password' is not supported处理办法就是在my.cnf的[mysqld]段加上mysql_native_password = ON。还不行的话,就得改用户认证方式:
ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'App@123456';但要注意,MySQL 8.4 是 LTS 版本,未来 9.x 会彻底移除mysql_native_password。所以这只能算短期兼容方案,长期还是要推动客户端升级。
4.5 内存不足导致初始化失败
初始化阶段跑mysqld --initialize-insecure,容器直接 OOM Kill。原因多半是innodb_buffer_pool_size配得太大。InnoDB 会按配置预分配内存,如果你在my.cnf里写了innodb_buffer_pool_size = 1G,而容器内存限制只有 512M,初始化阶段就可能因为内存不足而崩溃。
我的建议是:初始化阶段和正式运行阶段使用不同的内存配置。入口脚本里,在--initialize-insecure之前临时加一个小 buffer pool:
mysqld --initialize-insecure --user=mysql \ --datadir="$MYSQL_DATA_DIR" \ --innodb_buffer_pool_size=64M正式启动时再用my.cnf里的完整配置。这样既保证初始化能在小内存环境跑完,又不影响生产环境的性能参数。
5. 一点实战补充:关于镜像体积与维护
构建完的镜像体积可能会让你吓一跳。Ubuntu 版加 MySQL 8.4,解压后 TAR 包本身就接近 200MB,加上基础镜像,整体镜像体积轻松超过 300MB。这其实是正常现象,不需要过度焦虑。不过有两点可以优化:
第一,构建完记得清理 apt/dnf 缓存。Ubuntu 镜像里执行完apt-get install后要执行rm -rf /var/lib/apt/lists/*,CentOS 里要dnf clean all。不清理的话,包索引文件会白白占用几十到上百 MB。
第二,MySQL 安装目录里有很多我们不需要的组件。比如bin目录下的mysqlbinlog、mysqldump如果没有使用需求,可以删掉。但我不建议过度精简,因为后期排查问题时很可能需要这些工具。镜像体积大一点,换来的是一次docker exec就能诊断问题的便利,这笔账很划算。
关于版本基线,我最后多说一句:镜像里的 MySQL 小版本必须写死,比如8.4.3,不能漂移。你这次构建用 8.4.3,下次构建因为各种原因变成 8.4.4,虽然 LTS 版本小版本升级兼容性一般没问题,但数据库这种核心组件,最忌讳的就是“行为不确定”。把版本锁定,再配合完整的构建归档,每次出包都能复现,这才是自定义镜像最核心的价值所在。