在 VPS 上用 Docker 安装 MySQL,最容易漏掉的不是启动命令,而是三个问题:3306 到底暴露给谁、换容器后数据还在不在、备份能不能恢复。这篇用 MySQL 8.4、Docker Compose 和 SSH 隧道把这条路径走完整,适合给小网站、内部工具和 API 项目准备数据库。
本文针对全新实例,采用 Docker Official Image mysql:8.4,不是 mysql/mysql-server。配置和命令按官方文档整理;资源建议是部署起点,不是性能实测。已有 MySQL 或 MariaDB 数据目录不要直接套用,迁移要单独做兼容性检查。
| 使用场景 | 建议起点 | 购买前重点检查 |
|---|---|---|
| 学习、低频内部工具 | 2 vCPU、2GB RAM、30GB SSD | 空闲内存、备份空间、磁盘延迟 |
| 小网站,应用与数据库同机 | 2–4 vCPU、4GB RAM、60GB SSD | 应用峰值内存、CPU 争用、磁盘增长 |
| 数据库单独部署、持续写入 | 4 vCPU、8GB RAM 起 | IOPS、存储扩容、私网与恢复时间 |
这些配置不对应固定并发量。几十个慢查询可能比几百个短查询更吃资源;账单还要加上异地备份容量、出站流量和运维时间。应用和数据库尽量放在同一区域,先确认磁盘表现和可用内存,再决定是否为网络线路加预算。
选择 8.4 是为了明确应用兼容范围。mysql:8.4 会跟随这个系列更新;latest 和 lts 都不能代替明确版本。正式上线时记录镜像 digest,后续更新先在恢复实例验证。当前标签与初始化变量以 MySQL 官方镜像说明 为准。
准备一台安装了受支持 Docker Engine 与 Compose 插件的 Linux VPS,本文命令按 Ubuntu 24.04 的 Bash 环境编写。Docker 安装可参考 VPS Docker 部署教程。下面在 VPS 上使用 root shell 操作,目录统一为 /opt/mysql84。
先确认宿主机 3306 没被已有数据库占用:
docker version
docker compose version
ss -lntp 'sport = :3306'
install -d -m 700 /opt/mysql84
cd /opt/mysql84
install -d -m 700 secrets backups
生成 root 和应用账号的不同密码。下面只在全新目录执行一次;已有实例改密码要执行 SQL,并同步更新客户端文件。
umask 077
openssl rand -hex 32 > secrets/mysql_root_password
openssl rand -hex 32 > secrets/mysql_app_password
printf '[client]\nuser=root\npassword=%s\nprotocol=socket\n' \
"$(cat secrets/mysql_root_password)" > secrets/mysql_admin.cnf
chmod 600 secrets/*
把以下内容保存为 /opt/mysql84/compose.yaml:
name: mysql84
services:
mysql:
image: mysql:8.4
restart: unless-stopped
stop_grace_period: 1m
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
MYSQL_ROOT_HOST: localhost
MYSQL_DATABASE: appdb
MYSQL_USER: appuser
MYSQL_PASSWORD_FILE: /run/secrets/mysql_app_password
secrets:
- mysql_root_password
- mysql_app_password
- mysql_admin
ports:
- "127.0.0.1:3306:3306"
volumes:
- mysql_data:/var/lib/mysql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
- --require-secure-transport=ON
- --innodb-buffer-pool-size=256M
- --max-connections=50
- --binlog-expire-logs-seconds=604800
mem_limit: 1g
healthcheck:
test:
- CMD
- mysql
- --defaults-extra-file=/run/secrets/mysql_admin
- --batch
- --skip-column-names
- -e
- SELECT 1
interval: 15s
timeout: 5s
retries: 10
start_period: 90s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
mysql_data:
secrets:
mysql_root_password:
file: ./secrets/mysql_root_password
mysql_app_password:
file: ./secrets/mysql_app_password
mysql_admin:
file: ./secrets/mysql_admin.cnf
mysql_data 保存数据库文件,重建容器不会自动清空它。不要执行带 -v 的 docker compose down 来排障。Compose secrets 在这里是本地文件挂载,并非加密保险箱,拥有 root 或 Docker 管理权限的人仍能读取。
本例显式把 root 限制为 localhost;应用使用独立账号。镜像初始化脚本只在空数据目录中创建账号和数据库,改 secret 文件不会修改已存在的数据库密码。可核对 镜像入口脚本 的行为。
127.0.0.1:3306:3306 限制宿主机端口绑定,云安全组也不要开放 3306。使用受支持且不低于 28.0.0 的 Docker Engine;旧版存在同一二层网络访问 localhost 发布端口的例外,见 Docker 端口发布说明。这不限制同一 Docker 网络内的其他容器,因此不要把不可信容器接进数据库网络。
启动并检查实际查询是否成功:
cd /opt/mysql84
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 mysql
docker compose exec -T mysql \
mysql --defaults-extra-file=/run/secrets/mysql_admin \
-e 'SELECT VERSION(); SHOW DATABASES;'
docker image inspect mysql:8.4 --format '{{json .RepoDigests}}'
首次初始化需要时间,等 healthy 后再接应用。最后一条输出的 mysql@sha256:… 可用于固定 image;记录它,不要把示例省略号填进配置。健康检查执行的是认证后的 SQL,能同时发现密码不匹配问题。
在自己的电脑上建立 SSH 隧道,替换 SSH 用户和 VPS 地址:
ssh -N -T -o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-L 127.0.0.1:13306:127.0.0.1:3306 deploy@VPS_IP
确认 SSH 主机指纹后保持这个窗口运行。Navicat 或 DBeaver 的数据库主机填 127.0.0.1,端口 13306,数据库 appdb,用户 appuser,密码从 VPS 的 secrets/mysql_app_password 安全读取。使用 GUI 自带 SSH 隧道时,数据库端目标应填 VPS 上的 127.0.0.1:3306,不要再叠加外部隧道。
本例开启了 require_secure_transport,TCP 客户端必须启用 TLS。用 MySQL 8.4 客户端验证:
mysql --protocol=TCP -h 127.0.0.1 -P 13306 \
-u appuser -p --ssl-mode=REQUIRED appdb
进入后执行:
SHOW SESSION STATUS LIKE 'Ssl_cipher';
SELECT CURRENT_USER(), DATABASE();
Ssl_cipher 应有值。REQUIRED 只保证加密,不校验数据库服务器身份;这里的远端主机身份由已验证指纹的 SSH 连接提供。跨主机应用直连时,应使用私网或受控隧道,再配置可信 CA、与数据库主机名匹配的证书和 VERIFY_IDENTITY。MySQL 自动生成的自签名证书不能直接满足主机名校验,见 MySQL TLS 文档。
同一个 Compose 项目中的应用通常连接 mysql:3306;容器中的 127.0.0.1 指向容器自己。如果应用也在容器里,显式接入该项目网络并配置数据库驱动的 TLS 选项。不要把 MySQL TCP 连接交给普通 HTTP 反向代理。
初始化生成的 appuser 对 appdb 有较广权限,方便应用建表。完成迁移后,建议把建表账号与日常读写账号分开。在 VPS 上进入管理会话:
docker compose exec mysql \
mysql --defaults-extra-file=/run/secrets/mysql_admin
将下面占位密码替换为另一组随机值,再执行 SQL:
CREATE USER 'app_runtime'@'%'
IDENTIFIED BY 'REPLACE_WITH_A_NEW_RANDOM_PASSWORD' REQUIRE SSL;
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app_runtime'@'%';
SHOW GRANTS FOR 'app_runtime'@'%';
把应用运行配置切换到 app_runtime。需要迁移时才使用有 DDL 权限的账号;某些应用启动时自动迁移,要按它的实际需求保留权限。这里的 % 表示账号匹配任意来源,不是公网开放开关,访问范围仍依靠端口绑定、网络隔离和认证控制。
MySQL 8.4 默认禁用 mysql_native_password。遇到旧驱动认证失败,优先升级驱动,不要照抄 --default-authentication-plugin=mysql_native_password。官方说明见 认证插件文档。
下面备份 appdb 的表、触发器、存储过程和事件,不包含账号授权或其他数据库。示例用容器内管理凭据执行,文件只允许 root 读取;长期运行可换成按实际导出对象授权的专用备份账号。
保存为 /opt/mysql84/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
umask 077
cd /opt/mysql84
backup_stamp=$(date -u +%Y%m%dT%H%M%SZ)
backup_file="backups/appdb-${backup_stamp}.sql.gz"
test ! -e "$backup_file"
test ! -e "${backup_file}.partial"
docker compose exec -T mysql \
mysqldump --defaults-extra-file=/run/secrets/mysql_admin \
--single-transaction --quick --routines --events --triggers \
--hex-blob --no-tablespaces --set-gtid-purged=OFF \
--databases appdb | gzip > "${backup_file}.partial"
gzip -t "${backup_file}.partial"
mv "${backup_file}.partial" "$backup_file"
sha256sum "$backup_file" > "${backup_file}.sha256"
printf 'Backup ready: %s\n' "$backup_file"
手动执行一次:
chmod 700 /opt/mysql84/backup.sh
/opt/mysql84/backup.sh
pipefail 防止导出失败后 gzip 仍让整个任务显示成功;失败文件保留 .partial,不会伪装成完成的备份。--single-transaction 的一致性保证主要适用于 InnoDB,备份期间不要执行建表、删表或结构变更。MyISAM 等非事务表需要另外安排锁表或停写,依据 mysqldump 文档。
确认成功后,可在 root 的 crontab -e 中设置每天 03:20 执行;这里按服务器时区计算:
20 3 * * * /bin/bash /opt/mysql84/backup.sh >> /opt/mysql84/backups/backup.log 2>&1
定时任务要监控退出状态或“最后成功备份时间”,日志也要轮转。把完成的备份、校验文件、Compose 配置、镜像 digest 和必要的账号重建 SQL 复制到异地加密存储;密钥单独保管。相同 VPS 上的备份不能应对整机丢失,可接着看 VPS 自动备份方案。
保留几份、保留多久取决于业务可接受的数据损失和容量。每天一份逻辑备份可能丢失接近一天的数据;要恢复到误删之前的某一时刻,需要额外归档 binlog 并记录对应备份位置。本文不是完整的时间点恢复方案。
准备额外内存和磁盘。下面创建单独的数据卷,恢复容器没有网络,也不绑定生产端口。容器名和卷名应是本次演练专用,已有同名资源时先改名。
在 VPS 的 /opt/mysql84 目录执行,将备份文件名替换为真实文件:
cd /opt/mysql84
backup_file='backups/appdb-YYYYMMDDTHHMMSSZ.sql.gz'
sha256sum -c "${backup_file}.sha256"
gzip -t "$backup_file"
restore_volume=$(docker volume create)
restore_image=$(docker compose images -q mysql)
test -n "$restore_image"
docker run -d --name mysql84-restore --network none --memory 1g \
--mount "type=volume,src=${restore_volume},dst=/var/lib/mysql" \
--mount "type=bind,src=$PWD/secrets/mysql_root_password,dst=/run/secrets/mysql_root_password,readonly" \
--mount "type=bind,src=$PWD/secrets/mysql_admin.cnf,dst=/run/secrets/mysql_admin,readonly" \
-e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password \
-e MYSQL_ROOT_HOST=localhost \
"$restore_image" --event-scheduler=OFF --innodb-buffer-pool-size=256M
docker logs --tail=50 mysql84-restore
这里使用生产容器对应的本地镜像 ID,避免恢复时 8.4 标签指向另一个补丁版。等初始化完成,下面查询成功后再导入;执行失败就继续看日志,不要跳过:
docker exec mysql84-restore \
mysql --defaults-extra-file=/run/secrets/mysql_admin -e 'SELECT 1;'
set -o pipefail
gzip -dc "$backup_file" | docker exec -i mysql84-restore \
mysql --defaults-extra-file=/run/secrets/mysql_admin
docker exec mysql84-restore \
mysql --defaults-extra-file=/run/secrets/mysql_admin \
-e 'SHOW TABLES FROM appdb;'
导入会执行备份内的 SQL,所以只导入来源可信的备份。事件调度器保持关闭,避免演练环境触发定时任务。继续核对关键表的精确行数、最近订单或业务记录,并记录导入耗时;SHOW TABLES 成功并不代表业务恢复成功。
需要应用联调时,先重建必要账号与授权,检查视图、过程、事件中的 DEFINER,再接入隔离测试网络,禁用发信和支付等外部动作。演练完成可执行 docker stop mysql84-restore 释放运行内存,数据卷保留供检查。更多步骤见 VPS 备份恢复演练。
示例把 buffer pool 设为 256MB、容器内存限制为 1GB,只是小实例起点。buffer pool 不包含连接缓冲、排序、临时表和其他开销;不能把它设成容器内存上限。结合 InnoDB buffer pool 配置说明 和实际负载逐步调节。
docker stats --no-stream
df -h
docker compose exec -T mysql \
mysql --defaults-extra-file=/run/secrets/mysql_admin \
-e "SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Max_used_connections','Slow_queries'); SHOW BINARY LOGS;"
连接数接近上限时先检查应用连接池和连接泄漏;不要只把 max_connections 翻倍。MySQL 二进制日志默认启用,本例设置七天过期时间,但过期时间不是磁盘容量上限,大量写入仍可能提前写满磁盘。按 binlog 文档 调整归档与保留策略,不要直接删除数据目录中的日志文件。
更新步骤是:备份并验证恢复,记录旧镜像 digest,在恢复环境验证新补丁与应用,再安排生产停写维护。固定 digest 的部署需要显式修改 image 后拉取和重建;仅运行 pull 不会自动更换已固定的 digest。
从旧版本迁入时,先核对 官方升级路径 并运行相应升级检查。回退计划是使用升级前备份和旧镜像重建实例,再恢复应用连接;不要让旧镜像直接打开已经升级的数据卷。
镜像初始化变量只处理空数据目录。已有账号要通过 ALTER USER 改密码,再同步 secret 与客户端配置;无需删除数据卷。排查路径见 MySQL / MariaDB 连接失败指南。
确认 SSH 隧道还在、电脑本地用的是 13306、数据库用户是 appuser 而非 root,并启用了 TLS。SSH 连接成功只代表能登录 VPS,不代表数据库已完成初始化。
先核对服务器版本、账号认证插件与客户端驱动版本。新实例优先使用支持 caching_sha2_password 的驱动;迁移老账号时先验证应用兼容性,再切换认证方式。
可能启动,但本文默认值还要给宿主机留内存,不能直接套到整机只有 1GB 的环境。出现 OOM 或退出码 137 时,查看内存和容器限制,参考 低内存 MySQL Docker 排障。
在线直接复制可能得到不一致文件。小库可先用本文的逻辑备份;大库再评估与版本匹配的物理备份工具和恢复流程。单机快照也不能代替异地备份。
同机容器可以通过受控 Docker 网络连接,电脑管理可以走 SSH 隧道。跨主机应用优先私网或组网,结合 TLS 和最小权限账号。先完成一次独立恢复演练,再把正式业务接进来。
