Docker 运行 Sql Server 2017:数据库还原与 SQL 脚本批量执行实战

发布时间:2026/11/6 4:20:38
Docker 运行 Sql Server 2017:数据库还原与 SQL 脚本批量执行实战

1. 为什么要在 Docker 里跑 Sql Server 2017

把 Sql Server 2017 塞进 Docker 容器里跑,这件事在几年前还挺新鲜,现在已经是很多开发团队的常规操作了。我最早接触这个组合,是因为手头一个老项目需要做数据迁移验证,本地机器上不想装一整套 Sql Server 实例——安装包大、注册表写一堆、卸载还卸不干净,折腾一次少说半小时。后来改用容器,拉镜像、起容器、连上去,前后不到五分钟,用完直接删容器,机器干干净净。这个体验上的差距,是我后来一直坚持用容器跑数据库的根本原因。

具体来说,Docker 运行 Sql Server 2017 解决的核心问题是环境隔离与快速复现。你可以在同一台开发机上同时跑 Sql Server 2017、2019、2022 三个版本,各自监听不同端口,互不干扰;也可以把一套带数据的容器配置写成脚本,交给同事一键复现,不用再对着文档一步步装环境。对于需要频繁还原数据库、执行 SQL 脚本做验证的场景,这种能力尤其值钱。

这篇文章适合谁看?如果你是需要做数据库迁移验证的后端开发、要搭建测试环境的质量同学、或者单纯想学容器化数据库的运维新手,都能从下面找到可以直接抄作业的步骤。我会从镜像选型讲到容器启动、从数据库还原讲到 SQL 文件批量执行,中间穿插我自己踩过的坑和参数选择的理由。需要说明的是,Sql Server 2017 官方镜像已经比较老了,部分细节和更新版本有差异,我会在涉及的地方明确标注。

先给一个整体认知:整个流程分四步——拉镜像起容器、准备数据文件、还原数据库、执行 SQL 脚本。每一步都有几个容易翻车的点,尤其是文件权限和路径映射,后面会重点展开。

2. 环境准备与镜像选型的关键考量

2.1 镜像版本怎么选才不踩坑

Sql Server 2017 的官方镜像在镜像仓库里有多个标签,常见的有2017-latest、2017-CU31-ubuntu、2017-GDR等。我一般推荐用mcr.microsoft.com/mssql/server:2017-latest,理由很简单:latest标签指向该大版本下最新的累积更新,安全补丁和 bug 修复都包含在内,省得你自己去追 CU 编号。如果你对版本有严格锁定需求(比如要和线上环境完全一致),那就查清楚线上跑的是哪个 CU,用对应的精确标签,比如2017-CU31-ubuntu。

这里有个容易忽略的点:Sql Server 2017 的镜像基于 Ubuntu 16.04,这个基础镜像本身已经停止维护了。这意味着你拉下来的镜像里,系统层面的包是比较旧的。对于本地开发和测试验证,这通常不是问题;但如果要放到对安全基线有要求的环境里,就得评估一下。我的做法是本地验证用 2017,正式环境该升级就升级,别在一个停止维护的版本上死磕。

另外提醒一句,Sql Server 镜像对内存有硬性要求,官方文档写的是至少 2GB,实际跑起来加上还原数据库、执行脚本,我建议给到 4GB 以上。内存不够的时候容器会直接退出,日志里只留一句含糊的提示,排查起来很费劲。下面是我常用的启动参数表,先有个印象:

参数推荐值作用说明
-e ACCEPT_EULAY必须接受许可协议,否则容器起不来
-e MSSQL_SA_PASSWORD强密码SA 账户密码,需满足复杂度要求
-e MSSQL_PIDDeveloper版本类型,开发测试用 Developer 免费
-p1433:1433端口映射,注意宿主机端口冲突
-v自定义路径数据持久化与文件交换的关键
--memory4g限制内存,防止吃满宿主机

2.2 密码策略与许可协议这两个必填项

