先确认操作的是哪个 daemon、哪个 Compose 项目,再分清容器、镜像、卷和端口。以下示例名为 myapp,命令在 Docker 客户端执行;bind mount 路径属于 daemon 所在机器。
确认 daemon,再读容器状态
确认客户端连到哪里
docker context show
再用 docker context ls 对照 Endpoint。命令行 --context、环境变量 DOCKER_HOST / DOCKER_CONTEXT 也可能影响实际目标;docker info 能验证连接与 daemon 身份。连接失败先查地址、服务和权限。
定位容器及退出原因
docker ps -a --format '{{.Names}} {{.Status}} {{.Ports}}'
-a 避免只看到运行实例;后续 myapp 替换为这里查到的名称。Exited 是进程已结束,Restarting 是正在重试,不能仅凭状态推断原因。
docker inspect myapp --format '{{json .State}}'
优先看 ExitCode、OOMKilled、Error、StartedAt / FinishedAt。若存在 Health,展开其中 Log 的探针输出;下表给出判断分支。
docker logs --tail 100 --since 10m myapp
--tail 控制行数,--since 控制时间;持续观察另加 --follow,Ctrl-C 只结束跟随。日志来自容器标准输出/错误及其日志驱动,不保证包含应用自己写入文件的日志。
资源压力与进程是两种检查
docker stats --no-stream myapp
内存使用与限制一起看;短时峰值需要连续观察,单次低占用不能排除此前 OOM。
docker top myapp
这里回答进程存在与否,不代替日志或业务请求检查。
| 证据 | 解释与下一步 |
| ExitCode / Error | 先看应用退出日志;非零码不等于 Docker 自身故障 |
| OOMKilled=true | 结合限制、宿主内存和内核日志检查;137 单独不能证明 OOM |
| unhealthy | 看 Health.Log 的探针;容器可能仍运行 |
| Restarting | 先查应用为什么退出,再看 RestartPolicy;不要先加重启频率 |
把端口与数据的位置写清楚
创建网络与持久化卷
下面是一组新建示例,名称应尚未使用;已有对象先 inspect 核对再复用。创建失败先处理原因,不继续启动容器。
docker network create app-net
成功输出网络 ID;同网络容器可按名称通信。
docker volume create app-data
成功输出卷名;这一步只建立存储对象,不会替应用配置数据目录。
创建一个绑定回环端口的容器
docker run -d --name myapp \
--network app-net \
--publish 127.0.0.1:8080:80 \
--mount type=volume,src=app-data,dst=/app/data \
--restart unless-stopped \
--memory 512m \
nginx:stable
-d 后台运行,--name 固定操作名称,--memory 限制内存,unless-stopped 控制退出后的重启策略。成功返回容器 ID;接着查 ps / logs 和下一节的端口、挂载,不能仅以 ID 判断应用健康。
这是创建语法示例:nginx 不会自动向 /app/data 写业务数据;实际应用必须把持久化路径配置到挂载点。换行反斜杠后不能再接注释。生产镜像按项目要求固定版本或 digest。
正在呈现知识画面
删除容器时,数据留在哪里停止容器保留可写层;删除容器丢失可写层但保留普通命名卷。Compose down -v 还删除本项目的非 external 命名卷,镜像仍可存在。
{"id":"ops-docker-lifetimes","title":"删除容器时,数据留在哪里","summary":"停止容器保留可写层;删除容器丢失可写层但保留普通命名卷。Compose down -v 还删除本项目的非 external 命名卷,镜像仍可存在。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><div class=\"cards\"><section class=\"card \"><h3 data-i18n=\"container\"></h3><strong id=\"container\"></strong></section><section class=\"card \"><h3 data-i18n=\"volume\"></h3><strong id=\"volume\"></strong></section><section class=\"card \"><h3 data-i18n=\"image\"></h3><strong id=\"image\">nginx</strong></section></div><div class=\"controls\"><button type=\"button\" data-action=\"write\" data-i18n=\"write\"></button><button type=\"button\" data-action=\"stop\" data-i18n=\"stop\"></button><button type=\"button\" data-action=\"start\" data-i18n=\"start\"></button><button type=\"button\" data-action=\"remove\" data-i18n=\"remove\"></button><button type=\"button\" data-action=\"create\" data-i18n=\"create\"></button><button type=\"button\" data-action=\"down\" data-i18n=\"down\"></button><button type=\"button\" data-action=\"reset\" data-i18n=\"reset\"></button></div><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hint\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"invalid\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n.card strong {\n font-size: 19px;\n display: block;\n min-height: 3.2em;\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nlet exists = true,\n running = true,\n layer = false,\n volume = true,\n data = false;\nfunction draw() {\n put(\n \"container\",\n (exists ? (running ? \"running\" : \"stopped\") : \"∅\") +\n \" · \" +\n t(exists ? (layer ? \"data\" : \"empty\") : \"absent\"),\n );\n put(\"volume\", t(volume ? (data ? \"data\" : \"empty\") : \"absent\"));\n state({ exists, running, layer, volume, data });\n}\n$$(\"[data-action]\").forEach(\n (b) =>\n (b.onclick = () => {\n let a = b.dataset.action,\n ok = true;\n if (a === \"write\") {\n if (running) {\n layer = true;\n data = true;\n } else ok = false;\n }\n if (a === \"stop\" && exists) running = false;\n if (a === \"start\") {\n if (exists) running = true;\n else ok = false;\n }\n if (a === \"remove\") {\n exists = false;\n running = false;\n layer = false;\n }\n if (a === \"create\") {\n if (exists) ok = false;\n else {\n exists = true;\n running = true;\n layer = false;\n volume = true;\n }\n }\n if (a === \"down\") {\n exists = false;\n running = false;\n layer = false;\n volume = false;\n data = false;\n }\n if (a === \"reset\") {\n exists = true;\n running = true;\n layer = false;\n volume = true;\n data = false;\n }\n say(ok ? \"hint\" : \"invalid\");\n draw();\n }),\n);\nsay(\"hint\");\ndraw();\n","audio":false,"strings":{"absent":"不存在","container":"容器 / 可写层","create":"创建容器","data":"有数据","down":"down -v","empty":"空","hint":"镜像是重建程序的来源;应用写入不会自动进入镜像。卷被删除后,重新创建的是空卷。","image":"镜像","invalid":"这一步不适用于当前状态:写入需要运行容器,启动需要已有容器,创建需要容器不存在。","remove":"删除容器","reset":"重置示例","scope":"一个容器、一个非 external 命名卷、一个已有镜像。固定对象,可自由执行;不模拟 bind mount、匿名卷或 prune。","start":"启动","stop":"停止","volume":"命名卷 app_data","write":"写入两处"}}| 对象 | 操作含义 |
| 127.0.0.1:8080:80 | daemon 主机回环 8080 → 容器 80;其他容器应在自定义网络中访问服务名:80 |
| --mount type=bind | 宿主路径映射;挂载会遮住镜像里目标目录原来的内容 |
| --mount type=volume | Docker 管理的数据对象,生命周期独立于容器 |
| EXPOSE | 镜像元数据,不自动发布宿主端口 |
验证端口与数据路径
docker port myapp
应看到容器 80/tcp 映射到 127.0.0.1:8080。没有输出先查容器创建参数。
docker inspect myapp --format '{{json .Mounts}}'
查看 Type、Name、Destination 和 RW,确认应用实际写入该 Destination。挂载正确但应用写错目录,仍会丢失容器层里的数据。
docker inspect myapp --format '{{json .NetworkSettings.Networks}}'
容器间访问应核对网络是否相同及应用监听地址。需要读容器 DNS 配置时,另用 docker exec myapp cat /etc/resolv.conf。
curl -v http://127.0.0.1:8080/
看连接地址、HTTP 状态与响应正文;远程 context 时应登录 daemon 主机执行,而不是请求客户端自己的回环地址。
读取运行配置或进入容器
docker cp myapp:/etc/nginx/nginx.conf ./nginx.running.conf
源在容器、目标在客户端当前目录;目标文件名应未占用或已备份。复制不是热加载配置。交互诊断可另用 docker exec -it myapp sh;退出 shell 不停止主进程。
HTTP 请求中的 curl 要在 daemon 主机上验证;远程 context 下本机 localhost 不是 daemon。容器没有 shell 时 exec sh 会失败,可用 inspect、logs、cp 或适配其运行时的调试工具,不把缺少 shell 当成容器损坏。
重启不等于应用新配置
先确认 Compose 项目
在正确的 compose.yaml 所在目录执行;示例服务名 app 从 config 输出取得,不等于容器名 myapp。项目目录不同、-f / -p 不同,可能操作完全不同的一组对象。
docker compose config --quiet
成功通常无输出;报错先修复文件或环境变量,不继续发布。
docker compose config --services
对照 docker compose ps -a 的已有实例,确认 app 是本次要更新的服务。
拉取、应用、检查是连续的发布阶段
docker compose pull app
拉取失败先处理认证、网络或镜像引用;成功后才执行 up。本地源码构建服务改用 docker compose build app,不是必须同时执行 pull 与 build。
docker compose up -d app
会根据差异重建容器,需评估连接中断。只重启旧进程的 restart 不应用环境变量等容器配置变化。
docker compose ps -a app
再用 docker compose logs --tail 100 app 与实际业务请求验证。失败时先保留日志,再恢复已记录的上一版镜像及配置并重新 up;数据迁移不会随镜像回退自动撤销。
在正确 Compose 文件目录执行,必要时显式 -f 和 -p 固定项目。restart 重启已有容器,不应用改过的环境变量;up -d 根据变更重建,构建源码项目才加 --build。config 的完整输出可能含已展开凭据,不应随意分享。
| 命令 | 作用 |
| docker stop -t 30 myapp | 按配置的停止信号请求退出,超时后强制结束 |
| docker compose down | 删除项目容器及相应网络,默认不删除具名卷 |
| docker compose down -v | 还删除受管理的具名卷和附属匿名卷;external 卷不由它删除 |
先定位占用,再清理具体对象
从占用清单选择对象
docker system df -v
RECLAIMABLE 是引用规则下可回收的估计,不等于业务允许删除;共享层也使各项大小不能简单相加。
docker container ls -a --filter status=exited
检查 logs、inspect、挂载与容器可写层需要的文件,再决定删除;停止状态不会自动保护其数据。
docker rm myapp
此操作删除容器可写层,具名卷仍在。若容器运行,先按上表 stop 并确认业务停止,不直接加 -f。用 docker ps -a 确认对象消失。
docker volume inspect app-data
还需用 docker ps -a --filter volume=app-data 检查引用,并验证备份能恢复。确定销毁数据时才单独执行 docker volume rm app-data;“没有引用”不代表内容无价值。
镜像清理与镜像搬运分别处理
docker image ls
需要检查构建层时用 docker image history nginx:stable。确认不再需要后,以 docker image rm IMAGE 指定删除;IMAGE 从此列表取得,不用一键清空代替判断。
docker save nginx:stable -o nginx-image.tar
选择不会覆盖已有备份的输出路径。传输到目标机器后,才在目标 Docker 客户端执行下一步。
docker load -i nginx-image.tar
核对输出标签与 docker image inspect nginx:stable 的镜像 ID。save/load 不包含卷数据、运行配置或容器当前可写层。
先确认哪些数据已备份、哪些停止容器仍需保留,再逐个 rm/rmi/volume rm。prune 按“未被引用”等规则判断,不知道业务价值;system prune -a --volumes 不作为日常一键修复。镜像 save/load 不备份卷;commit 也不包含挂载卷内容。
docker.sock 的管理权限通常相当于宿主高权限;不要为消除报错直接 chmod 666。网络异常先检查 daemon、防火墙后端和容器网络,"iptables": false 不是切换到 nftables 后端的方法。
Docker storage · Compose up · Docker nftables