这一节会把纸舟书店的数据库服务真正启动起来。我们采用 Docker 官方 mongo:8.0 镜像,并在容器内使用 mongosh。这样做有三个目的:明确 MongoDB 的主版本边界、让启动过程可以重复、让后续清理有清楚的对象。
课程会创建三个有名字的资源:网络 paperboat-net、数据卷 paperboat-mongo-data、容器 paperboat-mongo。后面的命令都沿用这些名称,请不要随意改名,否则连接命令和副本集配置也要一起调整。
本节为了先理解数据库行为,只把端口绑定到 127.0.0.1,并且尚未启用身份认证。不要把这个容器暴露到公网或共享网络。用户、角色、最小权限和 TLS 会在安全部分单独配置。
MongoDB 可以通过多种方式运行。课程选择容器,并不意味着原生安装不可取;它只是让数据库版本、进程参数和数据目录更容易统一。
本课程完整实践走第一条路线。若你明确要学习原生服务管理,请从 MongoDB 8.0 的 Community Edition 安装入口 选择对应系统,并另外安装 mongosh。不要把不同版本博客中的仓库地址、包名和服务命令拼在一起。
Docker 的官方安装入口如下:
Docker Desktop 的系统要求和许可条款会更新,以链接中的当前说明为准。安装完成后启动 Docker 服务,再继续下面的体检。
课程中的多行命令统一使用 Bash 兼容语法。macOS 可以使用系统自带的终端,Linux 可以使用 Bash,Windows 建议在启用 Docker Desktop WSL 2 集成后使用 WSL 终端。不要把反斜线续行命令原样粘贴进不兼容的命令解释器。
前半程主要使用 docker、mongosh 和 curl,副本集与分片章节还会使用 docker compose。mongosh 已包含在课程使用的 MongoDB 镜像中,不要求额外安装;curl 用来向 Node.js API 发送请求。
这里先检查命令是否存在,不把某个补丁版本写成固定答案。只要 Docker 客户端与服务端能通信、Compose 子命令可用、curl 能显示版本信息,就具备继续学习的工具条件。
docker version --format 'client={{.Client.Version}} server={{.Server.Version}}'
docker compose version
curl --version输出会因安装时间和操作系统而变化,结构应类似:
client=<Docker 客户端版本> server=<Docker 服务端版本>
Docker Compose version v<版本号>
curl <版本号> ...如果 docker compose 不存在,先按 Docker 官方说明补齐 Compose 插件;如果 curl 不存在,先使用系统包管理器安装。不要等到 API 章节才发现基础命令缺失。
docker 命令是客户端,它需要连接到正在运行的 Docker 服务端。只看到客户端版本还不够;如果服务端没有启动,后续 pull 和 run 都会失败。docker version 同时返回两端信息,因此适合做第一道体检。
打开终端,执行:
docker version --format 'client={{.Client.Version}} server={{.Server.Version}}'正常结果会同时出现两个版本号,例如:
client=<Docker 客户端版本> server=<Docker 服务端版本>尖括号中的内容取决于你安装的 Docker 版本,不是课程要求固定的值。真正的检查条件是 client= 和 server= 后面都有值,并且命令正常退出。
如果看到 Cannot connect to the Docker daemon,先启动 Docker Desktop 或 Docker Engine,再重新执行。此时不要急着运行 MongoDB 命令,因为数据库容器还没有运行基础。
镜像是创建容器的只读模板,容器是镜像的一次运行实例。mongo:8.0 中的 8.0 是主次版本标签:它保持在 8.0 系列,但维护者可以把标签更新到新的 8.0 补丁版本。这样能获得同一主版本的修复,也意味着补丁号和镜像摘要可能随时间变化。
Docker Hub 的 mongo 官方镜像页面说明了启动、初始化脚本、认证变量和数据卷等约定。课程使用这个官方镜像,不使用名称相近的第三方镜像。
拉取 8.0 系列镜像,并查看它当前对应的仓库摘要:
docker pull mongo:8.0
docker image inspect mongo:8.0 --format '{{json .RepoDigests}}'拉取时会看到分层下载信息,最后类似:
8.0: Pulling from library/mongo
Digest: sha256:<镜像摘要>
Status: Downloaded newer image for mongo:8.0如果镜像已经存在,状态可能是 Image is up to date。第二条命令会输出包含 mongo@sha256:... 的数组。摘要由当前镜像内容决定,发生补丁更新后变化是正常现象。
容器本身可以删除重建,书店数据却需要跨重启保留。命名数据卷 paperboat-mongo-data 用于保存 /data/db;命名网络 paperboat-net 让后续应用容器可以通过容器名找到数据库。
这两个资源都有明确名称,因此可以检查、复用并在课程结束时精确删除,不会和其他项目混在一起。
依次创建网络与数据卷:
docker network create paperboat-net
docker volume create paperboat-mongo-data第一条命令返回网络 ID,它是一串动态生成的十六进制字符;第二条命令返回固定的数据卷名称:
<动态生成的网络 ID>
paperboat-mongo-data如果提示同名网络或数据卷已经存在,说明你以前执行过这一步。可以先运行 docker network inspect paperboat-net 和 docker volume inspect paperboat-mongo-data 确认它们确实属于这门课程,然后继续使用。
我们从一开始就为 mongod 加上 --replSet rs0。即使当前只有一个节点,它也会按副本集模式运行,为后面的事务、复制和故障转移课程留下连续路径。--bind_ip_all 让 MongoDB 在容器网络内监听所有接口,而端口映射左侧的 127.0.0.1 限制了容器所在设备上的访问入口。
命令中的 -d 表示后台运行,--name 和 --hostname 固定容器身份,-v 把命名卷挂载到 MongoDB 的数据目录。
执行下面这条完整命令:
docker run -d \
--name paperboat-mongo \
--hostname paperboat-mongo \
--network paperboat-net \
-p 127.0.0.1:27017:27017 \
-v paperboat-mongo-data:/data/db \
mongo:8.0 \
--replSet rs0 --bind_ip_all命令成功后会打印容器 ID:
<动态生成的容器 ID>容器 ID 每次创建都会不同,不能拿它和示例逐字符比较。接着查看容器表:
docker ps 展示正在运行的容器。我们用名称过滤,避免其他项目的容器干扰判断。STATUS 中的秒数会持续变化,PORTS 则应该表明 127.0.0.1:27017 已映射到容器的 27017。
docker ps \
--filter 'name=^/paperboat-mongo$' \
--format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'NAMES IMAGE STATUS PORTS
paperboat-mongo mongo:8.0 Up <运行时长> 127.0.0.1:27017->27017/tcpUp <运行时长> 是动态值。关键是只有目标容器这一行,并且状态以 Up 开头。
容器进入 Up 状态只说明主进程已经启动,不一定说明数据库已完成初始化。最可靠的检查不是猜等待几秒,而是实际发送 ping。docker exec 会在正在运行的容器中执行 mongosh,--quiet 减少与结果无关的启动信息。
执行:
docker exec paperboat-mongo \
mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'数据库就绪时返回:
{ ok: 1 }如果第一次执行时连接被拒绝,说明初始化尚未完成。稍等片刻后原样重试;不要修改端口或容器名来“碰运气”。
--replSet rs0 只告诉 mongod 以副本集模式启动,还没有写入成员配置。rs.initiate() 会创建名为 rs0 的副本集,并把当前容器登记为成员。成员地址使用 paperboat-mongo:27017,因为这个名称能在 paperboat-net 中解析。
初始化只需要执行一次。副本集配置保存在数据卷中,停止并重新启动容器后仍然存在。
docker exec paperboat-mongo mongosh --quiet --eval '
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "paperboat-mongo:27017" }
]
})
'首次初始化成功时,核心结果是:
{ ok: 1 }随后节点需要完成一次选举。检查它是否成为可写主节点:
db.hello() 返回连接所见的拓扑信息。单节点副本集中,我们关心 setName 是否为 rs0,以及 isWritablePrimary 是否为 true。第一次查询若仍为 false,等待短暂选举完成后重试即可。
docker exec paperboat-mongo mongosh --quiet --eval '
const hello = db.hello();
print(EJSON.stringify({
setName: hello.setName,
isWritablePrimary: hello.isWritablePrimary
}));
'{"setName":"rs0","isWritablePrimary":true}这说明当前部署已经是名为 rs0 的副本集,并且唯一成员可以接受写入。若重复执行 rs.initiate(),MongoDB 会提示副本集已经初始化;那不是数据损坏,而是说明初始化步骤不应重复。
课程要求的是 MongoDB 8.0 主版本,而不是某个永远固定的补丁号。我们同时查看服务端和 shell 版本:mongod --version 说明数据库版本,mongosh --version 说明交互客户端版本。两者是独立发布的软件,版本号不需要相同。
docker exec paperboat-mongo mongod --version
docker exec paperboat-mongo mongosh --version
docker exec paperboat-mongo mongosh --quiet --eval '
print(EJSON.stringify({
serverVersion: db.version(),
ping: db.adminCommand({ ping: 1 }).ok
}));
'一次实际运行中,mongo:8.0 解析到了 MongoDB 8.0.26,镜像内 mongosh 为 2.9.2,最终体检摘要为:
{"serverVersion":"8.0.26","ping":1}你执行时补丁号与 mongosh 版本可能更高。验收条件是 serverVersion 以 8.0. 开头,并且 ping 为 1。这比背诵示例补丁号更可靠。
停止容器会结束 MongoDB 进程,但不会删除容器和命名数据卷。再次启动同一个容器后,副本集配置仍在。这个区别很重要:stop 是暂停服务,rm 是删除容器,volume rm 才会删除课程数据。
docker stop paperboat-mongo
docker start paperboat-mongo
docker exec paperboat-mongo \
mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'前两条命令分别返回容器名,最后重新得到 ok: 1:
paperboat-mongo
paperboat-mongo
{ ok: 1 }启动后若数据库还在恢复,第一次 ping 可能暂时连接失败,稍后原样重试即可。数据卷没有被删除,因此不需要再次执行 rs.initiate()。
到这里,纸舟书店已经有了可连接的 MongoDB 8.0 服务:容器正在运行,数据写入命名卷,副本集 rs0 已可写,ping 返回 1。下一节会进入 bookstore 数据库并导入固定种子数据。