ACCEPT_EULA=Y和MSSQL_SA_PASSWORD是两个没有商量余地的环境变量。前者不接受容器直接退出,后者不设置或者设置得太简单,容器同样起不来。密码复杂度要求是:至少 8 位,且包含大写字母、小写字母、数字、符号中的至少三类。我见过有人设12345678然后对着日志找半天原因,其实日志第一行就写了密码不符合策略。

密码里如果包含特殊字符,在命令行里要注意转义。比如密码里有$,在 shell 里会被当变量展开,得用单引号包起来或者加反斜杠。我一般干脆避开$、!、反引号这类在 shell 里有特殊含义的字符,用@、#、_组合,省心。

2.3 数据持久化的目录规划思路

容器删了数据就没了,这是容器的基本特性。所以只要你的数据有价值,就必须做卷映射。我习惯映射两个目录:一个放数据库文件(mdf/ldf),一个放备份文件和待执行的 SQL 脚本。这样还原数据库时,容器内能直接读到宿主机上的 bak 文件,执行脚本时也能直接读到 sql 文件,不用反复docker cp。

目录规划上,我建议在宿主机建一个清晰的工作目录,比如:

mkdir -p /data/mssql/data mkdir -p /data/mssql/backup mkdir -p /data/mssql/scripts

对应容器内映射到/var/opt/mssql/data、/var/opt/mssql/backup、/var/opt/mssql/scripts。注意容器内/var/opt/mssql是 Sql Server 的工作目录,权限归mssql用户所有。宿主机目录的权限问题后面单独讲,这是还原数据库时最常见的坑之一。

3. 启动容器与连接验证的完整实操

3.1 一条命令起容器及其参数逐项拆解

把前面的准备串起来,启动命令大概长这样:

docker run -d \ --name mssql2017 \ -e ACCEPT_EULA=Y \ -e MSSQL_SA_PASSWORD='YourStrong@Passw0rd' \ -e MSSQL_PID=Developer \ -p 1433:1433 \ --memory 4g \ -v /data/mssql/data:/var/opt/mssql/data \ -v /data/mssql/backup:/var/opt/mssql/backup \ -v /data/mssql/scripts:/var/opt/mssql/scripts \ mcr.microsoft.com/mssql/server:2017-latest

逐项说一下为什么这么写。-d后台运行不用解释;--name给容器起个固定名字,后面docker exec进容器、看日志都靠它,比记容器 ID 方便太多;-p 1433:1433把容器端口映射到宿主机,如果你宿主机上已经有一个 Sql Server 在跑 1433,就改成-p 14330:1433之类,避免冲突;--memory 4g是软限制,防止数据库把宿主机内存吃光导致整机卡死,这个参数在开发机上尤其重要,我吃过一次没限制内存、容器把 16G 内存吃到只剩几百兆的亏。

MSSQL_PID=Developer这个参数值得单独说。Sql Server 有多个版本类型,Developer 版功能完整、免费,仅限开发和测试使用,正好匹配我们的场景。如果不设这个参数,默认可能是 Evaluation 版,有使用期限,到期后数据库会变成只读,到时候还原数据会莫名其妙失败,排查半天才发现是版本过期。这个坑我踩过一次,之后每次启动都显式指定。

3.2 容器启动后的健康检查

容器起来不代表数据库就绪。Sql Server 启动要初始化系统库,通常需要十几秒到半分钟。我一般用两种方式确认就绪:看日志和实际连接。

看日志:

docker logs -f mssql2017

当日志里出现SQL Server is now ready for client connections这行,基本就绪了。如果看到This program can not be run as root或者密码相关的报错,说明启动参数有问题,容器会反复重启,这时候docker ps -a能看到状态是Restarting。

实际连接验证,如果你宿主机装了 sqlcmd 客户端,直接:

sqlcmd -S localhost,1433 -U sa -P 'YourStrong@Passw0rd' -Q "SELECT @@VERSION"

没装客户端也没关系,进容器里用自带的 sqlcmd:

docker exec -it mssql2017 /opt/mssql-tools/bin/sqlcmd \ -S localhost -U sa -P 'YourStrong@Passw0rd' \ -Q "SELECT @@VERSION"

注意 Sql Server 2017 镜像里 sqlcmd 的路径是/opt/mssql-tools/bin/,更新的镜像里改成了/opt/mssql-tools18/bin/,路径不对会报找不到命令。这个细节在混用不同版本镜像时特别容易搞混。

