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_EULAY和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_EULAY \ -e MSSQL_SA_PASSWORDYourStrongPassw0rd \ -e MSSQL_PIDDeveloper \ -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_PIDDeveloper这个参数值得单独说。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 YourStrongPassw0rd -Q SELECT VERSION没装客户端也没关系进容器里用自带的 sqlcmddocker exec -it mssql2017 /opt/mssql-tools/bin/sqlcmd \ -S localhost -U sa -P YourStrongPassw0rd \ -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_USERExclusive 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 YourStrongPassw0rd \ -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 YourStrongPassw0rd \ -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 YourStrongPassw0rd \ -d DemoDB -i /var/opt/mssql/scripts/init.sql -b \ /data/mssql/scripts/exec_$(date %Y%m%d_%H%M%S).log 21日志文件名带时间戳多次执行不会互相覆盖。出问题时翻日志比凭记忆靠谱得多。6. 实操心得与高频问题排查6.1 我踩过的几个典型坑第一个坑是容器重启后数据丢失。早期我没做卷映射容器一删数据全没还原的库白做了。后来养成习惯任何带数据的容器启动命令里必须有-v而且映射的是宿主机上明确的目录不是匿名卷。匿名卷虽然也能持久化但位置不好找管理起来麻烦。第二个坑是密码里的特殊字符。有一次密码设了带!的在交互式 shell 里!触发历史展开实际传给容器的密码和我想的不一样连接一直失败。后来统一用单引号包密码并且避开 shell 元字符再没出过问题。第三个坑是还原大库时超时。一个几 GB 的备份还原到一半 sqlcmd 连接超时断开但数据库还原其实还在后台跑。这时候如果急着重试会撞上独占访问的报错。正确做法是等还原真正结束看日志或查sys.dm_exec_requests或者一开始就用-t 0关掉超时。第四个坑是脚本编码。带中文注释的 SQL 文件在某些编辑器里存成 GBKsqlcmd 按 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 里从环境变量读安全一些。这套东西用熟之后从零到能查数据真的就是几分钟的事。