先确认你正在操作哪台 Docker
本文按官方文档整理,核对日期为 2026-09-08,示例未实机验证。read 表示查询,包含仅查询的网络请求;write 表示新建文件、下载落盘或改变运行配置;danger 表示中断、替换、覆盖或删除。代码是供读者执行的操作说明。
本文假设你已经安装并启动 Docker,拥有当前目标引擎的操作权限,终端使用兼容 POSIX 的 shell。项目统一叫 bloglab,服务叫 web,配置文件叫 compose.yaml。Mac 示例目录为 /Users/Shared/bloglab(确认当前用户可写后使用),Linux 示例目录为 /srv/bloglab,群晖示例目录为 /volume1/docker/bloglab;这些都是需要按实际环境替换的路径,不表示目录已存在。后续带相对路径的命令必须在选定项目根目录执行,目录名不参与项目命名,因为所有 Compose 命令显式传入 -p bloglab。
先查 context、客户端与服务端版本,再查 Compose 插件。Mac 上 Docker Desktop 管理 Linux 虚拟机,引擎的数据目录不等于 Finder 中的同名路径;Linux 上通常直接连接本机 Engine,也可能连到远程 context。群晖环境不能只凭 DSM 大版本猜测 Docker、Container Manager 或 Compose 的能力;本文仅引用 Docker 文档,无法据此确认厂商打包版本。若 docker compose 不存在,应先确认设备提供的受支持安装方式,不能把旧 docker-compose 与新版语法随意互换。
docker context show
docker version
docker compose version只查询当前上下文及版本。version 只有 Client 而没有 Server 时,先处理引擎连接问题;不要继续启动服务。
把程序包、运行实例和数据分开理解
镜像可以理解为装有运行时、程序文件和默认启动方式的模板,标签方便引用,但标签可能被发布者重新指向其他内容。容器是从镜像创建的运行实例,有自己的进程和可写层;停止容器不等于删除容器,删除容器却会丢掉没有放到外部存储的可写层数据。对前端程序员而言,构建出的 dist、访问日志和用户上传文件虽然都是文件,生命周期却不同:发布产物可以重建,用户上传通常不能靠重新构建找回。
命名卷由 Docker 管理,绑定挂载则把引擎宿主机上的明确路径挂进容器。开发时把源文件绑定进去很方便,长期服务需要先决定谁负责创建目录、设置权限及备份。挂载会遮住容器原路径已有内容,因此把空目录挂到站点目录后出现空页面,可能是挂载选择问题,不是镜像损坏。我们的静态站点使用命名卷 site,默认实际卷名为 bloglab_site;首次创建空卷时,镜像对应目录的内容通常会复制进去,已有卷则会保留旧内容,不能指望更新镜像自动更新卷中的网页。
services:
web:
image: nginx:1.28.0-alpine
ports:
- "127.0.0.1:18080:80"
volumes:
- site:/usr/share/nginx/html:ro
restart: unless-stopped
volumes:
site: {}保存为项目根目录的 compose.yaml 会写入文件。镜像标签仅作固定版本语法示例,不代表截至核对日的最新或推荐安全版本。服务只读挂载静态内容。
端口映射解决入口,不代替容器内服务监听
映射中的 18080 是宿主机入口,80 是容器内 Nginx 监听端口。前面的 127.0.0.1 把示例入口限定在引擎宿主机回环地址;在 Mac 本机 Docker Desktop 上通常访问本机 localhost,在 NAS 上则是 NAS 自己的回环,不能拿笔记本的 localhost 代替。需要局域网访问时,应先决定监听地址和网络访问控制,再修改映射。旧 Engine 对回环发布的隔离存在已知版本差异,不能把这个配置当成所有网络条件下的完整安全边界。
同一个 Compose 网络里的后端服务通常通过服务名及容器端口互访,例如应用连接 db:5432,而不是宿主机映射端口。容器里的 localhost 指向该容器本身。浏览器运行在用户电脑上,无法直接解析 Compose 的服务名,因此前端浏览器请求 API 时仍要使用用户能够访问的域名、宿主机端口或反向代理入口。EXPOSE 只是镜像元数据,不能替代发布端口;看见端口数字并不意味着外部网络一定能够访问。
docker compose -p bloglab -f compose.yaml config -q
docker compose -p bloglab -f compose.yaml config --services正常时第一条无输出且退出码为零,第二条应出现 web。不要为了排错直接公开完整 config 或 --environment 输出。
docker compose -p bloglab -f compose.yaml up -d web会创建网络、卷和容器,缺少镜像时可能联网下载。仅在确认目录、端口和目标引擎正确后执行。
从首次启动走到能解释结果的验收
这次练习的交付物是一个只在引擎宿主回环端口提供页面的静态服务,以及一份说明它从哪里读取文件的记录。先准备一个可写项目目录,把上面的完整配置原样保存,确认项目名未被其他服务使用,再按顺序完成配置检查和启动。第一次练习不混入自己的前端构建、不增加代理、不连接数据库,是为了把故障范围固定在镜像、端口和挂载三件事上;它们都验收后,才有可靠的基础接入自己的页面。
服务启动后,用不带浏览器缓存的单次请求读取首页。示意中的成功条件是收到正常状态与非空页面,并且内容符合当前卷中的文件,不预先写死欢迎页文字,因为镜像版本和既有卷会影响实际内容。如果请求被拒绝,回到端口与进程;如果得到不存在或无权限的页面,说明请求已经抵达某个服务器,应进一步确认它是否就是本项目的容器。只凭页面样式相似也不够,另一个占用端口的程序可能返回同样的欢迎页面。
把响应时间和日志时间对应起来:发起一次请求后,查询最近一分钟的日志,找相同路径与状态。默认镜像的日志输出方式仍须以实际标签为准;如果没有访问记录,先检查请求是否走了代理、访问的回环端口是否正确以及应用日志去向,再解释空日志。这里不在容器内安装调试软件,避免为了观察而改变了被观察的运行环境。所有输出示例只定义验收格式,没有代表本机已经测得的数值。
curl --noproxy '*' --fail --show-error --connect-timeout 3 --max-time 10 http://127.0.0.1:18080/read:发起一次 GET,不保存响应文件;服务端可能记录访问。用于本机明确无写副作用的静态首页。
docker compose -p bloglab -f compose.yaml logs --since 1m --tail 30 --timestamps web找出与请求相符的路径和状态;没有条目时检查日志去向。
按配置、进程、入口、应用逐层排错
网页打不开时先把问题拆成四层。配置层看服务名和 YAML 是否正确;进程层看容器是否退出或反复重启;入口层看端口是否绑定到预期地址;应用层才查看路由、静态文件和依赖。不要同时改端口、换镜像、删卷和重启引擎,否则即使恢复也不知道哪个动作有效。ps 中的 running 只说明主进程存在,应用仍可能返回错误页;如果没有健康检查,不能把 running 解释为业务已就绪。
日志先限定最近十五分钟和最后两百行,保留时间戳来对齐浏览器报错发生时间。permission denied 应回到挂载目录和运行身份检查,不应直接递归开放全部权限;address already in use 应检查宿主机端口冲突,而非不断更换容器内部端口。日志本身也可能包含请求地址或敏感业务内容,对外分享前要脱敏。inspect 仅提取状态、镜像 ID 或挂载点等必要字段,不全量输出可能带密码的环境配置。
docker compose -p bloglab -f compose.yaml ps -a
docker compose -p bloglab -f compose.yaml port web 80看退出状态以及实际绑定地址;port 查询不到时核对服务是否创建及端口配置。
docker compose -p bloglab -f compose.yaml logs --since 15m --tail 200 --timestamps web按时间定位失败,日志为空不能单独证明正常,应用可能输出到文件而非标准输出。
docker compose -p bloglab -f compose.yaml ps -a -q web
# 把下一行的 CONTAINER_ID 替换为上一条返回的一个容器 ID
docker inspect --format '{{range .Mounts}}{{println .Type .Name .Destination .RW}}{{end}}' CONTAINER_ID应看到 volume、bloglab_site、站点目录和 false;不读取 Env、完整 HostConfig 或卷驱动凭据。
重建一次容器,验证数据生命周期
接下来做一次有目的的重建,目的是验证配置可以重新建立运行实例,同时仍使用同一个命名卷。先记录容器编号、镜像编号和挂载卷名,确认现有页面已通过验收,再执行限定到静态服务的强制重建。这个动作会停止旧实例,所以只适合自己的练习项目或已安排的维护窗口。它不是修复任意故障的默认手段,而是一项专门验证生命周期的实验;遇到前置检查失败就停止,不顺手删卷重新开始。
重建完成后再次查询容器编号及挂载,预期是容器编号变化,卷名仍然相同,页面仍能读取。镜像没有变化时镜像编号也应保持一致。若网页内容发生意外改变,先比较卷名、项目名、配置文件路径和实际镜像,判断是否启动了另一套资源;不要立刻得出卷不持久化的结论。如果新编号没变化,则核对是否真的执行了强制重建以及前一步是否因参数错误而退出。
容器可写层和数据卷的区别也可以在记录中表达:默认配置、可重建程序属于镜像,进程状态属于容器,静态目录的内容属于卷。为了验证可写层丢失而在运行容器里手工编辑正式文件没有必要;只要明确这种编辑不会自动进入原始镜像或部署配置,就能避免把一次救急修改误当成永久修复。未来引入构建流水线时,应让发布产物有明确来源,并单独决定网页究竟随镜像发布还是作为卷内容维护。
docker compose -p bloglab -f compose.yaml up -d --force-recreate --no-deps --no-build --pull never webdanger:停止旧实例并替换;要求镜像已在本地。重建后重复状态、挂载和 HTTP 验收。
让日常操作能复现
把配置文件作为部署记录,记录所用镜像版本、项目名、端口意图、数据位置和恢复办法,比保存某个容器名称更有价值。不要手工修改容器内文件后把它当成唯一部署产物,因为下次重建可能消失。示例没有数据库,真实项目增加 db 服务时,还要区分启动顺序与服务就绪:depends_on 的普通依赖关系不保证数据库可以接受连接,应用需要重试机制,必要时结合 healthcheck 与 service_healthy。
停机维护前明确自己需要的是暂停进程、重建某个服务还是拆除整个项目。restart 主要重启现有容器,并不是应用新镜像或所有配置变更的通用方式。数据还在卷里也不意味着备份已经完成,误删卷、磁盘故障及程序写坏数据都需要独立备份应对。下一篇以可恢复的静态卷演练这些步骤,再讨论数据库为什么不能照搬文件打包命令。
把常见症状写成下一步,并结束练习
示意故障一是启动时提示端口已占用。先看本项目是否已有旧实例,再确认该端口的现有使用者,决定保留它还是给练习选择新的宿主端口;容器内部监听端口仍按应用配置设置。示意故障二是容器持续退出,读取第一轮启动日志通常比反复重启更有效,重点看配置解析、文件访问和入口程序错误。示意故障三是容器稳定但页面不是预期版本,优先核对卷内容遮蔽镜像文件这一条,而不是重新下载相同镜像。
停止练习时可以只停止服务,保留容器和卷供下一篇备份演练使用。再次继续时启动原容器并重新请求页面,确认自己知道如何从停止状态恢复。停止后的回环入口不可用是预期状态,应在记录里写成“练习已暂停”,不能把它与异常停机混在一起。若确实要拆除项目,应另行核对资源清单;本文不把删除卷作为收尾步骤,因为后续恢复练习仍以这个静态卷为输入。
一份完整交接记录应能回答:在哪个上下文执行,配置文件在哪里,服务叫什么,镜像引用和编号是什么,宿主端口为何绑定回环,数据由哪个卷保存,哪次请求验收过,当前是运行还是暂停。最后附上尚未验证的项目,例如多架构运行、局域网访问、浏览器缓存、宿主重启和异机恢复。这样的记录能让下一次维护沿着同一条路径继续,也能防止把一个本地演示当成已经具备生产运行条件的自托管服务。
示意交接样本:用途为本地默认静态页面;配置文件为项目根目录的 compose.yaml;服务为 web;数据卷为 bloglab_site;入口为宿主回环的一八零八零端口;重建前后对照关注容器编号变化和卷名不变;收尾选择暂停并保留卷,供下一篇备份。这里填写的是设计和预期,没有伪造执行时间或设备结果。真正运行时把请求状态、编号和当前运行状态换成自己的记录。
docker compose -p bloglab -f compose.yaml stop -t 30 webdanger:服务中断,但不删除卷;准备备份时保持此状态。
docker compose -p bloglab -f compose.yaml start webwrite:启动已有实例;随后重新执行首页请求。
bloglab/
compose.yaml # 由上文 YAML 保存
backups/ # 下一篇的归档输出
Docker 管理资源:
bloglab_site # 独立于项目文件夹的命名卷
web 实例 # 容器编号由实际创建决定阅读用示意材料;不是命令输出或实测记录,复制保存为记录文件属于 write。