提示:如果连接时报Login failed for user 'sa',先确认密码有没有被 shell 转义搞乱,再确认容器是不是还在初始化中。密码错误和未就绪的报错有时候长得像,多等十几秒再试一次往往就好了。

3.3 宿主机目录权限这个隐形炸弹

这是我要重点强调的一节,因为还原数据库失败十次里有八次是权限问题。容器内的 Sql Server 以mssql用户运行,这个用户在容器内的 UID 通常是 10001。当你把宿主机目录映射进容器,容器内进程对这个目录的读写权限,取决于宿主机目录的属主和权限位。

如果你在宿主机用 root 建的目录,默认权限 755,属主 root,那么容器内的 mssql 用户(UID 10001)对这个目录只有读和执行权限,没有写权限。表现出来就是:还原数据库时提示无法创建文件、或者备份文件读不到。解决办法是给目录放权:

chown -R 10001:10001 /data/mssql/data chown -R 10001:10001 /data/mssql/backup chmod -R 750 /data/mssql/data

把属主改成 10001,和容器内 mssql 用户的 UID 对齐。备份目录和脚本目录如果只需要读,权限可以松一点,但数据目录必须可写。我一般三个目录统一处理,省得后面还原时又出幺蛾子。

还有一种情况:宿主机开了 SELinux 或者用了某些强制访问控制机制,即使权限位对了,容器还是访问不了。这时候可以在卷映射后面加:z或:Z标签,让系统自动处理上下文。不过这个和具体发行版有关,遇到访问被拒且权限位确认无误时,可以往这个方向查。

4. 数据库还原的两种路径与细节把控

4.1 从 bak 备份文件还原的标准流程

还原数据库是这套流程的核心诉求之一。假设你有一个demo.bak放在宿主机的/data/mssql/backup目录,容器内对应/var/opt/mssql/backup/demo.bak。还原前先做一件事:查备份文件里包含哪些逻辑文件名和物理路径。这一步很多人跳过,直接RESTORE DATABASE,然后报错说找不到文件或者路径不对。

查文件列表:

RESTORE FILELISTONLY FROM DISK = '/var/opt/mssql/backup/demo.bak';

这条语句会返回结果集,重点看两列:LogicalName和PhysicalName。LogicalName是还原时MOVE子句要用的名字,PhysicalName是备份时原始数据库文件所在的路径,这个路径在容器里通常不存在,所以必须用MOVE重定向到容器内的有效路径。

完整的还原语句:

RESTORE DATABASE DemoDB FROM DISK = '/var/opt/mssql/backup/demo.bak' WITH MOVE 'DemoDB' TO '/var/opt/mssql/data/DemoDB.mdf', MOVE 'DemoDB_log' TO '/var/opt/mssql/data/DemoDB_log.ldf', REPLACE, STATS = 10;

MOVE的两个逻辑名要和FILELISTONLY查出来的一致,物理路径指向映射的数据目录。REPLACE表示如果同名数据库已存在就覆盖,做反复验证时很有用。STATS = 10让还原过程每完成 10% 输出一次进度,长还原时心里有底。

如果备份文件很大,还原时间较长,建议在 sqlcmd 里执行时把超时设长一点,或者干脆用-t 0表示不超时。默认超时在某些客户端配置下可能只有几十秒,大库还原到一半连接断了,白等。

4.2 没有 bak 文件时用脚本建库的替代方案

有时候你手上没有备份文件,只有建表脚本和初始化数据脚本。这种情况就不需要还原,直接建一个空库,然后执行脚本。建库语句很简单:

CREATE DATABASE DemoDB;

但这里有个细节:建库时如果不指定文件路径,数据库文件会落到默认位置,通常是/var/opt/mssql/data,正好是我们映射的目录,所以一般不用额外指定。如果你想让文件落到特定子目录,可以在CREATE DATABASE时用ON PRIMARY指定文件路径,不过对大多数验证场景没必要。

