Docker Compose 部署 MySQL 8.4:从原理到实操(含 -v/-p 详解与改密码的坑)
数据库部署入门:从本地安装到 Docker
本文档整理自一次完整的实操学习过程,主线是:搞懂「部署一个数据库」到底是怎么回事,并亲手用 Docker + docker-compose 部署一个 MySQL,最终能够从「拿别人连接信息的人」变成「提供连接信息的人」。
阅读方式:概念部分建议顺读,实操部分可以照着敲。文中用了不少比喻,它们不是装饰,是帮你把抽象概念钉住的锚。
一、先想清楚:「部署一个数据库」到底是什么
新手容易把「部署」想成一件神秘的大事,其实它可以拆成很具体的5 件事。任何一份数据库连接配置,比如:
CONFIG = { 'server': '192.168.2.7', # 数据库跑在哪台机器上 'port': 1433, # 那台机器上,哪个端口在听 'database': 'substitute', # 服务里的哪个库 'user': 'sa', # 用哪个账号连 'password': 'xxxxxx', # 密码 }它背后就是有人把下面这 5 件事一件件落实了:
| 配置项 | 部署时要做的事 |
|---|---|
server有个 IP | 找一台机器 |
port有个端口在听 | 把数据库服务跑起来,它会占一个端口 |
database有个库 | 在服务里建一个库 |
user/password | 建一个账号并设密码 |
| 「别人能连」 | 配置成允许从网络连入 |
核心心智模型(贯穿全文):
一个数据库服务,本质就是守在某个端口上、配好账号密码、等着别人连进来的一个程序。 不管用哪种方式部署(本地安装 / Docker / 云托管),你最终都是在凑齐上面那份
{server, port, database, user, password}。方式换来换去,内核不变。
二、本地安装 vs Docker:到底差在哪
一个常见困惑:把数据库直接装进操作系统(本地/裸机安装),和用 Docker 起一个容器,有没有本质区别?
答案:没有致命性的本质差异。数据库还是同一个数据库,干的活一模一样。Docker 改变的不是「数据库本身」,而是「数据库外面那层壳(环境和依赖)怎么打包、怎么隔离」。
比喻:同一台咖啡机(数据库)。
- 本地安装= 把咖啡机直接拧在自家厨房台面上,水电直接接进墙里。
- Docker= 同一台咖啡机装进一个标准化的整装橱柜(集装箱),水电通过橱柜上的标准接口转接。
咖啡机没变,煮出来的咖啡一样。变的只是它怎么跟你的系统对接。
真正的区别全在隔离这一件事上:
| 维度 | 本地安装 | Docker |
|---|---|---|
| 数据库本身 | 完全相同 | 完全相同 |
| 和系统的关系 | 混在一起,共享环境 | 隔离,自带环境 |
| 装多个版本 | 难,会互相打架 | 轻松,各跑各的 |
| 删除 | 麻烦,留残渣 | 一条命令,干净 |
| 可复制性 | 差,靠记忆/文档 | 极强,一份配方(命令或 yml) |
| 数据安全 | 数据老实躺硬盘 | 需主动配卷,否则删容器丢数据 |
| 性能 | 无额外开销 | 极小开销(基本无感) |
结论:Docker 解决的是「规模化和一致性」的问题,不是「数据库好不好用」的问题。一台机器上同时稳定跑几十个服务(前端、后端、多种数据库)而互不干扰,靠的就是容器化。这也是公司普遍采用 Docker 的真正理由。
补充:生产环境的数据库,主流其实是云托管数据库(如 RDS)或K8s 有状态编排,而不是随手
docker run。随手docker run是开发/测试/学习的主流。但三者内核一致,先把docker run吃透,升级到别的方式只是换个更省事的壳。
三、用 Docker 部署 MySQL(docker run 方式)
先看单条命令的方式(PowerShell,行尾反引号`是换行符):
docker run --name my-mysql ` -e MYSQL_ROOT_PASSWORD=MyPass123! ` -e MYSQL_DATABASE=substitute ` -p 3306:3306 ` -v D:\Docker\Docker_project\MySQL\data:/var/lib/mysql ` -d mysql:8.4这一条命令,把「部署 5 件事」全包了:
| 这一段 | 干的事 | 对应「部署 5 件事」 |
|---|---|---|
docker run ... -d mysql:8.4 | 下载并后台跑 MySQL 8.4 | ①装软件 ②跑服务 |
--name my-mysql | 容器起名,方便管理 | —— |
-e MYSQL_ROOT_PASSWORD=... | 设 root 密码 | ③账号密码 |
-e MYSQL_DATABASE=substitute | 自动建一个叫 substitute 的库 | ④建库 |
-p 3306:3306 | 端口映射 | ⑤开端口 |
-v 路径:/var/lib/mysql | 数据存容器外,防丢 | 数据持久化 |
对比一下解压版当年要手动做的「解压→写 my.ini→初始化 data→注册服务→启动→设密码→建库」七八步——现在全压进了这一行。这就是「部署被压成一行能存进笔记的命令」。
四、关键概念一:端口映射-p
容器是一个隔离的盒子,里面的 MySQL 默认外面碰不到。-p 主机端口:容器端口就是在盒子上开个洞,把里面的端口接到你电脑的端口上。
- 左边 = 你电脑(宿主机)的端口
- 右边 = 容器里的端口
以后访问localhost:3306,请求就会被转进容器里的 MySQL。
比喻:容器是封闭的集装箱,MySQL 在里面。
-p就是在箱壁上钻个孔、接根管子出来,外面才能和里面通话。
一个真实生产的最佳实践:观察公司服务器上的容器端口,会发现——
- 应用容器:
0.0.0.0:8010->8000/tcp(把端口暴露到宿主机,谁都能从外面访问,因为 API 本来就要对外) - 数据库容器:只写
3306/tcp,没有->(故意不对外开放,只让同一个 Docker 网络里的应用容器能连到它)
数据库这样藏起来是为了安全——它最怕被外面直接连、被拖库。
比喻(餐厅):应用容器 = 对客人开放的大堂;数据库容器 = 只有内部员工能进的后厨仓库,客人根本摸不到门。
五、关键概念二:数据卷-v(数据持久化)
这是 Docker 里对数据库最要命的一块。核心问题只有一个:
数据会不会跟容器一起死?
「数据在本地硬盘上」是废话(它当然在),真正的问题是它绑在容器身上,还是独立于容器。这完全由-v决定,分三种情况:
情况一:不写-v(最危险)
数据写在容器内部。容器内部文件系统跟着容器生死,一旦docker rm删掉容器,数据灰飞烟灭,不可恢复。
比喻:数据写在便利贴上贴在集装箱内壁,箱子扔了便利贴跟着进垃圾场。
情况二:-v 卷名:/容器内路径(命名卷,推荐)
-v mysql-data:/var/lib/mysql/var/lib/mysql= 容器里 MySQL 固定的数据目录(镜像作者定死的,不改)mysql-data= 一个由 Docker 管理的、独立于容器的「卷」
MySQL 照常往/var/lib/mysql写,但数据实际落在容器外面的卷里。删容器,数据还在卷里,起个新容器接上同一个卷就能续用。
卷在硬盘的位置由 Docker 管理(Linux 上通常在/var/lib/docker/volumes/),用命令查看:
docker volume ls # 列出所有卷 docker volume inspect mysql-data # 看某个卷的详情和真实路径情况三:-v 本地真实路径:/容器内路径(绑定挂载)
-v D:\Docker\Docker_project\MySQL\data:/var/lib/mysql数据直接写进你能用文件资源管理器打开的真实文件夹。看得见、能备份、能拷走。适合学习阶段(有掌控感)。
三种对比
| 写法 | 数据存哪 | 删容器后 | 你能直接看到吗 |
|---|---|---|---|
不写-v | 容器内部 | 数据全没❌ | 看不到 |
-v 卷名:/路径 | Docker 管理的卷 | 数据还在 ✅ | 要用docker volume命令查 |
-v 本地路径:/路径 | 你指定的文件夹 | 数据还在 ✅ | 能,直接开文件夹 |
「卷」到底是什么
卷 = 一块由 Docker 管理的、独立于任何容器而存在的存储空间。它不属于哪个容器,容器死了它还在。
比喻:容器 = 租的房子,卷 = 你的行李箱。
- 东西摊在房子地板上(写容器内部)→ 房子拆了东西清空。共存亡。
- 东西装进自己的行李箱(写卷)→ 换房子拎着箱子走,东西一样不少。不共存亡。
背后的设计哲学
容器 = 会变的、随时可扔的躯壳;数据 = 不能变的、要留住的东西。这俩必须分开存。
-v就是把两者解耦的机制:让数据活在容器外面,容器随便换,数据岿然不动。
升级场景实例(数据库版本升级,生产天天发生):
# 旧:接卷 mysql-data docker run ... -v mysql-data:/var/lib/mysql -d mysql:8.0 # ↓ 删掉旧容器(数据在卷里,毫发无伤) # 新:换个版本号,接同一个卷 docker run ... -v mysql-data:/var/lib/mysql -d mysql:8.4 # 新容器一启动就看到全部老数据,无缝续用六、两种起容器的方式:docker runvsdocker-compose
Docker 起容器有两条路,它们是替代关系,做同一件事,只是形式不同:
docker run命令 | docker-compose(yml 文件) | |
|---|---|---|
| 怎么起 | 命令行敲一行 | 写个 yml 文件,然后docker compose up |
| 适合 | 起单个容器 | 一次起一堆容器 |
比喻:
docker run= 口头下单:「一杯拿铁、加燕麦奶、大杯、不要糖」——一句话说完。适合就点一杯。- docker-compose = 写好的订单清单:要点 20 杯还要按顺序上,写在纸上交给店员照单全做。适合复杂的、多个的。
yml 的本质,就是把你本来要敲的那些docker run参数,写进一个文件里。逐行对应:
| yml 里的 | 对应 docker run 的 |
|---|---|
image: mysql:8.4 | 命令结尾的mysql:8.4 |
container_name: my-mysql | --name my-mysql |
environment: | 每个-e |
ports: - "3306:3306" | -p 3306:3306 |
volumes: - ./data:/var/lib/mysql | -v |
depends_on: | (多容器才需要)控制启动顺序 |
networks: | (多容器才需要)让容器互相通信 |
七、读懂一个 yml 文件:数据位置是怎么写进指定文件夹的
以一个真实的 Milvus compose 片段为例:
volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd这一行就是-v的 yml 写法。冒号左边是宿主机路径,右边是容器内路径。关键在左边那串${DOCKER_VOLUME_DIRECTORY:-.}:
- 语法
${变量名:-默认值}意思是「有这个环境变量就用它的值,没有就用默认值」。 :-是「取不到就用后面兜底」。- 默认值是
.,代表「这个 compose 文件所在的文件夹」。
所以展开后:
${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd ↓ 没设变量,用默认 . . /volumes/etcd ↓ . = compose 文件所在文件夹(比如 D:\Docker\Docker_project\Milvus\) D:\Docker\Docker_project\Milvus\volumes\etcd核心结论:
它没把路径写死。数据落在和 yml 文件同一个文件夹下的
volumes\子目录里,靠的是.指向「yml 所在文件夹」。 你「指定文件夹」的动作,其实是通过「把 yml 文件放在那个文件夹」间接完成的。 外层的${...:-.}是个灵活开关:想换位置,启动前设个环境变量即可,文件一个字不用改;默认就落在文件旁边。
⚠️相对路径的坑:因为用了.,yml 文件不能单独挪走。只搬 yml 到新文件夹再up,.会指向新位置,重新建一套空数据,找不到老数据(老数据还在原文件夹)。要搬就整个文件夹一起搬。
八、实操:用 compose 部署 MySQL
1. 建目录并写 yml
mkdir D:\Docker\Docker_project\MySQL cd D:\Docker\Docker_project\MySQL在该文件夹建docker-compose.yml(注意别存成.txt,建议打开资源管理器的「文件扩展名」显示确认后缀):
services: mysql: image: mysql:8.4 container_name: my-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: MyPass123! MYSQL_DATABASE: substitute ports: - "3306:3306" volumes: - ./data:/var/lib/mysql
./data用了相对路径.,所以数据会落在 yml 旁边的data\文件夹里(和 Milvus 一个原理)。restart: unless-stopped让容器开机/崩溃自动重启。
用命令行直接生成文件(省得开记事本):
@" services: mysql: image: mysql:8.4 container_name: my-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: MyPass123! MYSQL_DATABASE: substitute ports: - "3306:3306" volumes: - ./data:/var/lib/mysql "@ | Out-File -FilePath "docker-compose.yml" -Encoding utf82. 启动
docker compose up -dup= 按 yml 把服务起起来-d= 后台运行
启动时会看到Network mysql_default Created——compose 默认给这组服务建了一个专属网络(单容器用不上,多容器时靠它互相找)。
3. 验证
docker ps # 看到 my-mysql,STATUS 是 Up netstat -ano | findstr "3306" # 3306 在 LISTENING Get-ChildItem D:\Docker\Docker_project\MySQL\data # data 文件夹自动生成,含 substitute 子目录4. 日常管理三条命令
docker compose up -d # 启动 docker compose stop # 停止(数据保留) docker compose down # 停止并删容器(数据在 data\ 里,不丢)九、连接数据库并验证
进容器内部用 MySQL 客户端登录:
docker exec -it my-mysql mysql -u root -pdocker exec -it my-mysql= 钻进容器(-it交互模式)mysql -u root -p= 用 root 登录,-p表示要输密码(输密码时屏幕不显示字符,正常,盲打回车)
登录成功后:
SHOW DATABASES; -- 列出所有库,能看到 substitute 就说明建库成功 exit -- 退出
information_schema/mysql/performance_schema/sys是 MySQL 自带的系统库,不用管。
十、改密码:一个必须知道的坑
陷阱:改 yml 里的MYSQL_ROOT_PASSWORD对一个「已经初始化过的容器」是无效的。
因为MYSQL_ROOT_PASSWORD这个环境变量只在数据目录为空、MySQL 第一次初始化时才生效。数据目录一旦有了数据,MySQL 启动时发现「已初始化过」,就跳过读密码那步,继续用老密码。
环境变量的本质:它是「出生时的设置」,不是「实时同步的配置」。房子盖好时按图纸装了把锁(初始密码),之后你换锁芯(改密码),图纸上那把锁的记录不会自己变,也不影响你换锁。
方案 A:进数据库直接改(推荐,保留数据)
ALTER USER 'root'@'%' IDENTIFIED BY '新密码'; ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'; FLUSH PRIVILEGES;⚠️密码必须用引号包起来!写成裸的
ALTER USER ... IDENTIFIED BY 123456会报语法错(ERROR 1064)。 两条ALTER的原因:MySQL 里 root 账号按「从哪连来」分身份,%是任意地方连、localhost是本机连,两个都改才彻底。 若报「password policy」相关错,是嫌密码太弱,换个复杂点的或调整密码策略。
方案 B:删数据重来,让 yml 新密码生效(会删数据)
docker compose down Remove-Item -Recurse -Force D:\Docker\Docker_project\MySQL\data # 改 yml 里的 MYSQL_ROOT_PASSWORD 为新密码 docker compose up -d适合库里没真数据、想让 yml 和实际密码彻底一致的情况。
十一、数据库的层级,与「改表名/用户名」
数据库世界是一层套一层的,像俄罗斯套娃:
MySQL 服务器(整个容器) └── 数据库/库 (database) ← 比如 substitute └── 表 (table) ← 比如 users、orders └── 字段/列 (column) ← 比如 id、name 用户 (user) ← root 等,独立于上面,管"谁能登录"注意:「表名」是表这一层,「用户名」是用户这一层,完全不同的东西,改法也不同。
-- 改库名(MySQL 不支持直接重命名,空库直接建新删旧) CREATE DATABASE new_name; DROP DATABASE old_name; -- 改表名 USE substitute; RENAME TABLE old_table TO new_table; -- 改字段名 ALTER TABLE 表名 RENAME COLUMN 旧列名 TO 新列名; -- 建一个专用用户(不要给 root 改名!) CREATE USER 'myuser'@'%' IDENTIFIED BY 'mypassword'; GRANT ALL PRIVILEGES ON substitute.* TO 'myuser'@'%'; FLUSH PRIVILEGES;别改 root 的名字:很多工具默认找 root,改了容易把自己锁在外面。规范做法是 root 保持不动,另建业务专用用户——这正是别人给你的连接配置里那个
user(如sa)的来历:不是把 root 改名,而是新建一个专用账号。
补充:真实开发里,建库/建表/建字段一般在应用代码(如 FastAPI + SQLModel)里创建;改结构用「数据库迁移」工具(如 Alembic)管理,有版本、能回滚。手敲
RENAME/ALTER主要用于学习、临时修补或应急。
十二、边界:改这些,yml 要不要动?
一条清晰的分界线:
yml 管的是「容器怎么起」,不管「容器里的数据长什么样」。
比喻:yml 像盖房子的图纸,数据库里的表/用户/数据像住进去后你怎么摆家具。挪张桌子不会去改建筑图纸。
| 你要改的东西 | 属于哪层 | 要改 yml 吗 |
|---|---|---|
| 表名、字段名 | 数据库内部 | 不用 |
| 建表、加数据、删数据 | 数据库内部 | 不用 |
| 新建用户 | 数据库内部 | 不用 |
| 改用户密码 | 数据库内部 | 不用(可选同步记录) |
| 改/建/删库名 | 数据库内部 | 不用 |
| 换端口 | 容器怎么起 | 要改ports: |
| 换镜像版本 | 容器怎么起 | 要改image: |
| 改数据存哪个文件夹 | 容器怎么起 | 要改volumes: |
| 改容器名 | 容器怎么起 | 要改container_name: |
判断口诀:问自己「我改的是壳,还是壳里的内容?」壳 → 改 yml;内容 → 不改。
改完 yml 后
docker compose up -d生效。compose 会对比差异,只重建真变了的容器。只要卷/数据目录没变,数据库里的表、用户、数据全都还在(它们在卷里,不在容器里)。
附录 A:常用命令速查
# —— 容器 —— docker ps # 看正在跑的容器 docker ps -a # 看所有容器(含已停止) docker rm -f 容器名 # 强制删除容器 docker exec -it 容器名 bash # 进入容器内部的命令行 # —— compose(在 yml 所在目录执行)—— docker compose up -d # 启动 docker compose stop # 停止(保留容器) docker compose down # 停止并删除容器 docker compose logs -f # 查看日志 # —— 卷 —— docker volume ls # 列出所有卷 docker volume inspect 卷名 # 看卷的真实路径 docker volume rm 卷名 # 删除卷(= 删数据,慎用) # —— 镜像 —— docker images # 列出本地镜像 # —— MySQL 连接 —— docker exec -it my-mysql mysql -u root -p # 登录 MySQL # —— 排查 —— netstat -ano | findstr "3306" # 看端口有没有在监听附录 B:术语表
| 术语 | 一句话解释 |
|---|---|
| 镜像 (image) | 数据库程序的「安装包/模板」,如mysql:8.4。存在 Docker 统一仓库,位置不可自选。 |
| 容器 (container) | 由镜像跑起来的一个运行实例,是「随时可扔的躯壳」。 |
| 卷 (volume) | 独立于容器的存储空间,用来让数据活得比容器久。 |
| 绑定挂载 (bind mount) | -v 真实文件夹:/容器路径,数据落到你能看见的文件夹。 |
| 端口映射 | -p 主机端口:容器端口,把容器内端口接到宿主机。 |
| docker-compose | 用一个 yml 文件定义并管理一个或多个容器。 |
| 守护进程 (daemon) | 一直在后台待命的服务,如mysqld。 |
| CRUD | 增(Create)/查(Read)/改(Update)/删(Delete),数据库四种基本操作。 |
附录 C:一句话总结整条学习线
数据库部署 = 让一个服务守在端口上、配好账号密码、允许别人连进来。 本地安装和 Docker 装的是同一个数据库,区别只在「外面那层壳怎么打包和隔离」。 Docker 用
-p通端口、用-v把数据存到容器外面(否则删容器丢数据)。docker run起单个、docker-compose(yml)起一堆,yml 每一行都对应一个 run 参数。 yml 管「容器怎么起」,数据库内部的表/用户/数据不归它管。 学会这套,你就从「拿别人连接信息的人」变成了「能提供连接信息的人」。