建完库之后就是执行 SQL 脚本,这部分下一节展开。这里先提一句:建库和执行脚本可以合并到一个 sqlcmd 调用里,用-i指定脚本文件,脚本开头写USE DemoDB;切换上下文,一气呵成。

4.3 还原过程中的常见报错与应对

还原环节的报错集中在几类,我整理成速查表:

报错信息关键词大概率原因处理方向
Cannot open backup device路径不对或权限不足确认容器内路径、检查目录属主
The file ... cannot be overwritten目标文件已存在且未加 REPLACE加REPLACE或先删旧库
Logical file ... is not part of databaseMOVE 的逻辑名写错重新跑 FILELISTONLY 核对
Operating system error 5权限被拒检查 UID 10001 的目录权限
Exclusive access could not be obtained数据库正被连接占用先ALTER DATABASE ... SET SINGLE_USER

Exclusive access这个报错在反复还原时特别常见。你上一次还原后连上去看过数据,连接没断干净,再还原就报独占访问失败。解决办法是先强制单用户模式踢掉其他连接:

ALTER DATABASE DemoDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE;

然后再执行还原,还原完数据库会自动回到多用户模式。这个技巧在自动化脚本里很实用,我一般把它放在还原语句前面,作为固定套路。

5. 批量执行 SQL 文件的实操与技巧

5.1 用 sqlcmd 执行单个与多个脚本

执行 SQL 文件最直接的方式是 sqlcmd 的-i参数。单个文件:

docker exec -it mssql2017 /opt/mssql-tools/bin/sqlcmd \ -S localhost -U sa -P 'YourStrong@Passw0rd' \ -d DemoDB -i /var/opt/mssql/scripts/init.sql

-d DemoDB指定默认数据库,脚本里就不用反复写USE。如果脚本里有中文,注意文件编码,sqlcmd 默认按当前代码页解析,UTF-8 带 BOM 的文件有时会出乱码或者语法错误。我的经验是把 SQL 文件存成 UTF-8 无 BOM,或者在 sqlcmd 加-f 65001指定编码。

多个文件批量执行,有两种思路。第一种是写一个总的入口脚本,里面用:r指令引用其他脚本:

:r /var/opt/mssql/scripts/01_schema.sql :r /var/opt/mssql/scripts/02_data.sql :r /var/opt/mssql/scripts/03_index.sql

:r是 sqlcmd 的包含指令,按顺序把文件内容读进来执行,适合有明确依赖顺序的场景。第二种是在宿主机用 shell 循环:

for f in /data/mssql/scripts/*.sql; do docker exec -i mssql2017 /opt/mssql-tools/bin/sqlcmd \ -S localhost -U sa -P 'YourStrong@Passw0rd' \ -d DemoDB -i "/var/opt/mssql/scripts/$(basename $f)" done

循环方式的好处是每个文件独立执行,某个文件失败不影响后续(取决于你要不要中断),日志也按文件分开,排查方便。缺点是每次docker exec有启动开销,文件多的时候慢一些。文件数量在几十个以内,两种方式差别不大,我一般用:r入口脚本,结构清晰。

5.2 脚本执行顺序与事务控制的取舍

批量执行脚本时,顺序很重要。建表要在插数据之前,建索引可以在插数据之后(数据量大时这样更快),外键约束的创建顺序也有讲究。我习惯按编号命名脚本文件,01_、02_前缀强制排序,配合:r入口脚本,顺序一目了然。

事务控制是个需要权衡的点。把整个脚本包在一个大事务里,出错可以整体回滚,听起来很美,但实际执行时如果脚本很长、数据量很大,事务日志会膨胀得厉害,甚至把日志文件撑爆。我的做法是:结构变更脚本(建表、改表)可以包事务,数据脚本按批次提交,比如每 1000 行COMMIT一次。这样出错时最多回滚一个批次,重跑成本可控。

注意:sqlcmd 默认遇到错误会继续执行后续语句,不会中断。如果你希望出错即停,加-b参数,让 sqlcmd 在遇到错误时返回非零退出码并停止。自动化脚本里这个参数几乎是必加的,否则脚本中间报错了你还以为执行成功了。

5.3 执行结果的验证与日志留存

脚本执行完,别急着说搞定,做几个验证。查表数量、查关键表的行数、查有没有编译错误。我常用的验证查询:

SELECT COUNT(*) FROM sys.tables; SELECT COUNT(*) FROM sys.objects WHERE type = 'P';

把预期值和实际值对一下,心里有数。另外,执行脚本时把输出重定向到日志文件,方便事后追溯:

docker exec -i mssql2017 /opt/mssql-tools/bin/sqlcmd \ -S localhost -U sa -P 'YourStrong@Passw0rd' \ -d DemoDB -i /var/opt/mssql/scripts/init.sql -b \ > /data/mssql/scripts/exec_$(date +%Y%m%d_%H%M%S).log 2>&1

日志文件名带时间戳,多次执行不会互相覆盖。出问题时翻日志,比凭记忆靠谱得多。

6. 实操心得与高频问题排查

6.1 我踩过的几个典型坑

第一个坑是容器重启后数据丢失。早期我没做卷映射,容器一删数据全没,还原的库白做了。后来养成习惯,任何带数据的容器,启动命令里必须有-v,而且映射的是宿主机上明确的目录,不是匿名卷。匿名卷虽然也能持久化,但位置不好找,管理起来麻烦。

第二个坑是密码里的特殊字符。有一次密码设了带!的,在交互式 shell 里!触发历史展开,实际传给容器的密码和我想的不一样,连接一直失败。后来统一用单引号包密码,并且避开 shell 元字符,再没出过问题。

第三个坑是还原大库时超时。一个几 GB 的备份,还原到一半 sqlcmd 连接超时断开,但数据库还原其实还在后台跑。这时候如果急着重试,会撞上独占访问的报错。正确做法是等还原真正结束(看日志或查sys.dm_exec_requests),或者一开始就用-t 0关掉超时。

第四个坑是脚本编码。带中文注释的 SQL 文件,在某些编辑器里存成 GBK,sqlcmd 按 UTF-8 解析就乱码,甚至把注释后面的语句吞掉。统一 UTF-8 无 BOM 之后问题消失。这个坑不常遇到,但遇到一次能查半天。

6.2 高频问题速查表

现象排查第一步常见根因
容器起不来,状态 Restartingdocker logs看首行报错密码不合规、未接受 EULA、内存不足
连不上 1433 端口docker port确认映射端口冲突、容器未就绪
还原报无法打开备份设备进容器ls备份目录路径写错、目录权限不足
脚本执行报语法错误检查文件编码和 BOM编码不匹配、特殊字符转义
中文数据变问号检查排序规则和编码库排序规则非中文、文件编码错
执行成功但数据不对查@@ERROR和日志脚本中途报错但未中断

中文乱码这个问题值得多说一句。Sql Server 的排序规则(Collation)决定了字符比较和存储行为,如果建库时用了默认的拉丁排序规则,存中文可能出问题。建库时显式指定中文排序规则更稳妥:

CREATE DATABASE DemoDB COLLATE Chinese_PRC_CI_AS;

CI表示不区分大小写,AS表示区分重音,这是中文场景下常用的组合。还原的库则沿用备份里的排序规则,不用额外指定。

6.3 把整套流程脚本化的建议

手动敲命令验证一两次可以,反复做就得脚本化。我一般写两个脚本:一个start.sh负责起容器(带好所有参数和卷映射),一个restore_and_run.sh负责还原加执行脚本。脚本里把密码、路径这些做成变量放顶部,换环境只改变量。这样同事拿到脚本,改两行就能跑,比口头交接靠谱。

脚本化时注意幂等性:容器已存在就先删再起,数据库已存在就REPLACE还原,脚本重复执行不报错。幂等做得好,出问题重跑一遍就行,不用手工清理残留状态。这一点在 CI 环境里尤其重要,每次构建都是干净环境,脚本必须能从头跑通。

最后分享一个小技巧:把常用的 sqlcmd 调用封装成 shell 函数或者 alias,比如mssql()自动带上服务器、用户、密码参数,日常敲命令能省不少事。密码别硬编码在 alias 里,从环境变量读,安全一些。这套东西用熟之后,从零到能查数据,真的就是几分钟的事。