[{"data":1,"prerenderedAt":16879},["ShallowReactive",2],{"\u002Ftutorials\u002Fdocker":3,"tutorials-search\u002Ftutorials\u002Fdocker":16413},{"id":4,"title":5,"body":6,"category":559,"cover":16403,"date":16404,"description":16405,"extension":16406,"meta":16407,"navigation":9716,"path":16408,"seo":16409,"sitemap":16410,"stem":16411,"__hash__":16412},"tutorials\u002Ftutorials\u002Fdocker.md","Docker 教程：从镜像分层到生产编排",{"type":7,"value":8,"toc":16259},"minimark",[9,14,27,39,44,142,153,157,160,258,265,269,272,278,382,400,411,415,422,532,539,542,546,550,577,580,620,638,642,673,679,797,801,808,871,896,900,921,946,964,974,978,988,1076,1089,1093,1103,1106,1133,1136,1139,1143,1150,1154,1157,1314,1317,1352,1356,1397,1404,1410,1421,1425,1428,1475,1499,1503,1532,1552,1555,1670,1676,1680,1687,1691,1716,1719,1752,1756,1828,1855,1886,1890,1915,1963,1985,1989,1995,2100,2103,2184,2188,2256,2276,2279,2319,2335,2339,2379,2382,2532,2536,2543,2588,2597,2602,2606,2660,2681,2685,2689,2737,2867,2889,2896,2907,2942,2972,2978,2982,2985,2996,3082,3085,3139,3143,3151,3198,3202,3205,3289,3328,3332,3431,3466,3470,3481,3526,3541,3560,3566,3570,3579,3583,3699,3702,3762,3766,4168,4172,4413,4430,4433,4522,4525,4579,4591,4596,4599,4603,4610,4618,4699,4716,4748,4752,4762,4828,4864,4868,4878,4896,4900,4903,4992,4995,5115,5123,5127,5140,5204,5219,5223,5232,5261,5265,5268,5296,5303,5318,5323,5326,5330,5333,5336,5444,5451,5455,5470,5499,5506,5510,5579,5598,5668,5681,5705,5709,5768,5775,5858,5876,5892,5896,5934,5971,5995,6021,6024,6094,6118,6123,6126,6130,6134,6140,6247,6254,6263,6266,6351,6461,6485,6508,6512,6566,6576,6580,6657,6682,6685,6777,6788,6792,6803,6852,6855,6940,6967,6971,6974,7008,7036,7042,7046,7049,7053,7056,7099,7127,7145,7149,7197,7203,7248,7262,7278,7282,7333,7357,7361,7433,7444,7468,7486,7489,7563,7566,7573,7661,7686,7749,7753,7803,7830,7834,7837,7843,7861,7864,7868,7872,7885,7915,7919,8053,8069,8073,8346,8349,8804,8834,8838,8849,8860,8909,8925,8937,8941,9126,9204,9221,9225,9228,9232,9236,9302,9392,9413,9425,9429,9474,9490,9580,9589,9618,9622,9798,9831,9835,9908,9945,9955,9958,10025,10052,10068,10071,10252,10255,11058,11081,11084,11088,11091,11094,11141,11219,11247,11257,11261,11353,11363,11367,11416,11458,11464,11468,11492,11517,11522,11528,11534,11538,11541,11564,11596,11600,11610,11680,11704,11719,11723,11763,11844,11854,11858,11864,11939,11969,11973,12058,12081,12137,12141,12234,12248,12308,12322,12337,12341,12345,12349,12356,12367,12371,12403,12405,12462,12465,12495,12623,12626,12661,12678,12681,12731,12741,12840,12860,12864,12894,12923,12926,12976,12980,12987,13071,13084,13100,13104,13146,13173,13198,13202,13217,13220,13229,13246,13249,13268,13295,13338,13363,13367,13371,13389,13437,13453,13491,13495,13514,13544,13553,13635,13648,13651,13654,13741,13750,13759,13817,13839,13843,13847,13850,13947,13954,13958,13961,14027,14221,14251,14260,14307,14332,14336,14343,14346,14402,14405,14585,14588,14591,14602,14606,14610,15010,15014,15292,15311,15315,15470,15736,15827,15969,16119,16235,16238,16243,16255],[10,11,13],"h2",{"id":12},"一容器到底解决了什么镜像容器与仓库三件套","一、容器到底解决了什么：镜像、容器与仓库三件套",[15,16,17,18,22,23,26],"p",{},"先还原一个每天都在上演的场景。你在自己机器上跑通了服务，",[19,20,21],"code",{},"python app.py"," 一切正常；交给测试同学，报错 ",[19,24,25],{},"ModuleNotFoundError: No module named 'pandas'","；测试环境装了 pandas，你本地是 1.1 而那边是 2.0，接口返回的日期格式对不上；终于版本对齐了，生产机是 ARM 架构，而依赖里有一个需要现场编译 C 扩展的包。",[15,28,29,30,34,35,38],{},"「在我机器上能跑」这句话之所以让人头疼，不是因为机器有坏有好，而是因为",[31,32,33],"strong",{},"运行环境是一组隐式依赖的集合","：操作系统版本、C 库（glibc 还是 musl）、解释器与依赖库的具体版本、时区、环境变量、字体、可用的系统调用、CPU 架构。传统部署方式里，这堆东西散落在各台机器上，靠文档和运维的记忆维持一致。容器要解决的正是这个：",[31,36,37],{},"把「应用 + 它需要的一切」打成一个不可变的包","，让隐式依赖变成显式内容。",[40,41,43],"h3",{"id":42},"镜像容器仓库三个名词先把边界划清","镜像、容器、仓库：三个名词先把边界划清",[45,46,47,66],"table",{},[48,49,50],"thead",{},[51,52,53,57,60,63],"tr",{},[54,55,56],"th",{},"概念",[54,58,59],{},"它是什么",[54,61,62],{},"类比",[54,64,65],{},"关键性质",[67,68,69,84,98,119],"tbody",{},[51,70,71,75,78,81],{},[72,73,74],"td",{},"镜像 image",[72,76,77],{},"只读的文件系统快照 + 启动参数",[72,79,80],{},"安装光盘 \u002F class",[72,82,83],{},"不可变、分层、可按内容寻址（digest）",[51,85,86,89,92,95],{},[72,87,88],{},"容器 container",[72,90,91],{},"镜像加上一个可写层，跑起来的进程",[72,93,94],{},"进程 \u002F new 出来的对象",[72,96,97],{},"可创建、可停止、可删除，数据默认随它消失",[51,99,100,103,106,109],{},[72,101,102],{},"仓库 repository",[72,104,105],{},"存放某个应用的一组镜像（按 tag 区分）",[72,107,108],{},"Maven 仓库",[72,110,111,114,115,118],{},[19,112,113],{},"nginx","、",[19,116,117],{},"ghcr.io\u002Fexample\u002Fshop-api"," 都是一个仓库",[51,120,121,124,127,130],{},[72,122,123],{},"注册中心 registry",[72,125,126],{},"提供仓库服务的服务器",[72,128,129],{},"私服 Nexus",[72,131,132,114,135,138,139],{},[19,133,134],{},"docker.io",[19,136,137],{},"ghcr.io","、自建 ",[19,140,141],{},"registry:2",[15,143,144,145,148,149,152],{},"最容易混的是最后两个：仓库是一组 tag，注册中心是那个服务。而镜像与容器的关系，第三章讲分层时会具体化：",[31,146,147],{},"镜像是只读的，容器启动时在这一叠只读层上面盖了一层自己的读写层","。你往容器里写的任何文件都只在那一层里，",[19,150,151],{},"docker rm"," 之后一并消失——这是新手最常踩的第一个坑（第九章）。",[40,154,156],{"id":155},"容器不是轻量虚拟机共享内核是全部差异的来源","容器不是轻量虚拟机：共享内核是全部差异的来源",[15,158,159],{},"很多人把容器理解成「小一点的虚拟机」，这个类比会让你后面所有判断都跑偏。",[45,161,162,175],{},[48,163,164],{},[51,165,166,169,172],{},[54,167,168],{},"维度",[54,170,171],{},"虚拟机",[54,173,174],{},"容器",[67,176,177,190,201,212,223,234,245],{},[51,178,179,182,185],{},[72,180,181],{},"内核",[72,183,184],{},"每个 VM 自己一个完整内核",[72,186,187],{},[31,188,189],{},"所有容器共享宿主内核",[51,191,192,195,198],{},[72,193,194],{},"启动",[72,196,197],{},"几十秒（走完整 BIOS\u002Fsystemd）",[72,199,200],{},"几十到几百毫秒（只是起一个进程）",[51,202,203,206,209],{},[72,204,205],{},"内存开销",[72,207,208],{},"预先分配整个 guest 内存",[72,210,211],{},"就是进程本身，用多少占多少",[51,213,214,217,220],{},[72,215,216],{},"镜像大小",[72,218,219],{},"几 GB",[72,221,222],{},"几十 MB 到几百 MB",[51,224,225,228,231],{},[72,226,227],{},"单机密度",[72,229,230],{},"几台到十几台",[72,232,233],{},"几十到几百个",[51,235,236,239,242],{},[72,237,238],{},"隔离强度",[72,240,241],{},"硬件虚拟化，边界很硬",[72,243,244],{},"内核特性 + 能力裁剪，边界是「软」的",[51,246,247,250,253],{},[72,248,249],{},"跨内核",[72,251,252],{},"Linux VM 可以跑在 Windows 宿主上",[72,254,255],{},[31,256,257],{},"Linux 容器必须跑在 Linux 内核上",[15,259,260,261,264],{},"最后一行值得单独强调：容器不是把硬件虚拟化了一遍，它只是把",[31,262,263],{},"一个进程的可见世界","做了限制。所以 Windows 上跑 Linux 容器必须在 Linux 内核上面垫一层虚拟机——第二章讲 Docker Desktop 时这个事实会造成一堆「奇怪现象」（bind mount 慢、host 网络不生效）。",[40,266,268],{"id":267},"namespace-管看得见什么cgroup-管用得了多少","namespace 管「看得见什么」，cgroup 管「用得了多少」",[15,270,271],{},"容器为什么能「像独立机器」？靠的是内核里两套机制，分工非常清楚。",[15,273,274,277],{},[31,275,276],{},"namespace"," 负责可见性的隔离，让同一个进程看到的世界上少了一半东西：",[45,279,280,292],{},[48,281,282],{},[51,283,284,286,289],{},[54,285,276],{},[54,287,288],{},"隔离了什么",[54,290,291],{},"你在容器里的直观感受",[67,293,294,311,327,340,356,369],{},[51,295,296,301,304],{},[72,297,298],{},[19,299,300],{},"mnt",[72,302,303],{},"挂载点",[72,305,306,307,310],{},"根目录是镜像的文件系统，宿主的 ",[19,308,309],{},"\u002Fhome"," 看不见",[51,312,313,318,321],{},[72,314,315],{},[19,316,317],{},"pid",[72,319,320],{},"进程号",[72,322,323,326],{},[19,324,325],{},"ps"," 里自己的应用是 PID 1，看不见宿主的 sshd",[51,328,329,334,337],{},[72,330,331],{},[19,332,333],{},"net",[72,335,336],{},"网络栈",[72,338,339],{},"有自己的网卡、IP、路由表、iptables",[51,341,342,347,350],{},[72,343,344],{},[19,345,346],{},"uts",[72,348,349],{},"主机名",[72,351,352,355],{},[19,353,354],{},"hostname"," 返回容器名",[51,357,358,363,366],{},[72,359,360],{},[19,361,362],{},"ipc",[72,364,365],{},"信号量 \u002F 共享内存",[72,367,368],{},"只能和同组容器通信",[51,370,371,376,379],{},[72,372,373],{},[19,374,375],{},"user",[72,377,378],{},"UID\u002FGID 映射",[72,380,381],{},"rootless 模式下容器 root 其实是宿主普通用户",[15,383,384,387,388,391,392,395,396,399],{},[31,385,386],{},"cgroup","（control group）负责资源的计量与限制：CPU 时间、内存上限、块设备 IO、进程数、网络带宽（通过 cgroup v2）。",[19,389,390],{},"docker stats"," 看到的数字、",[19,393,394],{},"--memory"," 的限制、第十四章那个 ",[19,397,398],{},"Exited (137)","，全部来自 cgroup。",[15,401,402,403,406,407,410],{},"一句话记住分工：",[31,404,405],{},"namespace 决定容器「不知道宿主存在」，cgroup 决定容器「不能把宿主吃穷」","。也正因为隔离是靠内核特性而不是硬件，容器才又快又省，但也才需要第十五章那整套安全加固——默认的容器",[31,408,409],{},"不是","一个足够硬的安全边界。",[40,412,414],{"id":413},"一次构建到处运行的边界在哪里","「一次构建，到处运行」的边界在哪里",[15,416,417,418,421],{},"这句宣传语是真的，但它有明确的适用范围。下面这些东西",[31,419,420],{},"不在镜像里","，因此换机器就可能变行为：",[45,423,424,437],{},[48,425,426],{},[51,427,428,431,434],{},[54,429,430],{},"不受镜像控制",[54,432,433],{},"后果示例",[54,435,436],{},"正确做法",[67,438,439,454,471,482,495,506,517],{},[51,440,441,444,451],{},[72,442,443],{},"宿主内核版本与模块",[72,445,446,447,450],{},"老内核跑新镜像报 ",[19,448,449],{},"unknown mount option"," \u002F overlay 不支持",[72,452,453],{},"统一宿主基线，CI 里用目标内核测",[51,455,456,459,465],{},[72,457,458],{},"CPU 架构",[72,460,461,464],{},[19,462,463],{},"exec format error","（第十八章）",[72,466,467,468],{},"多架构镜像（第五章）或固定 ",[19,469,470],{},"--platform",[51,472,473,476,479],{},[72,474,475],{},"设备与驱动",[72,477,478],{},"GPU 容器缺少 nvidia 运行时",[72,480,481],{},"设备透传 + 对应 runtime",[51,483,484,487,490],{},[72,485,486],{},"资源上限",[72,488,489],{},"同镜像在小机器上 OOM 被杀",[72,491,492,494],{},[19,493,394],{}," \u002F Compose 显式声明（第十四章）",[51,496,497,500,503],{},[72,498,499],{},"文件系统属主 UID",[72,501,502],{},"bind mount 进去写不进去",[72,504,505],{},"对齐 UID 或用卷（第九章）",[51,507,508,511,514],{},[72,509,510],{},"外部地址与密钥",[72,512,513],{},"连不上数据库、密钥泄漏",[72,515,516],{},"环境变量与 secrets 注入（第十三章）",[51,518,519,522,525],{},[72,520,521],{},"并发与连接池大小",[72,523,524],{},"单机调好的参数在 8 核机上打爆 DB",[72,526,527,528,531],{},"从 ",[19,529,530],{},"--cpus"," 推导，不写死",[15,533,534,535,538],{},"所以准确的说法是：",[31,536,537],{},"镜像保证「应用及其用户空间依赖」一致，不保证「运行时环境」一致","。剩下的差异要么由编排层注入，要么由你在配置里显式声明。理解了这条边界，后面十七章的所有「为什么要这样写」都有了落点。",[15,540,541],{},"下一章开始动手：装好它，跑通第一个容器，并看清那条命令背后到底发生了什么。",[10,543,545],{"id":544},"二安装与第一次运行daemon客户端与虚拟机的本质","二、安装与第一次运行：daemon、客户端与虚拟机的本质",[40,547,549],{"id":548},"daemon-与客户端docker-命令其实只是个-http-客户端","daemon 与客户端：docker 命令其实只是个 HTTP 客户端",[15,551,552,553,556,557,560,561,564,565,568,569,572,573,576],{},"很多人以为 ",[19,554,555],{},"docker run"," 是本机执行了容器，其实不是。真实链路是：",[19,558,559],{},"docker"," CLI 发 HTTP 请求 → ",[19,562,563],{},"\u002Fvar\u002Frun\u002Fdocker.sock"," → ",[31,566,567],{},"dockerd","（常驻服务，负责接收请求、拉镜像、管网络与卷）→ ",[31,570,571],{},"containerd","（真正的容器运行时管理层，镜像解包、任务生命周期归它）→ ",[31,574,575],{},"runc","（一次性小工具，做完 namespace\u002Fcgroup 那套系统调用把容器进程生出来就退出，不留常驻进程）。",[15,578,579],{},"关键推论有三个：",[581,582,583,601,611],"ol",{},[584,585,586,589,590,593,594,596,597,600],"li",{},[31,587,588],{},"客户端与 daemon 可以完全分离","。设 ",[19,591,592],{},"DOCKER_HOST=tcp:\u002F\u002Fbuild-server:2375","，你的 ",[19,595,559],{}," 命令就在操控远端机器。CI 里的 ",[19,598,599],{},"docker:24-dind"," 镜像就是这么用的。",[584,602,603,606,607,610],{},[31,604,605],{},"daemon 挂了，容器不一定挂","。已运行的容器由 runc 生成的进程继续跑；重启 dockerd 会重新接管它们（这也是为什么 ",[19,608,609],{},"docker restart dockerd"," 比想象中有用，但也可能造成短暂失联）。",[584,612,613,616,617,619],{},[31,614,615],{},"能访问那个 socket 就等于宿主 root","。",[19,618,563],{}," 的权限、挂载它的风险，第十五章会专门算这笔账。",[15,621,622,623,626,627,630,631,634,635,616],{},"安装方式上，Linux 建议用发行版的官方仓库装 ",[19,624,625],{},"docker-ce","（snap 版本会限制挂载路径，坑很多），装完 ",[19,628,629],{},"sudo systemctl enable --now docker","，再用 ",[19,632,633],{},"systemctl status docker --no-pager"," 确认是 ",[19,636,637],{},"active (running)",[40,639,641],{"id":640},"docker-version-与-docker-info先学会读字段","docker version 与 docker info：先学会读字段",[15,643,644,645,648,649,652,653,656,657,660,661,664,665,672],{},"装完先跑这两条，它们是你后面所有排障的信息源。",[19,646,647],{},"docker version"," 输出分 Client 和 Server 两段：",[19,650,651],{},"ApiVersion"," 决定客户端能说什么话，",[19,654,655],{},"Version"," 是引擎版本，",[19,658,659],{},"GitCommit"," 是构建标识（提 issue 时要给），",[19,662,663],{},"Os\u002FArch"," 是这台机器的目标平台——跨架构问题第一个看这里。",[31,666,667,668,671],{},"如果只有 Client 段然后报错 ",[19,669,670],{},"Cannot connect to the Docker daemon","，说明 daemon 没跑或你没权限","，这是第十八章表格的第一行。",[15,674,675,678],{},[19,676,677],{},"docker info"," 要重点读的字段：",[45,680,681,691],{},[48,682,683],{},[51,684,685,688],{},[54,686,687],{},"字段",[54,689,690],{},"看它干什么",[67,692,693,703,721,742,761,775],{},[51,694,695,700],{},[72,696,697],{},[19,698,699],{},"Server Version",[72,701,702],{},"引擎版本，决定可用特性",[51,704,705,710],{},[72,706,707],{},[19,708,709],{},"Storage Driver",[72,711,712,713,716,717,720],{},"几乎总是 ",[19,714,715],{},"overlay2","；",[19,718,719],{},"vfs"," 说明没加速层，又慢又占盘",[51,722,723,732],{},[72,724,725,728,729],{},[19,726,727],{},"Cgroup Driver"," \u002F ",[19,730,731],{},"Cgroup Version",[72,733,734,737,738,741],{},[19,735,736],{},"systemd"," 还是 ",[19,739,740],{},"cgroupfs","、v1 还是 v2——与 k8s 混用时驱动必须一致，v1\u002Fv2 的内存与 swap 语义有差异（第十四章）",[51,743,744,749],{},[72,745,746],{},[19,747,748],{},"Security Options",[72,750,751,114,754,114,757,760],{},[19,752,753],{},"seccomp",[19,755,756],{},"apparmor",[19,758,759],{},"rootless"," 都在这里体现",[51,762,763,768],{},[72,764,765],{},[19,766,767],{},"Docker Root Dir",[72,769,770,771,774],{},"镜像与卷的真实落盘位置，磁盘爆了对着它 ",[19,772,773],{},"du","（第九章）",[51,776,777,788],{},[72,778,779,114,782,114,785],{},[19,780,781],{},"Registry Mirrors",[19,783,784],{},"NCPU",[19,786,787],{},"Total Memory",[72,789,790,791,793,794,796],{},"加速器是否生效；宿主真实上限（",[19,792,394],{},"\u002F",[19,795,530],{}," 是相对它而言）",[40,798,800],{"id":799},"docker-run-hello-world拆开看它做了六件事","docker run hello-world：拆开看它做了六件事",[15,802,803,804,807],{},"跑一条 ",[19,805,806],{},"docker run hello-world","，屏幕上那段 \"Hello from Docker!\" 背后是一条完整链路：",[581,809,810,813,828,835,842,852],{},[584,811,812],{},"客户端把「跑 hello-world」的请求发给 daemon；",[584,814,815,816,819,820,823,824,827],{},"daemon 本地查 ",[19,817,818],{},"docker.io\u002Flibrary\u002Fhello-world:latest"," 的镜像记录，",[31,821,822],{},"没有","，于是向 ",[19,825,826],{},"registry-1.docker.io"," 发认证与拉取；",[584,829,830,831,834],{},"按 manifest 找到匹配 ",[19,832,833],{},"linux\u002Famd64"," 的那一组层，逐层下载并校验 sha256；",[584,836,837,838,841],{},"把层解压到 ",[19,839,840],{},"\u002Fvar\u002Flib\u002Fdocker\u002Foverlay2\u002F","，用联合挂载叠成一个只读根文件系统；",[584,843,844,845,848,849,716],{},"在上面盖一层容器可写层，配上 namespace\u002Fcgroup，让 runc 拉起镜像 ",[19,846,847],{},"CMD"," 指定的 ",[19,850,851],{},"\u002Fhello",[584,853,854,855,858,859,862,863,866,867,870],{},"进程打印一段文本后 ",[19,856,857],{},"exit 0","，容器进入 ",[19,860,861],{},"exited"," 状态——",[19,864,865],{},"docker ps"," 看不见它，",[19,868,869],{},"docker ps -a"," 能看见（第六章）。",[15,872,873,874,877,878,881,882,884,885,888,889,888,892,895],{},"这条链路解释了三个常见困惑：第一次跑慢是",[31,875,876],{},"拉镜像","（不是启动慢）；跑完就没了是",[31,879,880],{},"应用正常退出","（容器活着的时间 = 主进程活着的时间）；",[19,883,555],{}," 其实是 ",[19,886,887],{},"pull"," + ",[19,890,891],{},"create",[19,893,894],{},"start"," 三个动作的合称。",[40,897,899],{"id":898},"sudo-usermod-ag-docker-user为什么要重新登录","sudo usermod -aG docker $USER：为什么要重新登录",[15,901,902,903,906,907,910,911,914,915,918,919,616],{},"把用户加进 docker 组，是为了不用每条命令都 ",[19,904,905],{},"sudo","：",[19,908,909],{},"sudo usermod -aG docker $USER","，然后 ",[19,912,913],{},"newgrp docker"," 或直接退出终端重新登录，用 ",[19,916,917],{},"id"," 确认 groups 里能看到 ",[19,920,559],{},[15,922,923,924,927,928,931,932,935,936,939,940,942,943,616],{},"必须重新登录（或 ",[19,925,926],{},"newgrp","）的原因是：",[31,929,930],{},"组的成员关系是进程凭证的一部分，只在登录那一刻被读进进程","。你改的是 ",[19,933,934],{},"\u002Fetc\u002Fgroup","，而当前这个 shell 进程的凭证是登录时抄的一份快照，不重新登录它不会更新。新手在这里卡住的典型症状是明明执行过 ",[19,937,938],{},"usermod","，",[19,941,865],{}," 依旧报 ",[19,944,945],{},"permission denied while trying to connect to the Docker daemon socket",[15,947,948,951,952,955,956,959,960,963],{},[19,949,950],{},"-aG"," 的 ",[19,953,954],{},"a"," 不能省：",[19,957,958],{},"usermod -G docker $USER","（没有 a）会把用户从其他所有附加组里",[31,961,962],{},"移除","，这是能把机器权限改坏的经典事故。",[15,965,966,967,970,971,973],{},"还要清楚这件事的实质：",[31,968,969],{},"加入 docker 组不是「降权」，而是把宿主 root 能力授给了你","。能起容器就能挂载任意宿主目录，包括 ",[19,972,793],{},"。生产机器上谁该进这个组，要当成 sudoers 来审。",[40,975,977],{"id":976},"rootless-模式把-daemon-也变成普通用户","rootless 模式：把 daemon 也变成普通用户",[15,979,980,981,793,984,987],{},"rootless 模式让 dockerd 自己跑在普通用户下（靠 ",[19,982,983],{},"newuidmap",[19,985,986],{},"newgidmap"," 与 user namespace）。它换来的是「即使容器逃逸，攻击者拿到的也是普通用户」，代价是一串限制：",[45,989,990,1002],{},[48,991,992],{},[51,993,994,997,1000],{},[54,995,996],{},"项目",[54,998,999],{},"rootful",[54,1001,759],{},[67,1003,1004,1018,1031,1046,1061],{},[51,1005,1006,1009,1012],{},[72,1007,1008],{},"绑定 80\u002F443 特权端口",[72,1010,1011],{},"可以",[72,1013,1014,1017],{},[31,1015,1016],{},"不行","（>1024，除非改 sysctl）",[51,1019,1020,1026,1028],{},[72,1021,1022,1025],{},[19,1023,1024],{},"--network=host"," \u002F macvlan",[72,1027,1011],{},[72,1029,1030],{},"受限或不可用",[51,1032,1033,1036,1039],{},[72,1034,1035],{},"bind mount 宿主文件",[72,1037,1038],{},"直接对应",[72,1040,1041,1042,1045],{},"受 subuid 映射，属主看着是 ",[19,1043,1044],{},"100000+","（第九章、第十五章）",[51,1047,1048,1051,1056],{},[72,1049,1050],{},"数据目录",[72,1052,1053],{},[19,1054,1055],{},"\u002Fvar\u002Flib\u002Fdocker",[72,1057,1058],{},[19,1059,1060],{},"~\u002F.local\u002Fshare\u002Fdocker",[51,1062,1063,1066,1069],{},[72,1064,1065],{},"开机自启",[72,1067,1068],{},"systemd 服务",[72,1070,1071,1072,1075],{},"需要用户级 lingering（",[19,1073,1074],{},"loginctl enable-linger","）",[15,1077,1078,1079,888,1082,910,1085,1088],{},"装法是官方脚本 ",[19,1080,1081],{},"rootlesskit",[19,1083,1084],{},"dockerd-rootless-setuptool.sh install",[19,1086,1087],{},"systemctl --user"," 管理。对个人开发机和「只跑自己代码」的构建机很合适；给外部内容做隔离时它确实提高了一档门槛。",[40,1090,1092],{"id":1091},"docker-desktop-与那台看不见的虚拟机","Docker Desktop 与那台看不见的虚拟机",[15,1094,1095,1096,1099,1100,1102],{},"macOS 和 Windows 上你装的是 Docker Desktop，它不是「原生支持容器」，而是",[31,1097,1098],{},"在宿主上偷偷起了一台 Linux 虚拟机","（macOS 用 Apple 虚拟化框架 \u002F HyperKit，Windows 用 WSL2 或 Hyper-V），daemon 和所有容器都跑在那台 VM 里。你敲的 ",[19,1101,559],{}," 命令通过它暴露的 socket 转发过去。",[15,1104,1105],{},"由此产生的三个「为什么」必须提前知道，否则后面会一直被折磨：",[581,1107,1108,1114,1127],{},[584,1109,1110,1113],{},[31,1111,1112],{},"为什么 bind mount 这么慢？"," 容器在 Linux VM 里，而你的代码在 macOS\u002FWindows 文件系统上，每次读写都要跨一次虚拟机文件共享（virtiofs \u002F gRPC-FUSE \u002F WSL2 计划）。stat 密集型操作（npm install、大量小文件）慢一个数量级。解法：把代码放在命名卷里、或用 WSL2 内的文件系统（Windows 侧）、或直接在 VM 里跑构建。",[584,1115,1116,1122,1123,1126],{},[31,1117,1118,1119,1121],{},"为什么 ",[19,1120,1024],{}," 不生效？"," 那个 host 是 VM 的 host，不是你 Mac 的。同理 ",[19,1124,1125],{},"127.0.0.1"," 在容器里也不指向你的浏览器（第八章）。",[584,1128,1129,1132],{},[31,1130,1131],{},"为什么内存把 Mac 吃满？"," VM 会尽量占满分配给它的内存并不归还，需要在 Desktop 设置里给 Docker VM 分配上限（第八章的资源视角）。",[15,1134,1135],{},"Linux 上没有这层虚拟机，性能与语义都是「原生」的——这也是生产环境几乎清一色 Linux 容器的原因。",[15,1137,1138],{},"下一章来看镜像的真实结构：它是一叠只读层，理解这一点之后，Dockerfile 里所有「为什么要按这个顺序写」都会自动说得通。",[10,1140,1142],{"id":1141},"三镜像分层只读层联合挂载与容器可写层","三、镜像分层：只读层、联合挂载与容器可写层",[15,1144,1145,1146,1149],{},"镜像不是一个大文件，而是",[31,1147,1148],{},"一叠只读层","。这一章的目标是让你能从命令输出里亲眼看到这些层——看懂了层，第五章的缓存、第十四章的瘦身、第十五章的密钥泄漏才是「显然」的结论。",[40,1151,1153],{"id":1152},"一叠只读层-一个读写层","一叠只读层 + 一个读写层",[15,1155,1156],{},"启动容器时，Docker 用联合挂载（Linux 上默认 overlay2）把这叠只读层叠成一个根文件系统，再在最上面加一层属于这个容器的可写层：",[1158,1159,1164],"pre",{"className":1160,"code":1161,"language":1162,"meta":1163,"style":1163},"language-bash shiki shiki-themes github-dark","┌────────────────────────────────┐\n│ 容器可写层（每个容器一份）     │ ← 所有写入都发生在这里，docker rm 即消失\n├────────────────────────────────┤\n│ CMD [\"node\", \"server.js\"]   0B │ 元数据层：不占空间，但参与缓存判定\n│ COPY . .                 1.8MB │\n│ RUN npm ci                212MB│\n│ WORKDIR \u002Fsrv\u002Fshop           0B │\n│ FROM node:22-bookworm-slim 78MB│\n└────────────────────────────────┘\n  下面全是只读层：可被多个镜像、多个容器共享\n","bash","",[19,1165,1166,1175,1200,1206,1236,1255,1272,1288,1302,1308],{"__ignoreMap":1163},[1167,1168,1171],"span",{"class":1169,"line":1170},"line",1,[1167,1172,1174],{"class":1173},"svObZ","┌────────────────────────────────┐\n",[1167,1176,1178,1181,1185,1188,1191,1194,1197],{"class":1169,"line":1177},2,[1167,1179,1180],{"class":1173},"│",[1167,1182,1184],{"class":1183},"sU2Wk"," 容器可写层（每个容器一份）",[1167,1186,1187],{"class":1183},"     │",[1167,1189,1190],{"class":1183}," ←",[1167,1192,1193],{"class":1183}," 所有写入都发生在这里，docker",[1167,1195,1196],{"class":1183}," rm",[1167,1198,1199],{"class":1183}," 即消失\n",[1167,1201,1203],{"class":1169,"line":1202},3,[1167,1204,1205],{"class":1173},"├────────────────────────────────┤\n",[1167,1207,1209,1211,1214,1218,1221,1224,1227,1230,1233],{"class":1169,"line":1208},4,[1167,1210,1180],{"class":1173},[1167,1212,1213],{"class":1183}," CMD",[1167,1215,1217],{"class":1216},"s95oV"," [",[1167,1219,1220],{"class":1183},"\"node\"",[1167,1222,1223],{"class":1216},", ",[1167,1225,1226],{"class":1183},"\"server.js\"]",[1167,1228,1229],{"class":1183},"   0B",[1167,1231,1232],{"class":1183}," │",[1167,1234,1235],{"class":1183}," 元数据层：不占空间，但参与缓存判定\n",[1167,1237,1239,1241,1244,1247,1249,1252],{"class":1169,"line":1238},5,[1167,1240,1180],{"class":1173},[1167,1242,1243],{"class":1183}," COPY",[1167,1245,1246],{"class":1183}," .",[1167,1248,1246],{"class":1183},[1167,1250,1251],{"class":1183},"                 1.8MB",[1167,1253,1254],{"class":1183}," │\n",[1167,1256,1258,1260,1263,1266,1269],{"class":1169,"line":1257},6,[1167,1259,1180],{"class":1173},[1167,1261,1262],{"class":1183}," RUN",[1167,1264,1265],{"class":1183}," npm",[1167,1267,1268],{"class":1183}," ci",[1167,1270,1271],{"class":1183},"                212MB│\n",[1167,1273,1275,1277,1280,1283,1286],{"class":1169,"line":1274},7,[1167,1276,1180],{"class":1173},[1167,1278,1279],{"class":1183}," WORKDIR",[1167,1281,1282],{"class":1183}," \u002Fsrv\u002Fshop",[1167,1284,1285],{"class":1183},"           0B",[1167,1287,1254],{"class":1183},[1167,1289,1291,1293,1296,1299],{"class":1169,"line":1290},8,[1167,1292,1180],{"class":1173},[1167,1294,1295],{"class":1183}," FROM",[1167,1297,1298],{"class":1183}," node:22-bookworm-slim",[1167,1300,1301],{"class":1183}," 78MB│\n",[1167,1303,1305],{"class":1169,"line":1304},9,[1167,1306,1307],{"class":1173},"└────────────────────────────────┘\n",[1167,1309,1311],{"class":1169,"line":1310},10,[1167,1312,1313],{"class":1173},"  下面全是只读层：可被多个镜像、多个容器共享\n",[15,1315,1316],{},"三个直接后果：",[1318,1319,1320,1326,1344],"ul",{},[584,1321,1322,1325],{},[31,1323,1324],{},"共享","。跑 20 个基于同一镜像的容器，磁盘上只有一份镜像层，20 个可写层而已。这就是容器密度高、镜像「下载快」的物理原因（很多层早就在本地了，只拉缺的）。",[584,1327,1328,1331,1332,1335,1336,1339,1340,1343],{},[31,1329,1330],{},"写时复制（copy-up）","。你要改下层的 ",[19,1333,1334],{},"app.conf","，文件系统先把整份文件复制到可写层，你改的是副本；上层「遮住」下层。删文件同理，是在可写层放一个 whiteout 标记，",[31,1337,1338],{},"下层那个字节还在","——所以在 Dockerfile 里 ",[19,1341,1342],{},"RUN rm bigfile"," 不会让镜像变小，必须和产生它的指令合并在同一层（第十四章瘦身核心）。",[584,1345,1346,616,1349,1351],{},[31,1347,1348],{},"容器可写层是易逝的",[19,1350,151],{}," 就没了。数据库要活着必须走卷（第九章）。",[40,1353,1355],{"id":1354},"docker-history把层和-dockerfile-指令对上","docker history：把层和 Dockerfile 指令对上",[15,1357,1358,1361,1362,1365,1366,1368,1369,1372,1373,114,1375,114,1378,114,1381,114,1384,114,1387,114,1390,114,1393,1396],{},[19,1359,1360],{},"docker history ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --no-trunc"," 的输出里每条指令对应一层，",[19,1363,1364],{},"SIZE"," 是该层新增的字节数。你会看到大量 ",[19,1367,1364],{}," 为 0 的层——它们是",[31,1370,1371],{},"元数据层","（",[19,1374,847],{},[19,1376,1377],{},"ENTRYPOINT",[19,1379,1380],{},"ENV",[19,1382,1383],{},"EXPOSE",[19,1385,1386],{},"WORKDIR",[19,1388,1389],{},"USER",[19,1391,1392],{},"LABEL",[19,1394,1395],{},"VOLUME","），只改配置不写文件，不占空间但仍然算一层、仍然参与缓存判定。这个区分在第四章讲指令顺序时是关键。",[15,1398,1399,1400,1403],{},"示例 03 把这几条真实输出摆在一起，可以逐字段点开看：层是从下往上叠加的，",[19,1401,1402],{},"history"," 是从上往下打印的，方向感要建起来。",[1405,1406],"demo-html",{"height":1407,"src":1408,"title":1409},560,"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F03-image-layers.html","示例 03：分层观察——images \u002F history \u002F inspect \u002F system df",[15,1411,1412,1413,1416,1417,1420],{},"另外两个观察入口是 ",[19,1414,1415],{},"docker image inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --format '{{json .RootFS.Layers}}'","（层的内容哈希数组，从上到下即从最底层到最上层）和 ",[19,1418,1419],{},"docker system df","（给四行账：Images、Containers、Local Volumes、Build Cache，以及各自的「可回收」比例——第十四章讨论清理策略时，它就是你每次决策前该看的那张表）。",[40,1422,1424],{"id":1423},"改一行代码为什么要重建内容哈希决定失效点","改一行代码为什么要重建：内容哈希决定失效点",[15,1426,1427],{},"构建时每一层的 ID 由「父层 ID + 本条指令 + 本指令涉及的输入内容（对 COPY\u002FADD 是文件内容哈希）」算出来。因此：",[1158,1429,1433],{"className":1430,"code":1431,"language":1432,"meta":1163,"style":1163},"language-dockerfile shiki shiki-themes github-dark","FROM node:22-bookworm-slim           # 层 1\nWORKDIR \u002Fsrv\u002Fshop                    # 层 2（元数据）\nCOPY package.json package-lock.json .\u002F  # 层 3：只依赖这两个文件\nRUN npm ci                           # 层 4：npm 只依赖上一份 package-lock\nCOPY . .                             # 层 5：整个源码目录 —— 改任何一行源码，从这层起重跑\nENV NODE_ENV=production              # 层 6（元数据）\nEXPOSE 3000                          # 层 7（元数据）\nCMD [\"node\", \"server.js\"]            # 层 8（元数据）\n","dockerfile",[19,1434,1435,1440,1445,1450,1455,1460,1465,1470],{"__ignoreMap":1163},[1167,1436,1437],{"class":1169,"line":1170},[1167,1438,1439],{},"FROM node:22-bookworm-slim           # 层 1\n",[1167,1441,1442],{"class":1169,"line":1177},[1167,1443,1444],{},"WORKDIR \u002Fsrv\u002Fshop                    # 层 2（元数据）\n",[1167,1446,1447],{"class":1169,"line":1202},[1167,1448,1449],{},"COPY package.json package-lock.json .\u002F  # 层 3：只依赖这两个文件\n",[1167,1451,1452],{"class":1169,"line":1208},[1167,1453,1454],{},"RUN npm ci                           # 层 4：npm 只依赖上一份 package-lock\n",[1167,1456,1457],{"class":1169,"line":1238},[1167,1458,1459],{},"COPY . .                             # 层 5：整个源码目录 —— 改任何一行源码，从这层起重跑\n",[1167,1461,1462],{"class":1169,"line":1257},[1167,1463,1464],{},"ENV NODE_ENV=production              # 层 6（元数据）\n",[1167,1466,1467],{"class":1169,"line":1274},[1167,1468,1469],{},"EXPOSE 3000                          # 层 7（元数据）\n",[1167,1471,1472],{"class":1169,"line":1290},[1167,1473,1474],{},"CMD [\"node\", \"server.js\"]            # 层 8（元数据）\n",[15,1476,1477,1478,1495,1496,1498],{},"改一行源码 → 层 1–4 命中缓存，层 5 起重建。",[31,1479,1480,1481,1484,1485,1488,1489,1491,1492],{},"但如果你把 ",[19,1482,1483],{},"COPY . ."," 写在 ",[19,1486,1487],{},"COPY package.json"," 前面，或者干脆第一行就 ",[19,1490,1483],{},"，那么改一行代码就要重新 ",[19,1493,1494],{},"npm ci","（因为 ",[19,1497,1494],{}," 那层的父层哈希变了）——一个几秒的改动变成几分钟的构建。这一条顺序调整，是 Dockerfile 性能优化里性价比最高的一件事，没有之一。",[40,1500,1502],{"id":1501},"镜像就是一个-tar-的集合","镜像就是一个 tar 的集合",[15,1504,1505,1506,1509,1510,1513,1514,1517,1518,114,1521,114,1524,1527,1528,1531],{},"镜像在磁盘上是散的目录，但在网络上和 ",[19,1507,1508],{},"docker save"," 的结果里，它是一组 tar。执行 ",[19,1511,1512],{},"docker save -o shop-api-1.8.3.tar ghcr.io\u002Fexample\u002Fshop-api:1.8.3 && tar tf shop-api-1.8.3.tar | head -8","，你会看到 ",[19,1515,1516],{},"blobs\u002Fsha256\u002F..."," 一堆层文件加一个 ",[19,1519,1520],{},"manifest.json",[19,1522,1523],{},"index.json",[19,1525,1526],{},"oci-layout","。也就是说：",[31,1529,1530],{},"「镜像」本质上是一份带元数据的文件系统 tar 包集合","，OCI 规范只是把这格式标准化了。这带来两个实用能力：",[1318,1533,1534,1545],{},[584,1535,1536,728,1538,906,1541,1544],{},[19,1537,1508],{},[19,1539,1540],{},"docker load",[31,1542,1543],{},"离线分发","。内网机器没外网、气隔环境、应急回滚包，都靠它（第十章细讲用法与风险）。",[584,1546,1547,1548,1551],{},"层可以被任何兼容 OCI 的工具消费：Podman、containerd、Kubernetes 直接用 ",[19,1549,1550],{},"docker build"," 出来的镜像（第十七章讲 dockershim 移除时这条是重点）。",[15,1553,1554],{},"对应命令组：",[45,1556,1557,1570],{},[48,1558,1559],{},[51,1560,1561,1564,1567],{},[54,1562,1563],{},"命令",[54,1565,1566],{},"作用",[54,1568,1569],{},"注意",[67,1571,1572,1587,1603,1627,1644,1656],{},[51,1573,1574,1579,1582],{},[72,1575,1576],{},[19,1577,1578],{},"docker images",[72,1580,1581],{},"本地镜像列表",[72,1583,1584,1586],{},[19,1585,1364],{}," 是解压后共享大小，不等于它占的磁盘",[51,1588,1589,1594,1597],{},[72,1590,1591],{},[19,1592,1593],{},"docker history",[72,1595,1596],{},"层与指令对应表",[72,1598,1599,1602],{},[19,1600,1601],{},"--no-trunc"," 才看得到完整指令，也是查密钥泄漏的第一现场",[51,1604,1605,1610,1613],{},[72,1606,1607],{},[19,1608,1609],{},"docker image inspect",[72,1611,1612],{},"完整元数据 JSON",[72,1614,1615,114,1618,114,1621,114,1624],{},[19,1616,1617],{},"RootFS.Layers",[19,1619,1620],{},"Config.Env",[19,1622,1623],{},"RepoDigests",[19,1625,1626],{},"Architecture",[51,1628,1629,1634,1637],{},[72,1630,1631],{},[19,1632,1633],{},"docker image ls -q",[72,1635,1636],{},"只要 ID",[72,1638,1639,1640,1643],{},"配合 ",[19,1641,1642],{},"docker rmi $(...)"," 批量删",[51,1645,1646,1650,1653],{},[72,1647,1648],{},[19,1649,1419],{},[72,1651,1652],{},"磁盘账本",[72,1654,1655],{},"清理前先跑它，别反过来",[51,1657,1658,1664,1667],{},[72,1659,1660,728,1662],{},[19,1661,1508],{},[19,1663,1540],{},[72,1665,1666],{},"导出 \u002F 导入 tar",[72,1668,1669],{},"不跨架构；tar 体积按未压缩算",[15,1671,1672,1673,1675],{},"这一章的收获应该是：你从此看 ",[19,1674,1550],{}," 的输出不再是「它在忙」，而是「它重跑到第几层、为什么这层没命中」。下一章把 Dockerfile 的每一条指令讲透，那正是控制分层的唯一手段。",[10,1677,1679],{"id":1678},"四dockerfile-逐条讲透指令语义四种组合与元数据层","四、Dockerfile 逐条讲透：指令语义、四种组合与元数据层",[15,1681,1682,1683,1686],{},"Dockerfile 是镜像的源代码，也是本教程密度最高的一章。逐条讲不是因为记不住就没法用，而是因为",[31,1684,1685],{},"每条指令都有「看起来对、跑起来炸」的陷阱","，而陷阱基本都藏在默认行为里。",[40,1688,1690],{"id":1689},"from起点也是体积与漏洞的主要来源","FROM：起点，也是体积与漏洞的主要来源",[1158,1692,1694],{"className":1430,"code":1693,"language":1432,"meta":1163,"style":1163},"FROM node:22-bookworm-slim AS builder     # 带别名，供多阶段构建引用\nFROM golang:1.23-alpine\nFROM scratch                             # 空镜像：只有你显式复制进去的东西\nFROM ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0e... # 标签 + digest 双锁\n",[19,1695,1696,1701,1706,1711],{"__ignoreMap":1163},[1167,1697,1698],{"class":1169,"line":1170},[1167,1699,1700],{},"FROM node:22-bookworm-slim AS builder     # 带别名，供多阶段构建引用\n",[1167,1702,1703],{"class":1169,"line":1177},[1167,1704,1705],{},"FROM golang:1.23-alpine\n",[1167,1707,1708],{"class":1169,"line":1202},[1167,1709,1710],{},"FROM scratch                             # 空镜像：只有你显式复制进去的东西\n",[1167,1712,1713],{"class":1169,"line":1208},[1167,1714,1715],{},"FROM ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0e... # 标签 + digest 双锁\n",[15,1717,1718],{},"要点：",[1318,1720,1721,1733,1745],{},[584,1722,1723,1729,1730,1732],{},[31,1724,1725,1726],{},"一个 Dockerfile 可以有多个 ",[19,1727,1728],{},"FROM","，这就是多阶段构建的骨架（第五章）；最终镜像只包含最后一个 ",[19,1731,1728],{}," 之后的内容。",[584,1734,1735,1737,1738,793,1741,1744],{},[19,1736,1728],{}," 决定了一切上游：glibc 还是 musl、有没有 ",[19,1739,1740],{},"apt",[19,1742,1743],{},"apk","、默认用户是不是 root、有多少 CVE 等着被打。基础镜像选型在第十五章展开。",[584,1746,1747,1748,1751],{},"版本要写具体。",[19,1749,1750],{},"FROM node:latest"," 意味着你的构建今天绿、下个月不知道因为什么红。生产用「大版本 tag + digest」。",[40,1753,1755],{"id":1754},"copy-vs-add默认选-copy没有例外","COPY vs ADD：默认选 COPY，没有例外",[45,1757,1758,1774],{},[48,1759,1760],{},[51,1761,1762,1764,1769],{},[54,1763],{},[54,1765,1766],{},[19,1767,1768],{},"COPY",[54,1770,1771],{},[19,1772,1773],{},"ADD",[67,1775,1776,1786,1803,1813],{},[51,1777,1778,1781,1784],{},[72,1779,1780],{},"复制本地文件到镜像",[72,1782,1783],{},"✓",[72,1785,1783],{},[51,1787,1788,1794,1797],{},[72,1789,1790,1791,1075],{},"源是 URL（",[19,1792,1793],{},"ADD http:\u002F\u002F…",[72,1795,1796],{},"✗",[72,1798,1799,1800,1802],{},"✓（但",[31,1801,409],{},"为它设计的功能）",[51,1804,1805,1808,1810],{},[72,1806,1807],{},"源是 tar 自动解压",[72,1809,1796],{},[72,1811,1812],{},"✓（隐式行为，容易踩）",[51,1814,1815,1824,1826],{},[72,1816,1817,1818,728,1821],{},"支持 ",[19,1819,1820],{},"--chown",[19,1822,1823],{},"--chmod",[72,1825,1783],{},[72,1827,1783],{},[15,1829,1830,1831,1833,1834,1843,1844,1850,1851,1854],{},"官方文档自己就写着「通常用 COPY」。",[19,1832,1773],{}," 那两个额外能力都有更好的替代品：",[31,1835,1836,1837,888,1840],{},"下载用 ",[19,1838,1839],{},"RUN curl",[19,1841,1842],{},"RUN rm","（能校验、能清理、能合进一层），",[31,1845,1846,1847],{},"解压用 ",[19,1848,1849],{},"RUN tar -xzf","。一个细节值得记：",[19,1852,1853],{},"ADD \u003Ctar-url>"," 不解压（网络源不触发自动解压），这种不一致正是它该被避开的原因。",[15,1856,1857,1859,1860,1863,1864,1866,1867,1869,1870,939,1875,1878,1879,1882,1883,1885],{},[19,1858,1768],{}," 的两个易错点：",[19,1861,1862],{},"COPY --chown=app:app --chmod=0644 . \u002Fsrv\u002Fshop\u002F"," 避免整棵目录被 root 占有；目标以 ",[19,1865,793],{}," 结尾是「拷进目录」，不以 ",[19,1868,793],{}," 结尾且源是单个文件则是「重命名」。另外 ",[31,1871,1872,1874],{},[19,1873,1768],{}," 只能看见构建上下文",[19,1876,1877],{},"COPY ..\u002Fx"," 一律失败；",[19,1880,1881],{},".dockerignore"," 会同时影响 ",[19,1884,1483],{}," 的内容与缓存哈希（第五章）。",[40,1887,1889],{"id":1888},"runshell-形式与-exec-形式以及-pid-1-的伏笔","RUN：shell 形式与 exec 形式，以及 PID 1 的伏笔",[1158,1891,1893],{"className":1430,"code":1892,"language":1432,"meta":1163,"style":1163},"RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \\\n    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*          # 三层合一：一条 RUN 一个层，必须链起来\nSHELL [\"\u002Fbin\u002Fbash\", \"-o\", \"pipefail\", \"-c\"] # 改默认 shell（见本章 SHELL）\nRUN set -o pipefail && make 2>&1 | tee build.log\n",[19,1894,1895,1900,1905,1910],{"__ignoreMap":1163},[1167,1896,1897],{"class":1169,"line":1170},[1167,1898,1899],{},"RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \\\n",[1167,1901,1902],{"class":1169,"line":1177},[1167,1903,1904],{},"    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*          # 三层合一：一条 RUN 一个层，必须链起来\n",[1167,1906,1907],{"class":1169,"line":1202},[1167,1908,1909],{},"SHELL [\"\u002Fbin\u002Fbash\", \"-o\", \"pipefail\", \"-c\"] # 改默认 shell（见本章 SHELL）\n",[1167,1911,1912],{"class":1169,"line":1208},[1167,1913,1914],{},"RUN set -o pipefail && make 2>&1 | tee build.log\n",[1318,1916,1917,1934,1943],{},[584,1918,1919,1926,1927,1930,1931,1933],{},[31,1920,1921,1922,1925],{},"每多一条 ",[19,1923,1924],{},"RUN"," 就多一层","，而层里的东西删不掉。所以「装包 → 清理」必须用 ",[19,1928,1929],{},"&&"," 写在同一条 ",[19,1932,1924],{}," 里；分开写等于用下一层去遮蔽，磁盘上原文件还在。",[584,1935,1936,1942],{},[31,1937,1938,1941],{},[19,1939,1940],{},"apt-get update"," 单独成层会长期缓存","，之后装到的永远是那次快照的旧包；和 install 同层才能一起失效。",[584,1944,1945,1946,1948,1949,906,1952,1955,1956,1959,1960,1962],{},"每条 ",[19,1947,1924],{}," 都是全新 shell 进程，",[31,1950,1951],{},"互不继承状态",[19,1953,1954],{},"RUN cd \u002Ftmp"," 下一层回到工作目录，",[19,1957,1958],{},"RUN export FOO=1"," 下一层消失（要跨指令请用 ",[19,1961,1380],{},"）。",[15,1964,1965,1966,1969,1970,1973,1974,1977,1978,1980,1981,1984],{},"形式有两种，差别很重要：",[19,1967,1968],{},"RUN npm ci","（shell 形式，实际执行 ",[19,1971,1972],{},"\u002Fbin\u002Fsh -c \"npm ci\"","）与 ",[19,1975,1976],{},"RUN [\"npm\", \"ci\"]","（exec 形式，没有 shell）。shell 形式能用管道、",[19,1979,1929],{},"、通配符、变量展开，代价是",[31,1982,1983],{},"你真正想跑的程序不是 PID 1","——这直接决定信号能不能收到（第十六章的优雅停机整章在讲这个坑）。exec 形式里 node 就是 PID 1。",[40,1986,1988],{"id":1987},"cmd-vs-entrypoint四种组合必须一次讲清","CMD vs ENTRYPOINT：四种组合必须一次讲清",[15,1990,1991,1992,616],{},"这两个概念之所以绕，是因为它们一个管「主命令的参数」，一个管「主命令本身」，而且",[31,1993,1994],{},"只有最后一层生效",[45,1996,1997,2013],{},[48,1998,1999],{},[51,2000,2001,2004,2007],{},[54,2002,2003],{},"Dockerfile 里写了",[54,2005,2006],{},"容器启动时执行",[54,2008,2009,2012],{},[19,2010,2011],{},"docker run img python -m http.server"," 时执行",[67,2014,2015,2036,2058,2076],{},[51,2016,2017,2023,2028],{},[72,2018,2019,2022],{},[19,2020,2021],{},"CMD [\"node\",\"app.js\"]","，无 ENTRYPOINT",[72,2024,2025],{},[19,2026,2027],{},"node app.js",[72,2029,2030,1372,2033,1075],{},[19,2031,2032],{},"python -m http.server",[31,2034,2035],{},"整条被替换",[51,2037,2038,2044,2049],{},[72,2039,2040,2043],{},[19,2041,2042],{},"ENTRYPOINT [\"node\"]","，无 CMD",[72,2045,2046],{},[19,2047,2048],{},"node",[72,2050,2051,2054,2055,1075],{},[19,2052,2053],{},"node python -m http.server","（参数",[31,2056,2057],{},"追加",[51,2059,2060,2067,2071],{},[72,2061,2062,888,2064],{},[19,2063,2042],{},[19,2065,2066],{},"CMD [\"app.js\"]",[72,2068,2069],{},[19,2070,2027],{},[72,2072,2073,2075],{},[19,2074,2053],{},"（只替换参数——最常用的组合）",[51,2077,2078,2085,2088],{},[72,2079,2080,888,2083],{},[19,2081,2082],{},"ENTRYPOINT [\"sh\",\"-c\",\"node $FLAG app.js\"]",[19,2084,847],{},[72,2086,2087],{},"shell 解释",[72,2089,2090,2091,939,2097,2099],{},"shell 解释，",[31,2092,2093,2094],{},"追加的参数变成 ",[19,2095,2096],{},"$0 $1",[19,2098,847],{}," 被忽略",[15,2101,2102],{},"实践结论：",[1318,2104,2105,2120,2139,2158,2174],{},[584,2106,2107,906,2110,2112,2113,2115,2116,2119],{},[31,2108,2109],{},"一个纯服务型镜像",[19,2111,1377],{}," 定命令、",[19,2114,847],{}," 定默认参数。这样使用者可以用 ",[19,2117,2118],{},"docker run img --help"," 换参数，而不是被迫重写整个命令。",[584,2121,2122,1372,2125,114,2128,2131,2132,2135,2136,2138],{},[31,2123,2124],{},"一个工具型镜像",[19,2126,2127],{},"ubuntu",[19,2129,2130],{},"alpine","）：只写 ",[19,2133,2134],{},"CMD [\"bash\"]","，因为用户就是要随便跑点什么，",[19,2137,1377],{}," 会碍事。",[584,2140,2141,906,2154,2157],{},[31,2142,2143,2145,2146,939,2148,2151,2152],{},[19,2144,555],{}," 后面的参数永远覆盖 ",[19,2147,847],{},[19,2149,2150],{},"--entrypoint"," 永远覆盖 ",[19,2153,1377],{},[19,2155,2156],{},"docker run --rm --entrypoint sh ghcr.io\u002Fexample\u002Fshop-api:1.8.3 -c \"node -v\""," 是第七章调试的起手式。",[584,2159,2160,2162,2163,2165,2166,793,2168,2170,2171,616],{},[31,2161,1994],{},"：Dockerfile 里写两条 ",[19,2164,847],{},"，第一条静默失效；继承自基础镜像的 ",[19,2167,847],{},[19,2169,1377],{}," 也会被你覆盖。想确认最终值：",[19,2172,2173],{},"docker image inspect img --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'",[584,2175,2176,2183],{},[31,2177,2178,793,2180,2182],{},[19,2179,1377],{},[19,2181,847],{}," 是元数据层，不占空间","，把它们写在最后能最大化缓存命中。",[40,2185,2187],{"id":2186},"env-arg构建期与运行期以及它们都会留在历史里","ENV \u002F ARG：构建期与运行期，以及它们都会留在历史里",[45,2189,2190,2206],{},[48,2191,2192],{},[51,2193,2194,2197,2200,2203],{},[54,2195,2196],{},"指令",[54,2198,2199],{},"何时可见",[54,2201,2202],{},"是否留在最终镜像",[54,2204,2205],{},"典型用途",[67,2207,2208,2231],{},[51,2209,2210,2215,2222,2228],{},[72,2211,2212],{},[19,2213,2214],{},"ARG",[72,2216,2217,2218,2221],{},"仅构建期（",[19,2219,2220],{},"--build-arg"," 传入）",[72,2223,2224,2225,1075],{},"否（值不留，但",[31,2226,2227],{},"指令文本留",[72,2229,2230],{},"版本号、代理、构建目标",[51,2232,2233,2237,2240,2245],{},[72,2234,2235],{},[19,2236,1380],{},[72,2238,2239],{},"构建期 + 运行期",[72,2241,2242,2243,1075],{},"是（",[19,2244,1620],{},[72,2246,2247,114,2250,114,2253],{},[19,2248,2249],{},"NODE_ENV",[19,2251,2252],{},"PATH",[19,2254,2255],{},"TZ",[1158,2257,2259],{"className":1430,"code":2258,"language":1432,"meta":1163,"style":1163},"ARG NODE_VERSION=22.11.0        # 有默认值；ARG 只在声明之后的指令生效\nFROM node:${NODE_VERSION}-bookworm-slim   # FROM 之前的 ARG 是「全局作用域」，不进任何层\nENV NODE_ENV=production\n",[19,2260,2261,2266,2271],{"__ignoreMap":1163},[1167,2262,2263],{"class":1169,"line":1170},[1167,2264,2265],{},"ARG NODE_VERSION=22.11.0        # 有默认值；ARG 只在声明之后的指令生效\n",[1167,2267,2268],{"class":1169,"line":1177},[1167,2269,2270],{},"FROM node:${NODE_VERSION}-bookworm-slim   # FROM 之前的 ARG 是「全局作用域」，不进任何层\n",[1167,2272,2273],{"class":1169,"line":1202},[1167,2274,2275],{},"ENV NODE_ENV=production\n",[15,2277,2278],{},"两条铁律：",[581,2280,2281,2296],{},[584,2282,2283,2291,2292,2295],{},[31,2284,2285,2287,2288,2290],{},[19,2286,2214],{}," 不产生数据层，但 ",[19,2289,1593],{}," 里能看到那行指令文本","，所以 ",[19,2293,2294],{},"--build-arg PASSWORD=..."," 依然是泄漏。",[584,2297,2298,906,2307,2310,2311,2314,2315,2318],{},[31,2299,2300,793,2302,793,2304,2306],{},[19,2301,1380],{},[19,2303,2214],{},[19,2305,1924],{}," 都禁止放密钥",[19,2308,2309],{},"RUN curl -H \"token: $SECRET\""," 里字面量写死的 token，会原样出现在层历史与构建日志里（第十五章会演示用 ",[19,2312,2313],{},"docker history --no-trunc"," 把它捞出来）。密钥走 ",[19,2316,2317],{},"--mount=type=secret"," 或运行时注入。",[15,2320,2321,2323,2324,906,2327,2330,2331,2334],{},[19,2322,1380],{}," 的另一个用途是",[31,2325,2326],{},"运行期可覆盖的默认值",[19,2328,2329],{},"ENV TZ=Asia\u002FShanghai"," 进镜像，但 ",[19,2332,2333],{},"docker run -e TZ=UTC"," 仍能盖掉它——这是第十三章「配置优先级」的第一层。",[40,2336,2338],{"id":2337},"workdiruservolumeexposelabelhealthcheckshell","WORKDIR、USER、VOLUME、EXPOSE、LABEL、HEALTHCHECK、SHELL",[1158,2340,2342],{"className":1430,"code":2341,"language":1432,"meta":1163,"style":1163},"WORKDIR \u002Fsrv\u002Fshop      # 比 RUN cd 好：后续 RUN\u002FCOPY\u002FADD 的相对路径基准，且是元数据层\nUSER 1001:1001         # 之后的 RUN 与容器运行都以该身份（rootless 与第十五章相关）\nVOLUME \u002Fvar\u002Flib\u002Fmysql  # 声明「这里该有独立存储」，构建时不生效、运行时无名卷兜底\nEXPOSE 3000            # 纯文档：不发布端口、不写 iptables，只是给人和 tool 看的\nLABEL org.opencontainers.image.source=\"https:\u002F\u002Fgithub.com\u002Fexample\u002Fshop\"  # 治理用\nHEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD node healthcheck.js\nSHELL [\"\u002Fbin\u002Fbash\", \"-eo\", \"pipefail\", \"-c\"]   # 改默认 shell，让管道失败可见\n",[19,2343,2344,2349,2354,2359,2364,2369,2374],{"__ignoreMap":1163},[1167,2345,2346],{"class":1169,"line":1170},[1167,2347,2348],{},"WORKDIR \u002Fsrv\u002Fshop      # 比 RUN cd 好：后续 RUN\u002FCOPY\u002FADD 的相对路径基准，且是元数据层\n",[1167,2350,2351],{"class":1169,"line":1177},[1167,2352,2353],{},"USER 1001:1001         # 之后的 RUN 与容器运行都以该身份（rootless 与第十五章相关）\n",[1167,2355,2356],{"class":1169,"line":1202},[1167,2357,2358],{},"VOLUME \u002Fvar\u002Flib\u002Fmysql  # 声明「这里该有独立存储」，构建时不生效、运行时无名卷兜底\n",[1167,2360,2361],{"class":1169,"line":1208},[1167,2362,2363],{},"EXPOSE 3000            # 纯文档：不发布端口、不写 iptables，只是给人和 tool 看的\n",[1167,2365,2366],{"class":1169,"line":1238},[1167,2367,2368],{},"LABEL org.opencontainers.image.source=\"https:\u002F\u002Fgithub.com\u002Fexample\u002Fshop\"  # 治理用\n",[1167,2370,2371],{"class":1169,"line":1257},[1167,2372,2373],{},"HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD node healthcheck.js\n",[1167,2375,2376],{"class":1169,"line":1274},[1167,2377,2378],{},"SHELL [\"\u002Fbin\u002Fbash\", \"-eo\", \"pipefail\", \"-c\"]   # 改默认 shell，让管道失败可见\n",[15,2380,2381],{},"逐条说清「为什么这么设计」和「不这么做会怎样」：",[1318,2383,2384,2404,2427,2452,2480,2496,2513],{},[584,2385,2386,906,2394,2397,2398,2400,2401,616],{},[31,2387,2388,2390,2391],{},[19,2389,1386],{}," 而不是 ",[19,2392,2393],{},"RUN cd",[19,2395,2396],{},"RUN cd \u002Fx && make"," 那条指令内确实生效，但下一层又回到原地；而 ",[19,2399,1386],{}," 会自动创建目录、跨指令生效、且只是元数据（不新增大小）。用绝对路径，别用 ",[19,2402,2403],{},"~",[584,2405,2406,2411,2412,2415,2416,2418,2419,793,2422,2424,2425,616],{},[31,2407,2408,2410],{},[19,2409,1389],{}," 放在能放的位置","：如果 ",[19,2413,2414],{},"USER app"," 之后的 ",[19,2417,1924],{}," 需要往系统目录写，构建就会失败。正确姿势是 root 完成安装、",[19,2420,2421],{},"chown",[19,2423,1820],{}," 好、最后 ",[19,2426,1389],{},[584,2428,2429,2434,2435,2437,2438,2440,2441,2444,2445,2451],{},[31,2430,2431,2433],{},[19,2432,1395],{}," 有副作用","：它保证挂载点里不会被 ",[19,2436,1924],{}," 写入的内容进入镜像（构建时对该目录的写会被丢弃），也让 ",[19,2439,555],{}," 忘了挂卷时悄悄建一个匿名卷（第九章讲怎么认出这种「幽灵数据」）。数据库镜像里 ",[19,2442,2443],{},"VOLUME \u002Fvar\u002Flib\u002Fmysql"," 就是这个意思：那目录里的数据不该属于镜像。",[31,2446,2447,2448,2450],{},"也正因为 ",[19,2449,1395],{}," 会丢写","，如果你想在镜像里预置模板数据，得换个路径。",[584,2453,2454,906,2459,2462,2463,2465,2466,2468,2469,1372,2472,2475,2476,2479],{},[31,2455,2456,2458],{},[19,2457,1383],{}," 只是文档",[19,2460,2461],{},"docker run -p"," 才是真的开端口映射；",[19,2464,1383],{}," 的作用是让 ",[19,2467,865],{}," 显示、让 ",[19,2470,2471],{},"--publish-all",[19,2473,2474],{},"-P","）有东西可发布、让 Compose 的 ",[19,2477,2478],{},"expose"," 语义有依据。不写它不会连不上，写错它也不会连不上——很多人被这点反复误导。",[584,2481,2482,2487,2488,2491,2492,2495],{},[31,2483,2484,2486],{},[19,2485,1392],{}," 用于治理","：镜像多了以后，「这是谁建的、源码在哪、什么构建时间」全靠 OCI 标准标签（",[19,2489,2490],{},"org.opencontainers.image.*","）。别把动态值（如 ",[19,2493,2494],{},"git rev","）放进第一层，否则每次构建所有层全失效。",[584,2497,2498,2504,2505,2508,2509,2512],{},[31,2499,2500,2503],{},[19,2501,2502],{},"HEALTHCHECK"," 是「自证活着」的声明","：没有它，Docker 只知道进程在不在，不知道服务能不能用。它是第十六章就绪判断和 ",[19,2506,2507],{},"depends_on: service_healthy"," 的基础。注意：",[31,2510,2511],{},"unhealthy 不会自动重启容器","，得配合重启策略或外部 watcher。",[584,2514,2515,906,2521,2524,2525,2528,2529,616],{},[31,2516,2517,2520],{},[19,2518,2519],{},"SHELL"," 改默认 shell",[19,2522,2523],{},"\u002Fbin\u002Fsh"," 各发行版行为不同（dash vs bash），管道失败与否、",[19,2526,2527],{},"set -e"," 支持都不同。想写严谨的构建脚本就显式切 bash 并开 ",[19,2530,2531],{},"pipefail",[40,2533,2535],{"id":2534},"指令顺序缓存的分水岭","指令顺序：缓存的分水岭",[15,2537,2538,2539,2542],{},"把上面的知识合起来就是一条排序原则——",[31,2540,2541],{},"变化频率从低到高","。下面这个左右对比能背下来：",[1158,2544,2546],{"className":1430,"code":2545,"language":1432,"meta":1163,"style":1163},"FROM node:22-bookworm-slim                # 很少变\nWORKDIR \u002Fsrv\u002Fshop                          # 很少变\nCOPY package.json package-lock.json .\u002F     # 依赖变更时才变\nRUN npm ci --omit=dev                       # 跟着依赖锁变\nCOPY . .                                    # 每次都变 ← 分水岭\nENV NODE_ENV=production                     # 元数据\nEXPOSE 3000                                 # 元数据\nCMD [\"node\", \"server.js\"]                   # 元数据，永远放最后\n",[19,2547,2548,2553,2558,2563,2568,2573,2578,2583],{"__ignoreMap":1163},[1167,2549,2550],{"class":1169,"line":1170},[1167,2551,2552],{},"FROM node:22-bookworm-slim                # 很少变\n",[1167,2554,2555],{"class":1169,"line":1177},[1167,2556,2557],{},"WORKDIR \u002Fsrv\u002Fshop                          # 很少变\n",[1167,2559,2560],{"class":1169,"line":1202},[1167,2561,2562],{},"COPY package.json package-lock.json .\u002F     # 依赖变更时才变\n",[1167,2564,2565],{"class":1169,"line":1208},[1167,2566,2567],{},"RUN npm ci --omit=dev                       # 跟着依赖锁变\n",[1167,2569,2570],{"class":1169,"line":1238},[1167,2571,2572],{},"COPY . .                                    # 每次都变 ← 分水岭\n",[1167,2574,2575],{"class":1169,"line":1257},[1167,2576,2577],{},"ENV NODE_ENV=production                     # 元数据\n",[1167,2579,2580],{"class":1169,"line":1274},[1167,2581,2582],{},"EXPOSE 3000                                 # 元数据\n",[1167,2584,2585],{"class":1169,"line":1290},[1167,2586,2587],{},"CMD [\"node\", \"server.js\"]                   # 元数据，永远放最后\n",[15,2589,2590,2591,2593,2594,2596],{},"反过来写（先 ",[19,2592,1483],{}," 再 ",[19,2595,1968],{},"）的话，改一行注释就要重装依赖。示例 04 把这份 Dockerfile 做成了可点交互：点任意一条指令，右侧的层堆叠会即时高亮「新增层 \u002F 元数据层 \u002F 复用缓存」，并演示改动源码时的失效范围。",[1405,2598],{"height":2599,"src":2600,"title":2601},640,"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F04-dockerfile-layers.html","示例 04：Dockerfile 指令 → 分层效果可视化",[40,2603,2605],{"id":2604},"一个可以直接抄的最小-dockerfile","一个可以直接抄的最小 Dockerfile",[1158,2607,2609],{"className":1430,"code":2608,"language":1432,"meta":1163,"style":1163},"FROM node:22-bookworm-slim\nENV NODE_ENV=production\nWORKDIR \u002Fsrv\u002Fshop\nCOPY package.json package-lock.json .\u002F\nRUN npm ci --omit=dev --npm-config.ignore-scripts=false\nCOPY src .\u002Fsrc\nEXPOSE 3000\nUSER 1001:1001\nHEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD node -e \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"\nCMD [\"node\", \"src\u002Fserver.js\"]\n",[19,2610,2611,2616,2620,2625,2630,2635,2640,2645,2650,2655],{"__ignoreMap":1163},[1167,2612,2613],{"class":1169,"line":1170},[1167,2614,2615],{},"FROM node:22-bookworm-slim\n",[1167,2617,2618],{"class":1169,"line":1177},[1167,2619,2275],{},[1167,2621,2622],{"class":1169,"line":1202},[1167,2623,2624],{},"WORKDIR \u002Fsrv\u002Fshop\n",[1167,2626,2627],{"class":1169,"line":1208},[1167,2628,2629],{},"COPY package.json package-lock.json .\u002F\n",[1167,2631,2632],{"class":1169,"line":1238},[1167,2633,2634],{},"RUN npm ci --omit=dev --npm-config.ignore-scripts=false\n",[1167,2636,2637],{"class":1169,"line":1257},[1167,2638,2639],{},"COPY src .\u002Fsrc\n",[1167,2641,2642],{"class":1169,"line":1274},[1167,2643,2644],{},"EXPOSE 3000\n",[1167,2646,2647],{"class":1169,"line":1290},[1167,2648,2649],{},"USER 1001:1001\n",[1167,2651,2652],{"class":1169,"line":1304},[1167,2653,2654],{},"HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD node -e \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"\n",[1167,2656,2657],{"class":1169,"line":1310},[1167,2658,2659],{},"CMD [\"node\", \"src\u002Fserver.js\"]\n",[15,2661,2662,2663,2665,2666,2668,2669,2671,2672,2674,2675,716,2678,2680],{},"里面每一行的理由：",[19,2664,1380],{}," 提前让依赖只装生产包；两次 ",[19,2667,1768],{}," 切出缓存分水岭；",[19,2670,1389],{}," 用数字 UID（不依赖镜像里恰好存在那个用户名）；",[19,2673,2502],{}," 走 exec 形式并显式 ",[19,2676,2677],{},"|| exit",[19,2679,847],{}," 用 exec 形式以便收信号。下一章讲构建流程时，我们围绕它做缓存实验。",[10,2682,2684],{"id":2683},"五构建流程与缓存buildkitdockerignore-与多阶段构建","五、构建流程与缓存：BuildKit、.dockerignore 与多阶段构建",[40,2686,2688],{"id":2687},"docker-build-的最小形态与必给参数","docker build 的最小形态与必给参数",[1158,2690,2692],{"className":1160,"code":2691,"language":1162,"meta":1163,"style":1163},"docker build -t ghcr.io\u002Fexample\u002Fshop-api:1.8.3 .\ndocker build -f Dockerfile.prod --target runtime -t shop-api:1.8.3 .\u002Fbackend\n",[19,2693,2694,2711],{"__ignoreMap":1163},[1167,2695,2696,2698,2701,2705,2708],{"class":1169,"line":1170},[1167,2697,559],{"class":1173},[1167,2699,2700],{"class":1183}," build",[1167,2702,2704],{"class":2703},"sDLfK"," -t",[1167,2706,2707],{"class":1183}," ghcr.io\u002Fexample\u002Fshop-api:1.8.3",[1167,2709,2710],{"class":1183}," .\n",[1167,2712,2713,2715,2717,2720,2723,2726,2729,2731,2734],{"class":1169,"line":1177},[1167,2714,559],{"class":1173},[1167,2716,2700],{"class":1183},[1167,2718,2719],{"class":2703}," -f",[1167,2721,2722],{"class":1183}," Dockerfile.prod",[1167,2724,2725],{"class":2703}," --target",[1167,2727,2728],{"class":1183}," runtime",[1167,2730,2704],{"class":2703},[1167,2732,2733],{"class":1183}," shop-api:1.8.3",[1167,2735,2736],{"class":1183}," .\u002Fbackend\n",[45,2738,2739,2751],{},[48,2740,2741],{},[51,2742,2743,2746,2748],{},[54,2744,2745],{},"参数",[54,2747,1566],{},[54,2749,2750],{},"什么时候必须用",[67,2752,2753,2766,2782,2797,2810,2823,2835,2854],{},[51,2754,2755,2760,2763],{},[72,2756,2757],{},[19,2758,2759],{},"-t name:tag",[72,2761,2762],{},"命名并打标签",[72,2764,2765],{},"每次；没 tag 的镜像是悬空镜像，难找难删",[51,2767,2768,2776,2779],{},[72,2769,2770,728,2773],{},[19,2771,2772],{},"-f",[19,2774,2775],{},"--target",[72,2777,2778],{},"指定 Dockerfile \u002F 多阶段只做到某阶段",[72,2780,2781],{},"一仓库多构建；单独调试 builder 阶段",[51,2783,2784,2789,2794],{},[72,2785,2786],{},[19,2787,2788],{},"--build-arg K=V",[72,2790,2791,2792],{},"传 ",[19,2793,2214],{},[72,2795,2796],{},"版本号、代理、构建开关",[51,2798,2799,2804,2807],{},[72,2800,2801],{},[19,2802,2803],{},"--pull",[72,2805,2806],{},"强制重新拉基础镜像",[72,2808,2809],{},"追基础镜像的安全补丁（否则用本地旧副本）",[51,2811,2812,2817,2820],{},[72,2813,2814],{},[19,2815,2816],{},"--no-cache",[72,2818,2819],{},"全量重建",[72,2821,2822],{},"复现 CI 的干净构建，日常别用（慢）",[51,2824,2825,2829,2832],{},[72,2826,2827],{},[19,2828,470],{},[72,2830,2831],{},"指定目标平台",[72,2833,2834],{},"在 x86 上给 ARM 设备构建",[51,2836,2837,2845,2848],{},[72,2838,2839,728,2842],{},[19,2840,2841],{},"--push",[19,2843,2844],{},"--load",[72,2846,2847],{},"构建完推送 \u002F 载入本地",[72,2849,2850,2851,2853],{},"多架构时 ",[19,2852,2844],{}," 不支持多平台镜像",[51,2855,2856,2861,2864],{},[72,2857,2858],{},[19,2859,2860],{},"--progress=plain",[72,2862,2863],{},"展开完整日志",[72,2865,2866],{},"构建失败时看清是哪条指令哪一行",[15,2868,2869,2870,2873,2874,2877,2878,2881,2882,2885,2886,2888],{},"结尾那个 ",[19,2871,2872],{},"."," 是",[31,2875,2876],{},"构建上下文","：Docker 会把它整个目录打包传给 daemon。目录里塞了 2GB 的 ",[19,2879,2880],{},"node_modules"," 或 ",[19,2883,2884],{},".git","，你每次构建就白传 2GB——这是 ",[19,2887,1881],{}," 存在的第一理由。",[40,2890,2892,2893],{"id":2891},"buildkit-与-syntax","BuildKit 与 ",[19,2894,2895],{},"# syntax=",[15,2897,2898,2899,2902,2903,2906],{},"从 Docker 23 起 BuildKit 是默认构建器。它相对老构建器的差异不是「快一点」，而是三条实质能力（",[19,2900,2901],{},"--mount","、heredoc、并行构建）。启用新语法靠文件",[31,2904,2905],{},"第一行","的解析器指令：",[1158,2908,2910],{"className":1430,"code":2909,"language":1432,"meta":1163,"style":1163},"#syntax=docker\u002Fdockerfile:1\nRUN --mount=type=cache,target=\u002Froot\u002F.npm npm ci --omit=dev\nCOPY --link . .                         # 不因父层权限反复失效，也不重置时间戳\nRUN \u003C\u003CEOF                             # heredoc：多行脚本不用拼 && 与转义\n  addgroup -g 1001 app && adduser -S -D -u 1001 -G app app\nEOF\n",[19,2911,2912,2917,2922,2927,2932,2937],{"__ignoreMap":1163},[1167,2913,2914],{"class":1169,"line":1170},[1167,2915,2916],{},"#syntax=docker\u002Fdockerfile:1\n",[1167,2918,2919],{"class":1169,"line":1177},[1167,2920,2921],{},"RUN --mount=type=cache,target=\u002Froot\u002F.npm npm ci --omit=dev\n",[1167,2923,2924],{"class":1169,"line":1202},[1167,2925,2926],{},"COPY --link . .                         # 不因父层权限反复失效，也不重置时间戳\n",[1167,2928,2929],{"class":1169,"line":1208},[1167,2930,2931],{},"RUN \u003C\u003CEOF                             # heredoc：多行脚本不用拼 && 与转义\n",[1167,2933,2934],{"class":1169,"line":1238},[1167,2935,2936],{},"  addgroup -g 1001 app && adduser -S -D -u 1001 -G app app\n",[1167,2938,2939],{"class":1169,"line":1257},[1167,2940,2941],{},"EOF\n",[15,2943,2944,2947,2948,793,2950,2952,2953,2956,2957,2960,2961,2964,2965,2968,2969,1962],{},[19,2945,2946],{},"#syntax="," 必须放在 ",[19,2949,2214],{},[19,2951,1728],{}," 之前才生效（规范写法哈希后带空格也合法，解析器允许）。写 ",[19,2954,2955],{},"docker\u002Fdockerfile:1"," 表示跟随 1.x，写 ",[19,2958,2959],{},"docker\u002Fdockerfile:1.7"," 是固定版本、CI 可复现。",[19,2962,2963],{},"--mount=type=cache"," 的缓存",[31,2966,2967],{},"不进镜像","，只在构建环境保留，能把「每次重装依赖」从 90 秒压到 5 秒；代价是它不受层缓存控制，换基础镜像后可能读到旧包（必要时 ",[19,2970,2971],{},"docker builder prune",[15,2973,2974,2975,2977],{},"示例 05 用同一个 Dockerfile 做三种改动——改注释、改依赖锁、改源码——对比 ",[19,2976,1550],{}," 的逐层输出，失效起点完全不同。",[1405,2979],{"height":1407,"src":2980,"title":2981},"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F05-build-cache.html","示例 05：构建缓存——改哪一层就从哪一层重跑",[40,2983,2984],{"id":2984},"层缓存命中的判定规则",[15,2986,2987,2988,2991,2992,2995],{},"一句话规则：",[31,2989,2990],{},"命中 = 父层 ID 相同 + 指令文本相同 + （对 COPY\u002FADD）被复制文件的内容哈希相同","。三条任何一条变了，这一层以及",[31,2993,2994],{},"它之后的所有层","全部重建。",[45,2997,2998,3008],{},[48,2999,3000],{},[51,3001,3002,3005],{},[54,3003,3004],{},"变更内容",[54,3006,3007],{},"从哪一层开始重建",[67,3009,3010,3025,3040,3053,3064,3074],{},[51,3011,3012,3022],{},[72,3013,3014,3015,793,3017,793,3019,3021],{},"只改 ",[19,3016,847],{},[19,3018,1380],{},[19,3020,1383],{}," 的值",[72,3023,3024],{},"从那条元数据指令起（前面全命中，重建很快）",[51,3026,3027,3034],{},[72,3028,3029,3030,3033],{},"改 ",[19,3031,3032],{},"README.md","（没被 COPY 到）",[72,3035,3036,3037,3039],{},"通常不重建；但若它在上下文里且被 ",[19,3038,1483],{}," 覆盖，就重建",[51,3041,3042,3047],{},[72,3043,3029,3044],{},[19,3045,3046],{},"package-lock.json",[72,3048,3049,3052],{},[19,3050,3051],{},"COPY package-lock"," 那层起重建",[51,3054,3055,3060],{},[72,3056,3029,3057],{},[19,3058,3059],{},"src\u002Fserver.js",[72,3061,3062,3052],{},[19,3063,1483],{},[51,3065,3066,3071],{},[72,3067,3068,3069,1075],{},"基础镜像更新（",[19,3070,2803],{},[72,3072,3073],{},"全部重建",[51,3075,3076,3079],{},[72,3077,3078],{},"文件权限\u002Fmode 变化",[72,3080,3081],{},"也算内容变化（COPY 把 mode 纳入哈希）",[15,3083,3084],{},"两个反直觉细节：",[1318,3086,3087,3117],{},[584,3088,3089,3097,3098,3101,3102,3105,3106,3109,3110,3112,3113,3116],{},[31,3090,3091,3093,3094,3096],{},[19,3092,1483],{}," 的哈希是整个上下文（扣除 ",[19,3095,1881],{},"）算的","。于是 ",[19,3099,3100],{},".gitignore"," 改了、",[19,3103,3104],{},"dist\u002F"," 多了临时文件、生成了 ",[19,3107,3108],{},"*.log"," 都会击穿缓存。把 ",[19,3111,1483],{}," 收窄成 ",[19,3114,3115],{},"COPY src .\u002Fsrc","，既减小上下文又稳定缓存。",[584,3118,3119,616,3124,3127,3128,3131,3132,3134,3135,3138],{},[31,3120,3121,3123],{},[19,3122,1924],{}," 只看指令文本，不看外部世界",[19,3125,3126],{},"RUN apt-get install -y curl=7.88.1-1+deb12u1"," 带版本写法能按需失效；而 ",[19,3129,3130],{},"RUN git clone https:\u002F\u002Fgithub.com\u002Fexample\u002Fshop"," 只要文本不变就永远命中旧缓存，拿到的是半年前的代码——「代码明明更新了镜像里却是旧的」的成因。解法：用 ",[19,3133,1768],{}," 放进去，或加 ",[19,3136,3137],{},"--build-arg COMMIT="," 让文本变化。",[40,3140,3142],{"id":3141},"dockerignore不是可选项",".dockerignore：不是可选项",[1158,3144,3149],{"className":3145,"code":3147,"language":3148},[3146],"language-text",".git\n**\u002Fnode_modules\ndist\ncoverage\n*.log\n.env\n.env.*\n!.env.example\ncompose*.yaml\ntests\n","text",[19,3150,3147],{"__ignoreMap":1163},[15,3152,3153,3154,3157,3158,3161,3162,1372,3165,114,3168,3171,3172,3174,3175,3177,3178,3180,3181,3183,3184,3186,3187,3190,3191,3197],{},"不写它的三个后果：",[31,3155,3156],{},"构建变慢","（上下文几 GB）、",[31,3159,3160],{},"缓存被击穿","（任何杂文件都改内容哈希）、",[31,3163,3164],{},"泄密",[19,3166,3167],{},".env",[19,3169,3170],{},"id_rsa"," 被 ",[19,3173,1483],{}," 打进镜像，",[19,3176,1593],{}," 与 ",[19,3179,1508],{}," 之后都能翻出来）。语法接近 ",[19,3182,3100],{},"，有一处常见「误报错」：",[19,3185,3167],{}," 被 ignore 后 ",[19,3188,3189],{},"COPY .env \u002Fapp\u002F"," 直接失败——这不是 bug，而是提示：",[31,3192,3193,3194,3196],{},"镜像里不该有需要 ",[19,3195,3167],{}," 的东西","（第十三章）。",[40,3199,3201],{"id":3200},"多阶段构建编译阶段与运行阶段彻底分开","多阶段构建：编译阶段与运行阶段彻底分开",[15,3203,3204],{},"问题的形状：构建 Node\u002FGo\u002FJava 应用需要编译器、包管理器、源码、测试依赖、头文件，运行只需要运行时和产物。把它们留在一个镜像里，等于把攻击面和体积一起打包上线。",[1158,3206,3208],{"className":1430,"code":3207,"language":1432,"meta":1163,"style":1163},"#syntax=docker\u002Fdockerfile:1\nFROM node:22-bookworm-slim AS deps\nWORKDIR \u002Fsrv\u002Fshop\nCOPY package.json package-lock.json .\u002F\nRUN npm ci --omit=dev\nFROM node:22-bookworm-slim AS build\nCOPY --from=deps \u002Fsrv\u002Fshop\u002Fnode_modules .\u002Fnode_modules\nCOPY . .\nRUN npm run build && npm prune --omit=dev\nFROM node:22-bookworm-slim AS runtime        # 只有最后一个 FROM 之后的内容进最终镜像\nENV NODE_ENV=production\nWORKDIR \u002Fsrv\u002Fshop\nCOPY --from=build \u002Fsrv\u002Fshop .\u002F\nUSER 1001:1001\nEXPOSE 3000\nCMD [\"node\", \"server.js\"]\n",[19,3209,3210,3214,3219,3223,3227,3232,3237,3242,3247,3252,3257,3262,3267,3273,3278,3283],{"__ignoreMap":1163},[1167,3211,3212],{"class":1169,"line":1170},[1167,3213,2916],{},[1167,3215,3216],{"class":1169,"line":1177},[1167,3217,3218],{},"FROM node:22-bookworm-slim AS deps\n",[1167,3220,3221],{"class":1169,"line":1202},[1167,3222,2624],{},[1167,3224,3225],{"class":1169,"line":1208},[1167,3226,2629],{},[1167,3228,3229],{"class":1169,"line":1238},[1167,3230,3231],{},"RUN npm ci --omit=dev\n",[1167,3233,3234],{"class":1169,"line":1257},[1167,3235,3236],{},"FROM node:22-bookworm-slim AS build\n",[1167,3238,3239],{"class":1169,"line":1274},[1167,3240,3241],{},"COPY --from=deps \u002Fsrv\u002Fshop\u002Fnode_modules .\u002Fnode_modules\n",[1167,3243,3244],{"class":1169,"line":1290},[1167,3245,3246],{},"COPY . .\n",[1167,3248,3249],{"class":1169,"line":1304},[1167,3250,3251],{},"RUN npm run build && npm prune --omit=dev\n",[1167,3253,3254],{"class":1169,"line":1310},[1167,3255,3256],{},"FROM node:22-bookworm-slim AS runtime        # 只有最后一个 FROM 之后的内容进最终镜像\n",[1167,3258,3260],{"class":1169,"line":3259},11,[1167,3261,2275],{},[1167,3263,3265],{"class":1169,"line":3264},12,[1167,3266,2624],{},[1167,3268,3270],{"class":1169,"line":3269},13,[1167,3271,3272],{},"COPY --from=build \u002Fsrv\u002Fshop .\u002F\n",[1167,3274,3276],{"class":1169,"line":3275},14,[1167,3277,2649],{},[1167,3279,3281],{"class":1169,"line":3280},15,[1167,3282,2644],{},[1167,3284,3286],{"class":1169,"line":3285},16,[1167,3287,3288],{},"CMD [\"node\", \"server.js\"]\n",[15,3290,3291,3292,3297,3298,1372,3301,3304,3305,3308,3309,3312,3313,1372,3316,3319,3320,3323,3324,3327],{},"要点四条：",[31,3293,3294],{},[19,3295,3296],{},"COPY --from=\u003C阶段名|序号|外部镜像>"," 从别的阶段取产物，未被最终阶段引用的中间阶段会被跳过；",[31,3299,3300],{},"产物里不要夹带构建工具",[19,3302,3303],{},"npm prune --omit=dev","；Go 用 ",[19,3306,3307],{},"CGO_ENABLED=0"," 静态编译后拷进 ",[19,3310,3311],{},"FROM scratch","）；",[31,3314,3315],{},"阶段命名可读",[19,3317,3318],{},"AS deps","）才方便 ",[19,3321,3322],{},"--target deps"," 单独构建调试；",[31,3325,3326],{},"各阶段共享同一基础镜像的层","，多阶段不额外吃磁盘。常见量级：1.2GB → 180MB。",[40,3329,3331],{"id":3330},"buildx-与多架构","buildx 与多架构",[1158,3333,3335],{"className":1160,"code":3334,"language":1162,"meta":1163,"style":1163},"docker buildx create --name multi --driver docker-container --use\ndocker buildx inspect --bootstrap\ndocker run --privileged --rm tonistiigi\u002Fbinfmt --install all\ndocker buildx build --platform linux\u002Famd64,linux\u002Farm64 -t ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --push .\ndocker manifest inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n",[19,3336,3337,3362,3374,3396,3419],{"__ignoreMap":1163},[1167,3338,3339,3341,3344,3347,3350,3353,3356,3359],{"class":1169,"line":1170},[1167,3340,559],{"class":1173},[1167,3342,3343],{"class":1183}," buildx",[1167,3345,3346],{"class":1183}," create",[1167,3348,3349],{"class":2703}," --name",[1167,3351,3352],{"class":1183}," multi",[1167,3354,3355],{"class":2703}," --driver",[1167,3357,3358],{"class":1183}," docker-container",[1167,3360,3361],{"class":2703}," --use\n",[1167,3363,3364,3366,3368,3371],{"class":1169,"line":1177},[1167,3365,559],{"class":1173},[1167,3367,3343],{"class":1183},[1167,3369,3370],{"class":1183}," inspect",[1167,3372,3373],{"class":2703}," --bootstrap\n",[1167,3375,3376,3378,3381,3384,3387,3390,3393],{"class":1169,"line":1202},[1167,3377,559],{"class":1173},[1167,3379,3380],{"class":1183}," run",[1167,3382,3383],{"class":2703}," --privileged",[1167,3385,3386],{"class":2703}," --rm",[1167,3388,3389],{"class":1183}," tonistiigi\u002Fbinfmt",[1167,3391,3392],{"class":2703}," --install",[1167,3394,3395],{"class":1183}," all\n",[1167,3397,3398,3400,3402,3404,3407,3410,3412,3414,3417],{"class":1169,"line":1208},[1167,3399,559],{"class":1173},[1167,3401,3343],{"class":1183},[1167,3403,2700],{"class":1183},[1167,3405,3406],{"class":2703}," --platform",[1167,3408,3409],{"class":1183}," linux\u002Famd64,linux\u002Farm64",[1167,3411,2704],{"class":2703},[1167,3413,2707],{"class":1183},[1167,3415,3416],{"class":2703}," --push",[1167,3418,2710],{"class":1183},[1167,3420,3421,3423,3426,3428],{"class":1169,"line":1238},[1167,3422,559],{"class":1173},[1167,3424,3425],{"class":1183}," manifest",[1167,3427,3370],{"class":1183},[1167,3429,3430],{"class":1183}," ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n",[15,3432,3433,3434,1372,3437,3440,3441,939,3444,3446,3447,3449,3450,1962,3453,3458,3459,3462,3463,616],{},"跨架构构建靠 ",[31,3435,3436],{},"QEMU 用户态模拟",[19,3438,3439],{},"binfmt"," 注册解释器），慢且某些编译会段错误，稳妥做法是用原生 runner（CI 上的 ARM 机器或云端构建）。一次产出多架构的结果是",[31,3442,3443],{},"一个 manifest list（镜像索引）",[19,3445,2844],{}," 只能载入单个平台，多架构必须 ",[19,3448,2841],{},"（或 ",[19,3451,3452],{},"--output type=oci,dest=out.tar",[31,3454,3455,3457],{},[19,3456,463],{}," 的根因就在这里","：拿 amd64 镜像去 ARM 上跑；查平台用 ",[19,3460,3461],{},"docker image inspect --format '{{.Os}}\u002F{{.Architecture}}'","，验证可以用 ",[19,3464,3465],{},"docker pull --platform linux\u002Farm64 busybox",[40,3467,3469],{"id":3468},"构建参数与-secrets-mount","构建参数与 secrets mount",[15,3471,3472,3473,3476,3477,3480],{},"需要凭据才能构建（私有 npm registry、GitHub Packages）时，",[31,3474,3475],{},"不要","用 ",[19,3478,3479],{},"--build-arg TOKEN=xxx","，它会留在层历史里；用 BuildKit 的 secret mount：",[1158,3482,3484],{"className":1160,"code":3483,"language":1162,"meta":1163,"style":1163},"printf '%s' \"${NPM_TOKEN}\" | docker buildx build --secret id=npm_token,src=- -t shop-api:1.8.3 .\n",[19,3485,3486],{"__ignoreMap":1163},[1167,3487,3488,3491,3494,3497,3500,3503,3507,3510,3512,3514,3517,3520,3522,3524],{"class":1169,"line":1170},[1167,3489,3490],{"class":2703},"printf",[1167,3492,3493],{"class":1183}," '%s'",[1167,3495,3496],{"class":1183}," \"${",[1167,3498,3499],{"class":1216},"NPM_TOKEN",[1167,3501,3502],{"class":1183},"}\"",[1167,3504,3506],{"class":3505},"snl16"," |",[1167,3508,3509],{"class":1173}," docker",[1167,3511,3343],{"class":1183},[1167,3513,2700],{"class":1183},[1167,3515,3516],{"class":2703}," --secret",[1167,3518,3519],{"class":1183}," id=npm_token,src=-",[1167,3521,2704],{"class":2703},[1167,3523,2733],{"class":1183},[1167,3525,2710],{"class":1183},[1158,3527,3529],{"className":1430,"code":3528,"language":1432,"meta":1163,"style":1163},"RUN --mount=type=secret,id=npm_token,target=\u002Frun\u002Fsecrets\u002Fnpm_token \\\n    NPM_TOKEN=$(cat \u002Frun\u002Fsecrets\u002Fnpm_token) npm ci --omit=dev\n",[19,3530,3531,3536],{"__ignoreMap":1163},[1167,3532,3533],{"class":1169,"line":1170},[1167,3534,3535],{},"RUN --mount=type=secret,id=npm_token,target=\u002Frun\u002Fsecrets\u002Fnpm_token \\\n",[1167,3537,3538],{"class":1169,"line":1177},[1167,3539,3540],{},"    NPM_TOKEN=$(cat \u002Frun\u002Fsecrets\u002Fnpm_token) npm ci --omit=dev\n",[15,3542,3543,3545,3546,3549,3550,3552,3553,3555,3556,3559],{},[19,3544,2317],{}," 的文件只在",[31,3547,3548],{},"这条指令执行期间","存在，不进任何层、不出现在 ",[19,3551,1593],{},"。另外 ",[19,3554,2860],{}," 的日志会被 CI 存档，别把 secret ",[19,3557,3558],{},"echo"," 出来。",[15,3561,3562,3563,3565],{},"构建侧的收获总结成三句：Dockerfile 的顺序决定速度，",[19,3564,1881],{}," 决定速度与安全性，多阶段决定体积与攻击面。下面进入运行侧。",[10,3567,3569],{"id":3568},"六容器生命周期与-run-参数一张表把命令行讲清","六、容器生命周期与 run 参数：一张表把命令行讲清",[15,3571,3572,3573,3575,3576,3578],{},"镜像是死的，容器是活的。这一章把 ",[19,3574,555],{}," 的几十个参数按用途分组，再解决「",[19,3577,869],{}," 里那些状态字符串到底在说什么」。",[40,3580,3582],{"id":3581},"状态机created-running-paused-exited-removed","状态机：created → running → (paused) → exited → removed",[1158,3584,3586],{"className":1160,"code":3585,"language":1162,"meta":1163,"style":1163},"docker create --name shop-web nginx:1.27-alpine    # 只建不跑：created\ndocker start shop-web                              # running\ndocker pause shop-web; docker unpause shop-web     # 冻结 \u002F 解冻（cgroup freezer）\ndocker stop -t 10 shop-web                         # 先 SIGTERM，超时后 SIGKILL → exited\ndocker kill -s SIGTERM shop-web                    # 直接发信号，默认 SIGKILL\ndocker rm shop-web                                 # 删除容器（连同可写层）\ndocker container prune                             # 清掉所有已停止容器\n",[19,3587,3588,3606,3618,3640,3657,3675,3686],{"__ignoreMap":1163},[1167,3589,3590,3592,3594,3596,3599,3602],{"class":1169,"line":1170},[1167,3591,559],{"class":1173},[1167,3593,3346],{"class":1183},[1167,3595,3349],{"class":2703},[1167,3597,3598],{"class":1183}," shop-web",[1167,3600,3601],{"class":1183}," nginx:1.27-alpine",[1167,3603,3605],{"class":3604},"sAwPA","    # 只建不跑：created\n",[1167,3607,3608,3610,3613,3615],{"class":1169,"line":1177},[1167,3609,559],{"class":1173},[1167,3611,3612],{"class":1183}," start",[1167,3614,3598],{"class":1183},[1167,3616,3617],{"class":3604},"                              # running\n",[1167,3619,3620,3622,3625,3627,3630,3632,3635,3637],{"class":1169,"line":1202},[1167,3621,559],{"class":1173},[1167,3623,3624],{"class":1183}," pause",[1167,3626,3598],{"class":1183},[1167,3628,3629],{"class":1216},"; ",[1167,3631,559],{"class":1173},[1167,3633,3634],{"class":1183}," unpause",[1167,3636,3598],{"class":1183},[1167,3638,3639],{"class":3604},"     # 冻结 \u002F 解冻（cgroup freezer）\n",[1167,3641,3642,3644,3647,3649,3652,3654],{"class":1169,"line":1208},[1167,3643,559],{"class":1173},[1167,3645,3646],{"class":1183}," stop",[1167,3648,2704],{"class":2703},[1167,3650,3651],{"class":2703}," 10",[1167,3653,3598],{"class":1183},[1167,3655,3656],{"class":3604},"                         # 先 SIGTERM，超时后 SIGKILL → exited\n",[1167,3658,3659,3661,3664,3667,3670,3672],{"class":1169,"line":1238},[1167,3660,559],{"class":1173},[1167,3662,3663],{"class":1183}," kill",[1167,3665,3666],{"class":2703}," -s",[1167,3668,3669],{"class":1183}," SIGTERM",[1167,3671,3598],{"class":1183},[1167,3673,3674],{"class":3604},"                    # 直接发信号，默认 SIGKILL\n",[1167,3676,3677,3679,3681,3683],{"class":1169,"line":1257},[1167,3678,559],{"class":1173},[1167,3680,1196],{"class":1183},[1167,3682,3598],{"class":1183},[1167,3684,3685],{"class":3604},"                                 # 删除容器（连同可写层）\n",[1167,3687,3688,3690,3693,3696],{"class":1169,"line":1274},[1167,3689,559],{"class":1173},[1167,3691,3692],{"class":1183}," container",[1167,3694,3695],{"class":1183}," prune",[1167,3697,3698],{"class":3604},"                             # 清掉所有已停止容器\n",[15,3700,3701],{},"三条要点：",[1318,3703,3704,3723,3743],{},[584,3705,3706,616,3716,3718,3719,3722],{},[31,3707,3708,3711,3712,3715],{},[19,3709,3710],{},"stop"," 是请求，",[19,3713,3714],{},"kill"," 是强制",[19,3717,3710],{}," 发 SIGTERM 并等 ",[19,3720,3721],{},"-t"," 秒（默认 10），到点没退就 SIGKILL。这段等待期是应用收尾（放连接、刷缓冲、提交 offset）的唯一窗口——第十六章整章讲这条链路为什么经常失效。",[584,3724,3725,3731,3732,3735,3736,3738,3739,3742],{},[31,3726,3727,3730],{},[19,3728,3729],{},"pause"," 不是「暂停服务」","。它冻结进程，TCP 连接会一直挂着直到超时；而且暂停中的容器不能 ",[19,3733,3734],{},"exec"," 也不能 ",[19,3737,3710],{},"（要先 ",[19,3740,3741],{},"unpause","）。生产里几乎不用。",[584,3744,3745,3751,3752,3754,3755,3758,3759,3761],{},[31,3746,3747,3750],{},[19,3748,3749],{},"--rm"," 让容器一次性","：主进程退出即自动 ",[19,3753,151],{},"，可写层与匿名卷一起销毁。好处是不留垃圾，坏处是",[31,3756,3757],{},"排障现场消失了","——调试阶段永远别加 ",[19,3760,3749],{},"，有数据的容器永远不要加。",[40,3763,3765],{"id":3764},"run-参数分组速查","run 参数分组速查",[45,3767,3768,3780],{},[48,3769,3770],{},[51,3771,3772,3775,3777],{},[54,3773,3774],{},"组",[54,3776,2745],{},[54,3778,3779],{},"说明与坑",[67,3781,3782,3795,3823,3834,3857,3873,3894,3909,3929,3946,3968,3985,4004,4031,4047,4060,4085,4103,4116,4128,4144,4156],{},[51,3783,3784,3787,3792],{},[72,3785,3786],{},"运行控制",[72,3788,3789],{},[19,3790,3791],{},"-d",[72,3793,3794],{},"后台运行并打印容器 ID",[51,3796,3797,3799,3804],{},[72,3798],{},[72,3800,3801],{},[19,3802,3803],{},"-it",[72,3805,3806,3809,3810,3812,3813,3816,3817,3819,3820],{},[19,3807,3808],{},"-i"," 保持 stdin、",[19,3811,3721],{}," 分配伪终端，",[31,3814,3815],{},"通常两个一起用","；cron\u002FCI 里不要加 ",[19,3818,3803],{},"，否则 ",[19,3821,3822],{},"the input device is not a TTY",[51,3824,3825,3827,3831],{},[72,3826],{},[72,3828,3829],{},[19,3830,3749],{},[72,3832,3833],{},"退出即删，别用于有数据的容器",[51,3835,3836,3839,3847],{},[72,3837,3838],{},"标识",[72,3840,3841,728,3844],{},[19,3842,3843],{},"--name",[19,3845,3846],{},"--hostname",[72,3848,3849,3850,3853,3854,3856],{},"前者是容器名（也是网络里的 DNS 名），重名报 ",[19,3851,3852],{},"Conflict. The container name ... is already in use","；后者只改容器内 ",[19,3855,354],{}," 的值，两者是两回事",[51,3858,3859,3861,3866],{},[72,3860],{},[72,3862,3863],{},[19,3864,3865],{},"--label env=prod",[72,3867,3868,3869,3872],{},"打标签，配合 ",[19,3870,3871],{},"--filter label=env=prod"," 批量管理",[51,3874,3875,3878,3883],{},[72,3876,3877],{},"端口",[72,3879,3880],{},[19,3881,3882],{},"-p 8080:80",[72,3884,3885,3886,3889,3890,3893],{},"宿主 ",[19,3887,3888],{},"0.0.0.0:8080"," → 容器 80；写 ",[19,3891,3892],{},"-p 8080:80\u002Fudp"," 才是 UDP，不写默认 tcp",[51,3895,3896,3898,3903],{},[72,3897],{},[72,3899,3900],{},[19,3901,3902],{},"-p 127.0.0.1:8080:80",[72,3904,3905,3906],{},"只绑回环，",[31,3907,3908],{},"数据库与管理面板的默认写法",[51,3910,3911,3913,3920],{},[72,3912],{},[72,3914,3915,728,3918],{},[19,3916,3917],{},"-p 8080",[19,3919,2474],{},[72,3921,3922,3923,3925,3926,3928],{},"前者宿主端口随机、后者把所有 ",[19,3924,1383],{}," 过的端口都随机发布（",[19,3927,865],{}," 看结果）",[51,3930,3931,3934,3939],{},[72,3932,3933],{},"环境",[72,3935,3936],{},[19,3937,3938],{},"-e KEY=VAL",[72,3940,3941,3942,3945],{},"可重复；",[19,3943,3944],{},"-e KEY"," 表示从当前 shell 透传",[51,3947,3948,3950,3955],{},[72,3949],{},[72,3951,3952],{},[19,3953,3954],{},"--env-file .env.production",[72,3956,3957,3958,939,3961,3964,3965,3967],{},"每行 ",[19,3959,3960],{},"K=V",[19,3962,3963],{},"#"," 开头是注释；别把 ",[19,3966,3167],{}," 提交",[51,3969,3970,3973,3978],{},[72,3971,3972],{},"挂载",[72,3974,3975],{},[19,3976,3977],{},"-v name:\u002Fpath",[72,3979,3980,3981,3984],{},"命名卷（省略 name 则是匿名卷）；",[19,3982,3983],{},"-v .\u002Fconf:\u002Fetc\u002Fapp:ro"," 是 bind mount + 只读，相对路径基于当前目录",[51,3986,3987,3989,3994],{},[72,3988],{},[72,3990,3991],{},[19,3992,3993],{},"--mount type=volume,source=db,target=\u002Fvar\u002Flib\u002Fmysql",[72,3995,3996,3997,114,4000,4003],{},"等价但可写 ",[19,3998,3999],{},"readonly",[19,4001,4002],{},"volume-opt","，拼错会直接报错（第九章对照表）",[51,4005,4006,4009,4017],{},[72,4007,4008],{},"网络",[72,4010,4011,728,4014],{},[19,4012,4013],{},"--network shop-net",[19,4015,4016],{},"--network host",[72,4018,4019,4020,4023,4024,114,4027,4030],{},"自定义网络（有 DNS，第八章）；",[19,4021,4022],{},"host"," 共享宿主栈（Mac\u002FWindows 上不生效，且让 ",[19,4025,4026],{},"--dns",[19,4028,4029],{},"--network-alias"," 失效）",[51,4032,4033,4035,4040],{},[72,4034],{},[72,4036,4037],{},[19,4038,4039],{},"--add-host db:10.0.0.5",[72,4041,4042,4043,4046],{},"往 ",[19,4044,4045],{},"\u002Fetc\u002Fhosts"," 追加一条",[51,4048,4049,4052,4057],{},[72,4050,4051],{},"重启",[72,4053,4054],{},[19,4055,4056],{},"--restart unless-stopped",[72,4058,4059],{},"四种策略见第十六章",[51,4061,4062,4065,4073],{},[72,4063,4064],{},"资源",[72,4066,4067,728,4070],{},[19,4068,4069],{},"--memory 512m",[19,4071,4072],{},"-m",[72,4074,4075,4076,716,4078,2873,4081,4084],{},"内存上限，超了就 ",[19,4077,398],{},[19,4079,4080],{},"--memory-swap 768m",[31,4082,4083],{},"总内存 + swap","，语义特殊，第十四章专门讲",[51,4086,4087,4089,4100],{},[72,4088],{},[72,4090,4091,728,4094,728,4097],{},[19,4092,4093],{},"--cpus 1.5",[19,4095,4096],{},"--cpuset-cpus 0,1",[19,4098,4099],{},"--pids-limit 256",[72,4101,4102],{},"CPU 配额 \u002F 绑核 \u002F 防 fork 炸弹",[51,4104,4105,4108,4113],{},[72,4106,4107],{},"安全",[72,4109,4110],{},[19,4111,4112],{},"--user 1001:1001",[72,4114,4115],{},"以该身份运行（对齐 bind mount 属主时用）",[51,4117,4118,4120,4125],{},[72,4119],{},[72,4121,4122],{},[19,4123,4124],{},"--read-only --tmpfs \u002Ftmp",[72,4126,4127],{},"根文件系统只读 + 临时可写目录",[51,4129,4130,4132,4137],{},[72,4131],{},[72,4133,4134],{},[19,4135,4136],{},"--cap-drop ALL --cap-add NET_BIND_SERVICE",[72,4138,4139,4140,4143],{},"能力裁剪；",[19,4141,4142],{},"--security-opt no-new-privileges"," 禁止提权（第十五章）",[51,4145,4146,4148,4153],{},[72,4147],{},[72,4149,4150],{},[19,4151,4152],{},"--init",[72,4154,4155],{},"用 tini 当 PID 1，转发信号、回收僵尸进程",[51,4157,4158,4160,4165],{},[72,4159],{},[72,4161,4162],{},[19,4163,4164],{},"--entrypoint sh",[72,4166,4167],{},"覆盖 ENTRYPOINT 来调试（第七章）",[40,4169,4171],{"id":4170},"docker-ps-a-的-status-怎么读","docker ps -a 的 STATUS 怎么读",[45,4173,4174,4187],{},[48,4175,4176],{},[51,4177,4178,4181,4184],{},[54,4179,4180],{},"STATUS 片段",[54,4182,4183],{},"含义",[54,4185,4186],{},"下一步查什么",[67,4188,4189,4202,4217,4234,4250,4269,4282,4300,4316,4337,4364,4378,4397],{},[51,4190,4191,4196,4199],{},[72,4192,4193],{},[19,4194,4195],{},"Up 3 hours",[72,4197,4198],{},"正常运行 3 小时",[72,4200,4201],{},"—",[51,4203,4204,4209,4212],{},[72,4205,4206],{},[19,4207,4208],{},"Up 3 hours (healthy)",[72,4210,4211],{},"健康检查通过",[72,4213,4214,4215],{},"第十六章 ",[19,4216,2502],{},[51,4218,4219,4224,4231],{},[72,4220,4221],{},[19,4222,4223],{},"Up 3 hours (health: starting)",[72,4225,4226,4227,4230],{},"还在 ",[19,4228,4229],{},"--start-period"," 内",[72,4232,4233],{},"连续通过 retries 次才转 healthy",[51,4235,4236,4241,4244],{},[72,4237,4238],{},[19,4239,4240],{},"Up 3 hours (unhealthy)",[72,4242,4243],{},"检查连续失败",[72,4245,4246,4249],{},[31,4247,4248],{},"容器还在跑","但服务不可用；Docker 不会自动重启它",[51,4251,4252,4257,4260],{},[72,4253,4254],{},[19,4255,4256],{},"Restarting (1) 5 seconds ago",[72,4258,4259],{},"崩溃后被重启策略反复拉起",[72,4261,4262,716,4265,4268],{},[19,4263,4264],{},"docker logs --tail 50",[19,4266,4267],{},"1"," 是退出码",[51,4270,4271,4276,4279],{},[72,4272,4273],{},[19,4274,4275],{},"Exited (0) 2 hours ago",[72,4277,4278],{},"正常退出",[72,4280,4281],{},"服务型容器退 0 多半是「前台命令跑完了」，检查 CMD",[51,4283,4284,4289,4292],{},[72,4285,4286],{},[19,4287,4288],{},"Exited (1) 5 minutes ago",[72,4290,4291],{},"应用自身报错",[72,4293,4294,564,4297,4299],{},[19,4295,4296],{},"logs",[19,4298,4164],{},"（第七章三步法）",[51,4301,4302,4307,4310],{},[72,4303,4304],{},[19,4305,4306],{},"Exited (126)",[72,4308,4309],{},"命令不可执行（权限\u002F不是可执行文件）",[72,4311,4312,4313],{},"检查 ENTRYPOINT 指向的文件与 ",[19,4314,4315],{},"+x",[51,4317,4318,4323,4326],{},[72,4319,4320],{},[19,4321,4322],{},"Exited (127)",[72,4324,4325],{},"命令找不到（路径错\u002F缺动态库）",[72,4327,4328,4330,4331,114,4334],{},[19,4329,4164],{}," 进去 ",[19,4332,4333],{},"ls",[19,4335,4336],{},"ldd",[51,4338,4339,4344,4354],{},[72,4340,4341,4343],{},[19,4342,398],{}," = 128+9",[72,4345,4346,4347],{},"被 SIGKILL：",[31,4348,4349,4350,4353],{},"OOM、",[19,4351,4352],{},"docker kill","、或 stop 超时",[72,4355,4356,4359,4360,4363],{},[19,4357,4358],{},"inspect"," 看 ",[19,4361,4362],{},"OOMKilled","（第十四章）",[51,4365,4366,4372,4375],{},[72,4367,4368,4371],{},[19,4369,4370],{},"Exited (143)"," = 128+15",[72,4373,4374],{},"被 SIGTERM 终止（没自己处理）",[72,4376,4377],{},"优雅停机没做好（第十六章）",[51,4379,4380,4385,4391],{},[72,4381,4382],{},[19,4383,4384],{},"Created",[72,4386,4387,4388,4390],{},"只 ",[19,4389,891],{}," 过，从未成功启动",[72,4392,4393,4396],{},[19,4394,4395],{},"docker start"," 看它报什么",[51,4398,4399,4404,4407],{},[72,4400,4401],{},[19,4402,4403],{},"Dead",[72,4405,4406],{},"清理时出错，异常状态",[72,4408,4409,4412],{},[19,4410,4411],{},"docker rm -f","，并查内核日志",[15,4414,4415,4416,616,4419,4422,4423,4426,4427,4429],{},"退出码只需记一条：",[31,4417,4418],{},"128 以上是「被信号 X 杀死」，X = 码 − 128",[19,4420,4421],{},"137−128=9","（KILL）、",[19,4424,4425],{},"143−128=15","（TERM）。看到 ",[19,4428,398],{}," 先怀疑资源限制，别急着改业务代码。",[40,4431,4432],{"id":4432},"观察一个正在跑的容器",[1158,4434,4436],{"className":1160,"code":4435,"language":1162,"meta":1163,"style":1163},"docker logs -f --tail 100 --since 30m shop-api     # -f 跟随，-t\u002F--timestamps 加时间戳\ndocker stats --no-stream                            # 一次快照：CPU%、MEM USAGE\u002FLIMIT、NET、BLK IO、PIDS\ndocker top shop-api                                 # 宿主视角看容器进程（真实 PID 与 USER）\ndocker diff shop-web                                # 可写层里增(A)\u002F改(C)\u002F删(D) 的文件\ndocker events --since 30m --filter event=die        # 生命周期事件\n",[19,4437,4438,4465,4478,4490,4502],{"__ignoreMap":1163},[1167,4439,4440,4442,4445,4447,4450,4453,4456,4459,4462],{"class":1169,"line":1170},[1167,4441,559],{"class":1173},[1167,4443,4444],{"class":1183}," logs",[1167,4446,2719],{"class":2703},[1167,4448,4449],{"class":2703}," --tail",[1167,4451,4452],{"class":2703}," 100",[1167,4454,4455],{"class":2703}," --since",[1167,4457,4458],{"class":1183}," 30m",[1167,4460,4461],{"class":1183}," shop-api",[1167,4463,4464],{"class":3604},"     # -f 跟随，-t\u002F--timestamps 加时间戳\n",[1167,4466,4467,4469,4472,4475],{"class":1169,"line":1177},[1167,4468,559],{"class":1173},[1167,4470,4471],{"class":1183}," stats",[1167,4473,4474],{"class":2703}," --no-stream",[1167,4476,4477],{"class":3604},"                            # 一次快照：CPU%、MEM USAGE\u002FLIMIT、NET、BLK IO、PIDS\n",[1167,4479,4480,4482,4485,4487],{"class":1169,"line":1202},[1167,4481,559],{"class":1173},[1167,4483,4484],{"class":1183}," top",[1167,4486,4461],{"class":1183},[1167,4488,4489],{"class":3604},"                                 # 宿主视角看容器进程（真实 PID 与 USER）\n",[1167,4491,4492,4494,4497,4499],{"class":1169,"line":1208},[1167,4493,559],{"class":1173},[1167,4495,4496],{"class":1183}," diff",[1167,4498,3598],{"class":1183},[1167,4500,4501],{"class":3604},"                                # 可写层里增(A)\u002F改(C)\u002F删(D) 的文件\n",[1167,4503,4504,4506,4509,4511,4513,4516,4519],{"class":1169,"line":1238},[1167,4505,559],{"class":1173},[1167,4507,4508],{"class":1183}," events",[1167,4510,4455],{"class":2703},[1167,4512,4458],{"class":1183},[1167,4514,4515],{"class":2703}," --filter",[1167,4517,4518],{"class":1183}," event=die",[1167,4520,4521],{"class":3604},"        # 生命周期事件\n",[15,4523,4524],{},"各自的用处：",[1318,4526,4527,4540,4557,4566],{},[584,4528,4529,4531,4532,4535,4536,4539],{},[19,4530,4296],{}," 读的是容器的 ",[31,4533,4534],{},"stdout\u002Fstderr","，不是容器里的文件。应用若把日志写进容器内文件，",[19,4537,4538],{},"docker logs"," 永远空白——这就是 12-factor 要求「日志打到标准输出」的原因（第十三章）。",[584,4541,4542,951,4545,4548,4549,4552,4553,4556],{},[19,4543,4544],{},"stats",[19,4546,4547],{},"MEM USAGE \u002F LIMIT","，LIMIT 是 ",[31,4550,4551],{},"cgroup 上限","而不是宿主内存，百分比按上限算——判断「是不是被限制死了」的最快入口。",[19,4554,4555],{},"--no-stream"," 只采一次，适合脚本。",[584,4558,4559,4562,4563,4565],{},[19,4560,4561],{},"top"," 解决「容器里跑着 java 但镜像没装 ",[19,4564,325],{},"」的尴尬（第七章）。",[584,4567,4568,4571,4572,4574,4575,4578],{},[19,4569,4570],{},"diff"," 是个便宜的侦探：让容器跑一轮业务再看 ",[19,4573,4570],{},"，就知道该挂哪个卷、",[19,4576,4577],{},"--read-only"," 会挡住哪些路径。",[15,4580,4581,4582,4584,4585,4588,4589,616],{},"示例 06 把这套观察串成一次真实的排查流：从 ",[19,4583,869],{}," 里那行 ",[19,4586,4587],{},"Restarting (1)"," 开始，逐步定位到 ",[19,4590,398],{},[1405,4592],{"height":4593,"src":4594,"title":4595},580,"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F06-lifecycle-status.html","示例 06：生命周期与 STATUS——把状态字符串读成结论",[15,4597,4598],{},"生命周期搞清楚了，下一章学「进去看」——以及当镜像里什么都没有时怎么调试。",[10,4600,4602],{"id":4601},"七进入容器与调试execinspect-与镜像里什么都没有","七、进入容器与调试：exec、inspect 与「镜像里什么都没有」",[15,4604,4605,4606,4609],{},"调试容器的心法只有一句：",[31,4607,4608],{},"容器就是一个进程，进不去就用外部的眼睛看","。这一章把两种眼睛都给你。",[40,4611,4613,4614,4617],{"id":4612},"docker-exec-与-的坑","docker exec 与 ",[19,4615,4616],{},"--"," 的坑",[1158,4619,4621],{"className":1160,"code":4620,"language":1162,"meta":1163,"style":1163},"docker exec -it shop-api sh          # 最常用：alpine\u002Fbusybox 只有 sh；Debian 系镜像才有 bash\ndocker exec -u root -it shop-api sh  # 镜像里 USER 非 root 时临时提权\ndocker exec shop-api cat \u002Fetc\u002Fos-release        # 非交互，适合脚本\ndocker exec shop-api ls -- -p                   # 文件名以 - 开头时必须用 -- 分隔\n",[19,4622,4623,4641,4662,4679],{"__ignoreMap":1163},[1167,4624,4625,4627,4630,4633,4635,4638],{"class":1169,"line":1170},[1167,4626,559],{"class":1173},[1167,4628,4629],{"class":1183}," exec",[1167,4631,4632],{"class":2703}," -it",[1167,4634,4461],{"class":1183},[1167,4636,4637],{"class":1183}," sh",[1167,4639,4640],{"class":3604},"          # 最常用：alpine\u002Fbusybox 只有 sh；Debian 系镜像才有 bash\n",[1167,4642,4643,4645,4647,4650,4653,4655,4657,4659],{"class":1169,"line":1177},[1167,4644,559],{"class":1173},[1167,4646,4629],{"class":1183},[1167,4648,4649],{"class":2703}," -u",[1167,4651,4652],{"class":1183}," root",[1167,4654,4632],{"class":2703},[1167,4656,4461],{"class":1183},[1167,4658,4637],{"class":1183},[1167,4660,4661],{"class":3604},"  # 镜像里 USER 非 root 时临时提权\n",[1167,4663,4664,4666,4668,4670,4673,4676],{"class":1169,"line":1202},[1167,4665,559],{"class":1173},[1167,4667,4629],{"class":1183},[1167,4669,4461],{"class":1183},[1167,4671,4672],{"class":1183}," cat",[1167,4674,4675],{"class":1183}," \u002Fetc\u002Fos-release",[1167,4677,4678],{"class":3604},"        # 非交互，适合脚本\n",[1167,4680,4681,4683,4685,4687,4690,4693,4696],{"class":1169,"line":1208},[1167,4682,559],{"class":1173},[1167,4684,4629],{"class":1183},[1167,4686,4461],{"class":1183},[1167,4688,4689],{"class":1183}," ls",[1167,4691,4692],{"class":2703}," --",[1167,4694,4695],{"class":2703}," -p",[1167,4697,4698],{"class":3604},"                   # 文件名以 - 开头时必须用 -- 分隔\n",[15,4700,4701,4702,4704,4705,793,4708,4711,4712,4715],{},"为什么很多镜像没有 ",[19,4703,1162],{},"？因为基础镜像是 alpine\u002Fbusybox（体积小、攻击面小），甚至 ",[19,4706,4707],{},"distroless",[19,4709,4710],{},"scratch"," 连 ",[19,4713,4714],{},"sh"," 都没有。这不是作者恶意，而是第十五章的取舍结果。遇到没 shell 的镜像，看本章后半部分。",[15,4717,4718,4720,4721,616,4724,4727,4728,4730,4731,4734,4735,4737,4738,1372,4741,114,4744,4747],{},[19,4719,4616],{}," 的规则只有一条：",[31,4722,4723],{},"「选项解析到此为止」",[19,4725,4726],{},"docker exec -it c -- node -v"," 里 ",[19,4729,4616],{}," 之后全部交给容器；",[19,4732,4733],{},"docker run --rm img -- --version"," 里 docker 会把第二个 ",[19,4736,4616],{}," 之后的内容当成「镜像自己的参数」，行为和你预期不同。当你要传的参数本身带 ",[19,4739,4740],{},"-",[19,4742,4743],{},"-p",[19,4745,4746],{},"--version","），不加分隔符时 docker 会先把它当成自己的选项，报出的错误通常很难看。",[40,4749,4751],{"id":4750},"inspect用-format-只要你要的字段","inspect：用 --format 只要你要的字段",[15,4753,4754,4757,4758,4761],{},[19,4755,4756],{},"docker inspect"," 一次吐几 KB JSON，硬读会累死，",[19,4759,4760],{},"--format"," 才是正解：",[1158,4763,4765],{"className":1160,"code":4764,"language":1162,"meta":1163,"style":1163},"docker inspect shop-api --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'\ndocker inspect shop-api --format '{{json .Config.Env}}'      # 同样套路：{{json .Mounts}}、.State.*\ndocker inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} health={{.State.Health.Status}}' shop-api shop-db\ndocker ps --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}'   # ps 同样支持，脚本神器\n",[19,4766,4767,4781,4797,4813],{"__ignoreMap":1163},[1167,4768,4769,4771,4773,4775,4778],{"class":1169,"line":1170},[1167,4770,559],{"class":1173},[1167,4772,3370],{"class":1183},[1167,4774,4461],{"class":1183},[1167,4776,4777],{"class":2703}," --format",[1167,4779,4780],{"class":1183}," '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'\n",[1167,4782,4783,4785,4787,4789,4791,4794],{"class":1169,"line":1177},[1167,4784,559],{"class":1173},[1167,4786,3370],{"class":1183},[1167,4788,4461],{"class":1183},[1167,4790,4777],{"class":2703},[1167,4792,4793],{"class":1183}," '{{json .Config.Env}}'",[1167,4795,4796],{"class":3604},"      # 同样套路：{{json .Mounts}}、.State.*\n",[1167,4798,4799,4801,4803,4805,4808,4810],{"class":1169,"line":1202},[1167,4800,559],{"class":1173},[1167,4802,3370],{"class":1183},[1167,4804,4777],{"class":2703},[1167,4806,4807],{"class":1183}," '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} health={{.State.Health.Status}}'",[1167,4809,4461],{"class":1183},[1167,4811,4812],{"class":1183}," shop-db\n",[1167,4814,4815,4817,4820,4822,4825],{"class":1169,"line":1208},[1167,4816,559],{"class":1173},[1167,4818,4819],{"class":1183}," ps",[1167,4821,4777],{"class":2703},[1167,4823,4824],{"class":1183}," '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}'",[1167,4826,4827],{"class":3604},"   # ps 同样支持，脚本神器\n",[15,4829,4830,4832,4833,4836,4837,4840,4841,4844,4845,4848,4849,4852,4853,4856,4857,4860,4861,4863],{},[19,4831,4760],{}," 是 Go 模板：",[19,4834,4835],{},"{{.X}}"," 取值、",[19,4838,4839],{},"{{json .X}}"," 打印结构、",[19,4842,4843],{},"{{range}}"," 遍历、",[19,4846,4847],{},"{{if}}"," 条件。报 ",[19,4850,4851],{},"nil can't be fields"," 一般是路径写错（比如 ",[19,4854,4855],{},"NetworkSettings.Networks"," 是 map，得 ",[19,4858,4859],{},"range"," 进去），先原样 ",[19,4862,4358],{}," 一次看清结构再改模板。",[40,4865,4867],{"id":4866},"docker-cp双向搬文件","docker cp：双向搬文件",[15,4869,4870,4873,4874,4877],{},[19,4871,4872],{},"docker cp shop-api:\u002Fsrv\u002Fshop\u002Fconfig\u002Fapp.yaml .\u002Fapp.yaml"," 是容器 → 宿主，",[19,4875,4876],{},"docker cp .\u002Fnginx.conf shop-web:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fsite.conf"," 是宿主 → 容器。",[15,4879,4880,4881,4884,4885,4888,4889,4892,4893,4895],{},"用途是把配置塞进去做一次性试验、把日志与产物捞出来。两个限制：",[31,4882,4883],{},"运行中改的文件容器重建就没了","（所以它是调试手段，不是部署手段）；",[19,4886,4887],{},"cp"," 目录时的合并语义与 ",[19,4890,4891],{},"rsync"," 不同，路径末尾的 ",[19,4894,793],{}," 要写清楚。",[40,4897,4899],{"id":4898},"镜像里没有编辑器没有-ps没有-curl-怎么办","镜像里没有编辑器、没有 ps、没有 curl 怎么办",[15,4901,4902],{},"按代价从小到大三个方案：",[45,4904,4905,4921],{},[48,4906,4907],{},[51,4908,4909,4912,4915,4918],{},[54,4910,4911],{},"方案",[54,4913,4914],{},"做法",[54,4916,4917],{},"适合",[54,4919,4920],{},"代价",[67,4922,4923,4951,4970],{},[51,4924,4925,4928,4945,4948],{},[72,4926,4927],{},"从外部看",[72,4929,4930,114,4933,114,4935,4938,4939,114,4942],{},[19,4931,4932],{},"docker top",[19,4934,4538],{},[19,4936,4937],{},"docker cp","、宿主 ",[19,4940,4941],{},"\u002Fproc\u002F\u003Cpid>\u002F",[19,4943,4944],{},"nsenter",[72,4946,4947],{},"只想看状态与文件",[72,4949,4950],{},"需要宿主权限",[51,4952,4953,4956,4961,4964],{},[72,4954,4955],{},"临时装工具",[72,4957,4958],{},[19,4959,4960],{},"docker exec -it shop-api sh -c 'apk add --no-cache curl tcpdump'",[72,4962,4963],{},"一次性排查",[72,4965,4966,4967],{},"破坏「镜像不可变」，",[31,4968,4969],{},"只允许在开发\u002F临时环境",[51,4971,4972,4975,4986,4989],{},[72,4973,4974],{},"调试镜像",[72,4976,4977,4978,4981,4982,4985],{},"单独 ",[19,4979,4980],{},"Dockerfile.debug","（含 strace\u002Flsof\u002Fjq），或把 ",[19,4983,4984],{},"nicolaka\u002Fnetshoot"," 连进同一网络",[72,4987,4988],{},"反复排查网络与进程",[72,4990,4991],{},"多一份镜像要维护",[15,4993,4994],{},"从宿主直接看容器内部，是很值钱的一招（Linux 上）：",[1158,4996,4998],{"className":1160,"code":4997,"language":1162,"meta":1163,"style":1163},"PID=$(docker inspect --format '{{.State.Pid}}' shop-api)\nsudo ls -l \u002Fproc\u002F$PID\u002Froot\u002Fsrv\u002Fshop; sudo cat \u002Fproc\u002F$PID\u002Fcgroup   # 容器的根文件系统 \u002F 它归属哪个 cgroup\nsudo nsenter -t $PID -n ss -lntp                                 # 进入它的 net namespace 看监听端口\nsudo nsenter -t $PID -m -p -- cat \u002Fproc\u002F1\u002Fenviron | tr '\\0' '\\n'\n",[19,4999,5000,5025,5059,5083],{"__ignoreMap":1163},[1167,5001,5002,5005,5008,5011,5013,5015,5017,5020,5022],{"class":1169,"line":1170},[1167,5003,5004],{"class":1216},"PID",[1167,5006,5007],{"class":3505},"=",[1167,5009,5010],{"class":1216},"$(",[1167,5012,559],{"class":1173},[1167,5014,3370],{"class":1183},[1167,5016,4777],{"class":2703},[1167,5018,5019],{"class":1183}," '{{.State.Pid}}'",[1167,5021,4461],{"class":1183},[1167,5023,5024],{"class":1216},")\n",[1167,5026,5027,5029,5031,5034,5037,5040,5043,5045,5047,5049,5051,5053,5056],{"class":1169,"line":1177},[1167,5028,905],{"class":1173},[1167,5030,4689],{"class":1183},[1167,5032,5033],{"class":2703}," -l",[1167,5035,5036],{"class":1183}," \u002Fproc\u002F",[1167,5038,5039],{"class":1216},"$PID",[1167,5041,5042],{"class":1183},"\u002Froot\u002Fsrv\u002Fshop",[1167,5044,3629],{"class":1216},[1167,5046,905],{"class":1173},[1167,5048,4672],{"class":1183},[1167,5050,5036],{"class":1183},[1167,5052,5039],{"class":1216},[1167,5054,5055],{"class":1183},"\u002Fcgroup",[1167,5057,5058],{"class":3604},"   # 容器的根文件系统 \u002F 它归属哪个 cgroup\n",[1167,5060,5061,5063,5066,5068,5071,5074,5077,5080],{"class":1169,"line":1202},[1167,5062,905],{"class":1173},[1167,5064,5065],{"class":1183}," nsenter",[1167,5067,2704],{"class":2703},[1167,5069,5070],{"class":1216}," $PID ",[1167,5072,5073],{"class":2703},"-n",[1167,5075,5076],{"class":1183}," ss",[1167,5078,5079],{"class":2703}," -lntp",[1167,5081,5082],{"class":3604},"                                 # 进入它的 net namespace 看监听端口\n",[1167,5084,5085,5087,5089,5091,5093,5095,5097,5099,5101,5104,5106,5109,5112],{"class":1169,"line":1208},[1167,5086,905],{"class":1173},[1167,5088,5065],{"class":1183},[1167,5090,2704],{"class":2703},[1167,5092,5070],{"class":1216},[1167,5094,4072],{"class":2703},[1167,5096,4695],{"class":2703},[1167,5098,4692],{"class":2703},[1167,5100,4672],{"class":1183},[1167,5102,5103],{"class":1183}," \u002Fproc\u002F1\u002Fenviron",[1167,5105,3506],{"class":3505},[1167,5107,5108],{"class":1173}," tr",[1167,5110,5111],{"class":1183}," '\\0'",[1167,5113,5114],{"class":1183}," '\\n'\n",[15,5116,5117,5119,5120,616],{},[19,5118,4944],{}," 借用容器的 namespace，看到的端口、路由、文件都是容器视角，而工具装在宿主上——",[31,5121,5122],{},"镜像里什么都没有也能查",[40,5124,5126],{"id":5125},"起来就退出-entrypoint-sh-是唯一可靠的入口","起来就退出：--entrypoint sh 是唯一可靠的入口",[15,5128,5129,5130,5133,5134,5136,5137,906],{},"容器 ",[19,5131,5132],{},"Exited (1)"," 且没有日志时，你连 ",[19,5135,3734],{}," 的机会都没有（它已经不跑了）。正确做法是",[31,5138,5139],{},"把主命令换成 shell，再手工跑原来那条",[1158,5141,5143],{"className":1160,"code":5142,"language":1162,"meta":1163,"style":1163},"docker run --rm -it --entrypoint sh ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n\u002Fsrv\u002Fshop $ node server.js        # 错误现在直接打到你的终端上\n\u002Fsrv\u002Fshop $ env | sort; id; ls -l .\n",[19,5144,5145,5162,5178],{"__ignoreMap":1163},[1167,5146,5147,5149,5151,5153,5155,5158,5160],{"class":1169,"line":1170},[1167,5148,559],{"class":1173},[1167,5150,3380],{"class":1183},[1167,5152,3386],{"class":2703},[1167,5154,4632],{"class":2703},[1167,5156,5157],{"class":2703}," --entrypoint",[1167,5159,4637],{"class":1183},[1167,5161,3430],{"class":1183},[1167,5163,5164,5167,5170,5172,5175],{"class":1169,"line":1177},[1167,5165,5166],{"class":1173},"\u002Fsrv\u002Fshop",[1167,5168,5169],{"class":1216}," $ ",[1167,5171,2048],{"class":1183},[1167,5173,5174],{"class":1183}," server.js",[1167,5176,5177],{"class":3604},"        # 错误现在直接打到你的终端上\n",[1167,5179,5180,5182,5184,5187,5189,5192,5194,5196,5198,5200,5202],{"class":1169,"line":1202},[1167,5181,5166],{"class":1173},[1167,5183,5169],{"class":1216},[1167,5185,5186],{"class":1183},"env",[1167,5188,3506],{"class":3505},[1167,5190,5191],{"class":1173}," sort",[1167,5193,3629],{"class":1216},[1167,5195,917],{"class":1173},[1167,5197,3629],{"class":1216},[1167,5199,4333],{"class":1173},[1167,5201,5033],{"class":2703},[1167,5203,2710],{"class":1183},[15,5205,5206,5207,5210,5211,5213,5214,114,5216,5218],{},"三步定位法（第十八章的排障表也按它走）：",[19,5208,5209],{},"docker logs --tail 50 \u003Cname>"," → 没输出就 ",[19,5212,4164],{}," 手工启动 → 怀疑环境差异就在 shell 里核对 ",[19,5215,5186],{},[19,5217,917],{},"、路径与权限。",[40,5220,5222],{"id":5221},"docker-events谁在什么时候动了什么","docker events：谁在什么时候动了什么",[15,5224,5225,5228,5229,616],{},[19,5226,5227],{},"docker events --since 30m --until 10m --filter type=container","，或者按事件与标签收窄：",[19,5230,5231],{},"docker events --filter 'event=die' --filter 'label=app=shop-api'",[15,5233,5234,5237,5238,939,5241,793,5244,5247,5248,5250,5251,5253,5254,5256,5257,5260],{},[19,5235,5236],{},"die"," 事件带 ",[19,5239,5240],{},"exitCode",[19,5242,5243],{},"--since",[19,5245,5246],{},"--until"," 支持相对时间。它能回答「谁杀了我的容器」：如果 ",[19,5249,5236],{}," 前面有 ",[19,5252,3714],{}," 事件，那是人为或脚本发的信号；只有一个 ",[19,5255,5236],{}," 且 ",[19,5258,5259],{},"OOMKilled=true","，那是内核干的（第十四章）。",[40,5262,5264],{"id":5263},"为什么容器里通常没有-systemd","为什么容器里通常没有 systemd",[15,5266,5267],{},"因为容器不是虚拟机。systemd 的职责是管理一整台机器（init、服务、cgroup、日志、登录会话），而容器的定位是「一个进程 + 它需要的东西」：",[1318,5269,5270,5273,5289],{},[584,5271,5272],{},"PID 1 应该留给你的应用；给了 systemd，信号、退出码、日志转发全乱（第十六章的信号问题会加倍）。",[584,5274,5275,5276,5279,5280,5283,5284,4727,5286,5288],{},"systemd 要写 cgroup 树、访问 ",[19,5277,5278],{},"\u002Fsys\u002Ffs\u002Fcgroup","、可能需要 ",[19,5281,5282],{},"CAP_SYS_ADMIN","——在共享内核的容器里做这些，等于和宿主的管理者（dockerd、kubelet）抢 cgroup。这就是第二章 ",[19,5285,677],{},[19,5287,727],{}," 必须统一的深层原因。",[584,5290,5291,5292,5295],{},"需要多个服务时的答案是",[31,5293,5294],{},"多容器 + Compose","（第十一章），不是在一个容器里跑五个 daemon。",[15,5297,5298,5299,5302],{},"确有特例（跑完整发行版做系统级测试），社区做法是带 systemd 的特权镜像，形如 ",[19,5300,5301],{},"--cgroupns=host --tmpfs \u002Frun --tmpfs \u002Frun\u002Flock -v \u002Fsys\u002Ffs\u002Fcgroup:\u002Fsys\u002Ffs\u002Fcgroup:ro","。生产里把它当代码异味看待。",[15,5304,5305,5306,114,5308,114,5311,114,5313,114,5315,5317],{},"示例 07 把 ",[19,5307,3734],{},[19,5309,5310],{},"inspect --format",[19,5312,4561],{},[19,5314,4887],{},[19,5316,2150],{}," 串成一次完整排查演练，全部输出是真实样本。",[1405,5319],{"height":5320,"src":5321,"title":5322},600,"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F07-exec-debug.html","示例 07：进入容器与调试——exec \u002F inspect \u002F cp \u002F entrypoint",[15,5324,5325],{},"调试能力有了，下一章处理运行侧最容易出事的一块：网络。",[10,5327,5329],{"id":5328},"八端口与网络bridge自定义网络与那个会变-ip-的坑","八、端口与网络：bridge、自定义网络与那个会变 IP 的坑",[15,5331,5332],{},"网络是 Docker 里概念最密的一块，因为在 Linux 上网络本来就是内核子系统。路线：先讲驱动有哪些，再讲默认网桥为什么不适合生产，然后是自定义网络的 DNS，最后是端口与暴露边界。",[40,5334,5335],{"id":5335},"五种网络驱动与取舍",[45,5337,5338,5353],{},[48,5339,5340],{},[51,5341,5342,5345,5348,5350],{},[54,5343,5344],{},"驱动",[54,5346,5347],{},"机制",[54,5349,2205],{},[54,5351,5352],{},"代价与限制",[67,5354,5355,5372,5390,5406,5425],{},[51,5356,5357,5363,5366,5369],{},[72,5358,5359,5362],{},[19,5360,5361],{},"bridge","（默认）",[72,5364,5365],{},"Linux bridge + veth + NAT",[72,5367,5368],{},"本地开发、单机多容器",[72,5370,5371],{},"默认网络无 DNS、有 NAT 开销",[51,5373,5374,5378,5381,5384],{},[72,5375,5376],{},[19,5377,4022],{},[72,5379,5380],{},"容器直接用宿主网络栈",[72,5382,5383],{},"追求极致性能、需要看宿主网卡",[72,5385,5386,5389],{},[31,5387,5388],{},"直接占用宿主端口","、与其他容器 hostname 混在一起、Mac\u002FWindows 无效",[51,5391,5392,5397,5400,5403],{},[72,5393,5394],{},[19,5395,5396],{},"none",[72,5398,5399],{},"只有 lo 回环",[72,5401,5402],{},"纯计算任务、彻底断网",[72,5404,5405],{},"完全没网络",[51,5407,5408,5413,5416,5419],{},[72,5409,5410],{},[19,5411,5412],{},"overlay",[72,5414,5415],{},"VXLAN 隧道跨宿主",[72,5417,5418],{},"Swarm 多机服务",[72,5420,5421,5424],{},[31,5422,5423],{},"只有 Swarm 才用得上","（第十七章）",[51,5426,5427,5435,5438,5441],{},[72,5428,5429,728,5432],{},[19,5430,5431],{},"macvlan",[19,5433,5434],{},"ipvlan",[72,5436,5437],{},"容器在局域网里拿到独立 MAC\u002FIP",[72,5439,5440],{},"需要以「独立网络设备」身份存在",[72,5442,5443],{},"与宿主通信受阻、交换机端口安全\u002F802.1X 会拒、AP 隔离网络无效",[15,5445,5446,5447,5450],{},"选择顺序基本是：",[31,5448,5449],{},"自定义 bridge（单机首选） → host（明确要性能或要宿主栈） → macvlan（明确要求二层可达） → overlay（跨机且用 Swarm）","。「默认 bridge」不在可选之列，理由见下一节。",[40,5452,5454],{"id":5453},"默认网桥-docker0-的真实行为","默认网桥 docker0 的真实行为",[15,5456,5457,5458,5461,5462,5465,5466,5469],{},"装完 Docker，宿主上会出现 ",[19,5459,5460],{},"docker0"," 网桥，默认网段 ",[19,5463,5464],{},"172.17.0.0\u002F16","。不加 ",[19,5467,5468],{},"--network"," 的容器都接在上面。它有两个坑：",[581,5471,5472,5490],{},[584,5473,5474,5477,5478,5481,5482,5485,5486,5489],{},[31,5475,5476],{},"没有基于名字的 DNS","。默认网络里容器只能靠 IP 互相访问，而 IP 每次重启都可能变：",[19,5479,5480],{},"docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' shop-api"," 今天拿到 ",[19,5483,5484],{},"172.17.0.2","，重启后可能变成 ",[19,5487,5488],{},"172.17.0.3","——任何把 IP 写进配置的用法都是定时炸弹。",[584,5491,5492,1372,5495,5498],{},[31,5493,5494],{},"同网段一律互通",[19,5496,5497],{},"icc=true","），且所有容器混在一张广播域里，噪音与端口冲突随容器数量增长。",[15,5500,5501,5502,5505],{},"结论：",[31,5503,5504],{},"生产与多容器开发一律用自定义网络","，默认 bridge 只适合「临时跑一个孤立容器」。",[40,5507,5509],{"id":5508},"自定义网络与-dns-解析","自定义网络与 DNS 解析",[1158,5511,5513],{"className":1160,"code":5512,"language":1162,"meta":1163,"style":1163},"docker network create --driver bridge --subnet 172.28.0.0\u002F16 --gateway 172.28.0.1 shop-net\ndocker network connect shop-net shop-api     # 断开是 network disconnect；network inspect 看细节\ndocker network ls; docker network rm shop-net\n",[19,5514,5515,5544,5561],{"__ignoreMap":1163},[1167,5516,5517,5519,5522,5524,5526,5529,5532,5535,5538,5541],{"class":1169,"line":1170},[1167,5518,559],{"class":1173},[1167,5520,5521],{"class":1183}," network",[1167,5523,3346],{"class":1183},[1167,5525,3355],{"class":2703},[1167,5527,5528],{"class":1183}," bridge",[1167,5530,5531],{"class":2703}," --subnet",[1167,5533,5534],{"class":1183}," 172.28.0.0\u002F16",[1167,5536,5537],{"class":2703}," --gateway",[1167,5539,5540],{"class":2703}," 172.28.0.1",[1167,5542,5543],{"class":1183}," shop-net\n",[1167,5545,5546,5548,5550,5553,5556,5558],{"class":1169,"line":1177},[1167,5547,559],{"class":1173},[1167,5549,5521],{"class":1183},[1167,5551,5552],{"class":1183}," connect",[1167,5554,5555],{"class":1183}," shop-net",[1167,5557,4461],{"class":1183},[1167,5559,5560],{"class":3604},"     # 断开是 network disconnect；network inspect 看细节\n",[1167,5562,5563,5565,5567,5569,5571,5573,5575,5577],{"class":1169,"line":1202},[1167,5564,559],{"class":1173},[1167,5566,5521],{"class":1183},[1167,5568,4689],{"class":1183},[1167,5570,3629],{"class":1216},[1167,5572,559],{"class":1173},[1167,5574,5521],{"class":1183},[1167,5576,1196],{"class":1183},[1167,5578,5543],{"class":1183},[15,5580,5581,5582,5585,5586,5589,5590,5593,5594,5597],{},"自定义网络里，",[31,5583,5584],{},"容器名就是主机名","，可以直接 ",[19,5587,5588],{},"curl http:\u002F\u002Fshop-api:3000","。原理是每个网络内嵌一个 DNS 服务器（容器里 ",[19,5591,5592],{},"cat \u002Fetc\u002Fresolv.conf"," 会看到 ",[19,5595,5596],{},"nameserver 127.0.0.11","），它把容器名与网络别名解析成当前 IP——IP 变了名字不变，配置不用改。这就是「不要靠 IP，要靠名字」的技术依据。",[1158,5599,5601],{"className":1160,"code":5600,"language":1162,"meta":1163,"style":1163},"docker run -d --name shop-api --network shop-net --network-alias api ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker exec shop-web curl -s http:\u002F\u002Fapi:3000\u002Fhealthz        # 别名同样可用\ndocker exec shop-api getent hosts shop-db                    # 看 DNS 实际解析到什么\n",[19,5602,5603,5629,5648],{"__ignoreMap":1163},[1167,5604,5605,5607,5609,5612,5614,5616,5619,5621,5624,5627],{"class":1169,"line":1170},[1167,5606,559],{"class":1173},[1167,5608,3380],{"class":1183},[1167,5610,5611],{"class":2703}," -d",[1167,5613,3349],{"class":2703},[1167,5615,4461],{"class":1183},[1167,5617,5618],{"class":2703}," --network",[1167,5620,5555],{"class":1183},[1167,5622,5623],{"class":2703}," --network-alias",[1167,5625,5626],{"class":1183}," api",[1167,5628,3430],{"class":1183},[1167,5630,5631,5633,5635,5637,5640,5642,5645],{"class":1169,"line":1177},[1167,5632,559],{"class":1173},[1167,5634,4629],{"class":1183},[1167,5636,3598],{"class":1183},[1167,5638,5639],{"class":1183}," curl",[1167,5641,3666],{"class":2703},[1167,5643,5644],{"class":1183}," http:\u002F\u002Fapi:3000\u002Fhealthz",[1167,5646,5647],{"class":3604},"        # 别名同样可用\n",[1167,5649,5650,5652,5654,5656,5659,5662,5665],{"class":1169,"line":1202},[1167,5651,559],{"class":1173},[1167,5653,4629],{"class":1183},[1167,5655,4461],{"class":1183},[1167,5657,5658],{"class":1183}," getent",[1167,5660,5661],{"class":1183}," hosts",[1167,5663,5664],{"class":1183}," shop-db",[1167,5666,5667],{"class":3604},"                    # 看 DNS 实际解析到什么\n",[15,5669,5670,5673,5674,5677,5678,5680],{},[19,5671,5672],{},"--link"," 是老写法（默认网络时代的补丁），现在",[31,5675,5676],{},"忘掉它","：它靠注入环境变量和改 ",[19,5679,4045],{}," 工作，行为隐晦、已废弃，Compose 里写了也不起作用。",[15,5682,5683,5686,5687,5690,5691,114,5694,114,5697,5700,5701,5704],{},[19,5684,5685],{},"network create"," 值得知道的开关：",[19,5688,5689],{},"--internal","（完全不出外网，只让容器间互通）、",[19,5692,5693],{},"--ipv6",[19,5695,5696],{},"--ip-range",[19,5698,5699],{},"--opt com.docker.network.driver.mtu=1400","（overlay\u002FVXLAN 必须减 MTU，否则出现「小请求正常、大请求卡死」的经典诡异现象）、",[19,5702,5703],{},"--attachable","（Swarm，第十七章）。",[40,5706,5708],{"id":5707},"端口映射四种写法与port-is-already-allocated","端口映射：四种写法与「port is already allocated」",[1158,5710,5712],{"className":1160,"code":5711,"language":1162,"meta":1163,"style":1163},"docker run -d -p 8080:80 shop-web             # 宿主 0.0.0.0:8080 → 容器 80\ndocker run -d -p 127.0.0.1:8080:80 shop-web   # 只监听回环（推荐的默认姿势）\ndocker run -d -p 8080 shop-web                # 宿主端口随机；`-p 80\u002Fudp` 则是显式指定协议\n",[19,5713,5714,5732,5750],{"__ignoreMap":1163},[1167,5715,5716,5718,5720,5722,5724,5727,5729],{"class":1169,"line":1170},[1167,5717,559],{"class":1173},[1167,5719,3380],{"class":1183},[1167,5721,5611],{"class":2703},[1167,5723,4695],{"class":2703},[1167,5725,5726],{"class":1183}," 8080:80",[1167,5728,3598],{"class":1183},[1167,5730,5731],{"class":3604},"             # 宿主 0.0.0.0:8080 → 容器 80\n",[1167,5733,5734,5736,5738,5740,5742,5745,5747],{"class":1169,"line":1177},[1167,5735,559],{"class":1173},[1167,5737,3380],{"class":1183},[1167,5739,5611],{"class":2703},[1167,5741,4695],{"class":2703},[1167,5743,5744],{"class":1183}," 127.0.0.1:8080:80",[1167,5746,3598],{"class":1183},[1167,5748,5749],{"class":3604},"   # 只监听回环（推荐的默认姿势）\n",[1167,5751,5752,5754,5756,5758,5760,5763,5765],{"class":1169,"line":1202},[1167,5753,559],{"class":1173},[1167,5755,3380],{"class":1183},[1167,5757,5611],{"class":2703},[1167,5759,4695],{"class":2703},[1167,5761,5762],{"class":2703}," 8080",[1167,5764,3598],{"class":1183},[1167,5766,5767],{"class":3604},"                # 宿主端口随机；`-p 80\u002Fudp` 则是显式指定协议\n",[15,5769,5770,5771,5774],{},"背后是 ",[19,5772,5773],{},"docker-proxy"," 进程 + iptables DNAT。因此端口不通的排查顺序是固定的四步：",[1158,5776,5778],{"className":1160,"code":5777,"language":1162,"meta":1163,"style":1163},"docker ps --format '{{.Names}} {{.Ports}}'                # ① 映射到底存在吗\ndocker exec shop-web sh -c 'ss -lntp || netstat -lntp'    # ② 容器里真的在听吗？听的是 0.0.0.0 吗\nsudo ss -lntp | grep 8080                                 # ③ 宿主上这个端口被谁占了\nsudo iptables -t nat -S DOCKER | grep 8080                # ④ NAT 规则真的写进去了吗\n",[19,5779,5780,5794,5813,5831],{"__ignoreMap":1163},[1167,5781,5782,5784,5786,5788,5791],{"class":1169,"line":1170},[1167,5783,559],{"class":1173},[1167,5785,4819],{"class":1183},[1167,5787,4777],{"class":2703},[1167,5789,5790],{"class":1183}," '{{.Names}} {{.Ports}}'",[1167,5792,5793],{"class":3604},"                # ① 映射到底存在吗\n",[1167,5795,5796,5798,5800,5802,5804,5807,5810],{"class":1169,"line":1177},[1167,5797,559],{"class":1173},[1167,5799,4629],{"class":1183},[1167,5801,3598],{"class":1183},[1167,5803,4637],{"class":1183},[1167,5805,5806],{"class":2703}," -c",[1167,5808,5809],{"class":1183}," 'ss -lntp || netstat -lntp'",[1167,5811,5812],{"class":3604},"    # ② 容器里真的在听吗？听的是 0.0.0.0 吗\n",[1167,5814,5815,5817,5819,5821,5823,5826,5828],{"class":1169,"line":1202},[1167,5816,905],{"class":1173},[1167,5818,5076],{"class":1183},[1167,5820,5079],{"class":2703},[1167,5822,3506],{"class":3505},[1167,5824,5825],{"class":1173}," grep",[1167,5827,5762],{"class":2703},[1167,5829,5830],{"class":3604},"                                 # ③ 宿主上这个端口被谁占了\n",[1167,5832,5833,5835,5838,5840,5843,5846,5849,5851,5853,5855],{"class":1169,"line":1208},[1167,5834,905],{"class":1173},[1167,5836,5837],{"class":1183}," iptables",[1167,5839,2704],{"class":2703},[1167,5841,5842],{"class":1183}," nat",[1167,5844,5845],{"class":2703}," -S",[1167,5847,5848],{"class":1183}," DOCKER",[1167,5850,3506],{"class":3505},[1167,5852,5825],{"class":1173},[1167,5854,5762],{"class":2703},[1167,5856,5857],{"class":3604},"                # ④ NAT 规则真的写进去了吗\n",[15,5859,5860,5861,5867,5868,5871,5872,5875],{},"第②步是新手最大的盲区：",[31,5862,5863,5864,5866],{},"应用只 bind ",[19,5865,1125],{}," 时，端口映射永远不会通","——从 veth 进来的包不是本地回环来源。所以「容器里的服务必须 bind ",[19,5869,5870],{},"0.0.0.0","」是硬性要求，而很多框架默认绑 ",[19,5873,5874],{},"localhost","（Uvicorn、部分 Spring 配置、老版 webpack devServer），必须显式改。",[15,5877,5878,5881,5882,5884,5885,5888,5889,5891],{},[19,5879,5880],{},"Error response from daemon: driver failed programming external connectivity ... port is already allocated"," 的三类原因：别的容器占了（",[19,5883,865],{}," 查）、宿主进程占了（",[19,5886,5887],{},"ss -lntp"," 查）、daemon 上次异常退出留下僵尸 ",[19,5890,5773],{},"（重启 docker 解决）。示例 08 是一次完整演练：从「连不上」一路查到「应用只 bind 了 127.0.0.1」。",[40,5893,5895],{"id":5894},"容器互访宿主访问容器","容器互访、宿主访问容器",[1318,5897,5898,5911,5924],{},[584,5899,5900,5903,5904,1372,5907,5910],{},[31,5901,5902],{},"容器 → 容器","：同一个自定义网络，用容器名\u002F别名 + ",[31,5905,5906],{},"容器内端口",[19,5908,5909],{},"http:\u002F\u002Fshop-db:5432","）。不要绕道宿主映射端口，也不要用 IP。",[584,5912,5913,906,5916,5919,5920,5923],{},[31,5914,5915],{},"宿主 → 容器",[19,5917,5918],{},"curl localhost:8080","（走映射）；Linux 上还能直接 ",[19,5921,5922],{},"curl 172.28.0.5:5432","（容器 IP，只用于调试）。",[584,5925,5926,5929,5930,5933],{},[31,5927,5928],{},"容器 → 宿主上的服务","：用 ",[19,5931,5932],{},"host.docker.internal","。Docker Desktop 自带该域名；Linux 上要手动加：",[1158,5935,5937],{"className":1160,"code":5936,"language":1162,"meta":1163,"style":1163},"docker run -d --name shop-api --add-host=host.docker.internal:host-gateway ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker exec shop-api curl -s http:\u002F\u002Fhost.docker.internal:5432\u002F\n",[19,5938,5939,5956],{"__ignoreMap":1163},[1167,5940,5941,5943,5945,5947,5949,5951,5954],{"class":1169,"line":1170},[1167,5942,559],{"class":1173},[1167,5944,3380],{"class":1183},[1167,5946,5611],{"class":2703},[1167,5948,3349],{"class":2703},[1167,5950,4461],{"class":1183},[1167,5952,5953],{"class":2703}," --add-host=host.docker.internal:host-gateway",[1167,5955,3430],{"class":1183},[1167,5957,5958,5960,5962,5964,5966,5968],{"class":1169,"line":1177},[1167,5959,559],{"class":1173},[1167,5961,4629],{"class":1183},[1167,5963,4461],{"class":1183},[1167,5965,5639],{"class":1183},[1167,5967,3666],{"class":2703},[1167,5969,5970],{"class":1183}," http:\u002F\u002Fhost.docker.internal:5432\u002F\n",[15,5972,5973,5976,5977,5984,5985,5988,5989,5991,5992,5994],{},[19,5974,5975],{},"host-gateway"," 是特殊值，让 Docker 自己填宿主在容器网络里的网关 IP，",[31,5978,5979,5980,5983],{},"比写死 ",[19,5981,5982],{},"172.17.0.1"," 可靠","。Compose 里对应 ",[19,5986,5987],{},"extra_hosts: [\"host.docker.internal:host-gateway\"]","（第十二章）。注意 macOS\u002FWindows 上容器里的 ",[19,5990,1125],{}," 不是你本机（第二章那台虚拟机），别拿 ",[19,5993,5874],{}," 指宿主。",[1318,5996,5997],{},[584,5998,5999,6004,6005,6007,6008,6010,6011,6014,6015,6017,6018,616],{},[31,6000,6001,6003],{},[19,6002,1024],{}," 为什么在 Mac 上不生效","：那个 host 是 Docker VM 的 host，不是你 Mac 的；表现为容器内部能通、外部访问不了、",[19,6006,4743],{}," 被忽略。Linux 上则真的共享栈：",[19,6009,5887],{}," 能直接看到容器进程，同时",[31,6012,6013],{},"端口冲突会打到宿主服务","——宿主跑着 nginx 又 ",[19,6016,4016],{}," 起一个 nginx，就是 ",[19,6019,6020],{},"address already in use",[40,6022,6023],{"id":6023},"暴露到公网的安全边界",[45,6025,6026,6039],{},[48,6027,6028],{},[51,6029,6030,6033,6036],{},[54,6031,6032],{},"写法",[54,6034,6035],{},"可达范围",[54,6037,6038],{},"评价",[67,6040,6041,6053,6067,6080],{},[51,6042,6043,6047,6050],{},[72,6044,6045],{},[19,6046,3882],{},[72,6048,6049],{},"所有网卡（含公网 IP）",[72,6051,6052],{},"只在真正对外服务时用",[51,6054,6055,6059,6062],{},[72,6056,6057],{},[19,6058,3902],{},[72,6060,6061],{},"仅本机",[72,6063,6064],{},[31,6065,6066],{},"数据库、管理面板、内部 API 的默认写法",[51,6068,6069,6074,6077],{},[72,6070,6071],{},[19,6072,6073],{},"-p 10.0.0.5:8080:80",[72,6075,6076],{},"指定网卡（如内网口）",[72,6078,6079],{},"内外网分流时用",[51,6081,6082,6088,6091],{},[72,6083,6084,6085,6087],{},"不写 ",[19,6086,4743],{},"，只在网络内互通",[72,6089,6090],{},"同网络容器",[72,6092,6093],{},"最稳妥的默认",[15,6095,6096,6097,6100,6101,793,6104,6107,6108,6111,6112,6115,6116,616],{},"还有一条只在 Docker 出现的经典事故：",[31,6098,6099],{},"iptables 让 ufw\u002Ffirewalld 的规则形同虚设","。Docker 在 ",[19,6102,6103],{},"PREROUTING",[19,6105,6106],{},"FORWARD"," 里插了自己的链，优先级高于 ufw，于是你 ",[19,6109,6110],{},"ufw deny 5432"," 之后公网照样连得上数据库。解法是把访问控制放进 ",[19,6113,6114],{},"DOCKER-USER"," 链（Docker 不会重写它），或者从一开始就不给容器绑 ",[19,6117,5870],{},[1405,6119],{"height":6120,"src":6121,"title":6122},620,"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F08-network.html","示例 08：网络与容器互访——网段、DNS 与端口排查",[15,6124,6125],{},"网络通了，数据该往哪儿放？下一章讲四种存储方式，以及「容器删了数据没了」这个事故是怎么发生的。",[10,6127,6129],{"id":6128},"九数据持久化匿名卷命名卷bind-mount-与-tmpfs","九、数据持久化：匿名卷、命名卷、bind mount 与 tmpfs",[40,6131,6133],{"id":6132},"四种存储先分清谁拥有这块空间","四种存储，先分清「谁拥有这块空间」",[15,6135,6136,6137,6139],{},"容器可写层随 ",[19,6138,151],{}," 消失，所以要持久化的数据必须放到容器文件系统之外。Docker 给了四条路：",[45,6141,6142,6160],{},[48,6143,6144],{},[51,6145,6146,6149,6152,6155,6157],{},[54,6147,6148],{},"类型",[54,6150,6151],{},"谁创建与管理",[54,6153,6154],{},"宿主上的位置",[54,6156,2205],{},[54,6158,6159],{},"容器删了数据还在吗",[67,6161,6162,6189,6210,6227],{},[51,6163,6164,6167,6174,6179,6182],{},[72,6165,6166],{},"匿名卷",[72,6168,6169,6170,6173],{},"Docker 自动建（",[19,6171,6172],{},"a1b2c3..."," 这种名字）",[72,6175,6176],{},[19,6177,6178],{},"\u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F\u003C随机名>\u002F_data",[72,6180,6181],{},"临时容器、忘了挂卷时的兜底",[72,6183,6184,6185,6188],{},"在（成为孤儿卷，",[19,6186,6187],{},"prune"," 才清）",[51,6190,6191,6194,6197,6202,6207],{},[72,6192,6193],{},"命名卷",[72,6195,6196],{},"你显式命名，或 Compose 声明",[72,6198,6199],{},[19,6200,6201],{},"\u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002Fshop_db-data\u002F_data",[72,6203,6204],{},[31,6205,6206],{},"数据库、需要备份与迁移的数据",[72,6208,6209],{},"在，且可预期",[51,6211,6212,6215,6218,6221,6224],{},[72,6213,6214],{},"bind mount",[72,6216,6217],{},"你指向宿主任意路径",[72,6219,6220],{},"就是那个路径本身",[72,6222,6223],{},"开发时挂载源码、挂配置文件",[72,6225,6226],{},"在（本来就在宿主）",[51,6228,6229,6232,6235,6238,6241],{},[72,6230,6231],{},"tmpfs",[72,6233,6234],{},"内存\u002Fswap，不落盘",[72,6236,6237],{},"内存",[72,6239,6240],{},"密钥、会话、临时上传缓冲",[72,6242,6243,6246],{},[31,6244,6245],{},"不在","（重启即清零）",[15,6248,6249,6250,6253],{},"选择依据只有一句：",[31,6251,6252],{},"要不要被 Docker 生命周期管理","。要 → 卷（跨机器一致、可备份、不受宿主目录权限影响）；要直接读写宿主上的真实文件（编辑即生效）→ bind mount；不能落盘 → tmpfs。",[40,6255,6257,3177,6260,6262],{"id":6256},"v-与-mount-语法对照",[19,6258,6259],{},"-v",[19,6261,2901],{}," 语法对照",[15,6264,6265],{},"两种写法能表达的集合不同：",[1158,6267,6269],{"className":1160,"code":6268,"language":1162,"meta":1163,"style":1163},"docker run -d --name shop-db -v db-data:\u002Fvar\u002Flib\u002Fmysql postgres:16.4\ndocker run -d --name shop-db --mount type=volume,source=db-data,target=\u002Fvar\u002Flib\u002Fmysql postgres:16.4\ndocker run -d --name shop-web -v \u002Fsrv\u002Fshop\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf:ro nginx:1.27-alpine\ndocker run -d --name shop-web --mount type=bind,source=\u002Fsrv\u002Fshop\u002Fnginx.conf,target=\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf,readonly nginx:1.27-alpine\n",[19,6270,6271,6292,6312,6332],{"__ignoreMap":1163},[1167,6272,6273,6275,6277,6279,6281,6283,6286,6289],{"class":1169,"line":1170},[1167,6274,559],{"class":1173},[1167,6276,3380],{"class":1183},[1167,6278,5611],{"class":2703},[1167,6280,3349],{"class":2703},[1167,6282,5664],{"class":1183},[1167,6284,6285],{"class":2703}," -v",[1167,6287,6288],{"class":1183}," db-data:\u002Fvar\u002Flib\u002Fmysql",[1167,6290,6291],{"class":1183}," postgres:16.4\n",[1167,6293,6294,6296,6298,6300,6302,6304,6307,6310],{"class":1169,"line":1177},[1167,6295,559],{"class":1173},[1167,6297,3380],{"class":1183},[1167,6299,5611],{"class":2703},[1167,6301,3349],{"class":2703},[1167,6303,5664],{"class":1183},[1167,6305,6306],{"class":2703}," --mount",[1167,6308,6309],{"class":1183}," type=volume,source=db-data,target=\u002Fvar\u002Flib\u002Fmysql",[1167,6311,6291],{"class":1183},[1167,6313,6314,6316,6318,6320,6322,6324,6326,6329],{"class":1169,"line":1202},[1167,6315,559],{"class":1173},[1167,6317,3380],{"class":1183},[1167,6319,5611],{"class":2703},[1167,6321,3349],{"class":2703},[1167,6323,3598],{"class":1183},[1167,6325,6285],{"class":2703},[1167,6327,6328],{"class":1183}," \u002Fsrv\u002Fshop\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf:ro",[1167,6330,6331],{"class":1183}," nginx:1.27-alpine\n",[1167,6333,6334,6336,6338,6340,6342,6344,6346,6349],{"class":1169,"line":1208},[1167,6335,559],{"class":1173},[1167,6337,3380],{"class":1183},[1167,6339,5611],{"class":2703},[1167,6341,3349],{"class":2703},[1167,6343,3598],{"class":1183},[1167,6345,6306],{"class":2703},[1167,6347,6348],{"class":1183}," type=bind,source=\u002Fsrv\u002Fshop\u002Fnginx.conf,target=\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf,readonly",[1167,6350,6331],{"class":1183},[45,6352,6353,6370],{},[48,6354,6355],{},[51,6356,6357,6362,6367],{},[54,6358,6359,6361],{},[19,6360,6259],{}," 写法",[54,6363,6364,6366],{},[19,6365,2901],{}," 等价",[54,6368,6369],{},"说明",[67,6371,6372,6390,6413,6431],{},[51,6373,6374,6379,6384],{},[72,6375,6376],{},[19,6377,6378],{},"-v \u002Fabs\u002Fpath",[72,6380,6381],{},[19,6382,6383],{},"--mount type=volume,target=\u002Fabs\u002Fpath",[72,6385,6386,6389],{},[31,6387,6388],{},"只给目标 → 匿名卷","（最容易长出孤儿卷）",[51,6391,6392,6396,6401],{},[72,6393,6394],{},[19,6395,3977],{},[72,6397,6398],{},[19,6399,6400],{},"--mount type=volume,source=name,target=\u002Fpath",[72,6402,6403,6404,6407,6408,793,6410,6412],{},"命名卷；",[19,6405,6406],{},"name"," 不以 ",[19,6409,2872],{},[19,6411,793],{}," 开头即判定为卷名",[51,6414,6415,6420,6425],{},[72,6416,6417],{},[19,6418,6419],{},"-v .\u002Fhost:\u002Fcont",[72,6421,6422],{},[19,6423,6424],{},"--mount type=bind,source=$(pwd)\u002Fhost,target=\u002Fcont",[72,6426,6427,6428,6430],{},"bind；",[19,6429,6259],{}," 相对路径基于当前工作目录",[51,6432,6433,6438,6443],{},[72,6434,6435],{},[19,6436,6437],{},"-v a:b:ro,z",[72,6439,6440],{},[19,6441,6442],{},"--mount type=bind,...,readonly,z",[72,6444,6445,6446,114,6449,114,6452,793,6455,6458,6459],{},"选项：",[19,6447,6448],{},"ro",[19,6450,6451],{},"rw",[19,6453,6454],{},"z",[19,6456,6457],{},"Z","（SELinux）、",[19,6460,4002],{},[15,6462,6463,6464,6469,6470,6472,6473,6475,6476,6478,6479,1372,6482,6484],{},"实践建议：",[31,6465,6466,6467],{},"脚本与 Compose 用 ",[19,6468,2901],{},"——参数拼错（比如卷名写了相对路径）时 ",[19,6471,2901],{}," 会明确报错，而 ",[19,6474,6259],{}," 会静默地给你建一个空目录，最后表现为「配置没生效」。手工敲命令时 ",[19,6477,6259],{}," 省事。tmpfs 则只能用长语法：",[19,6480,6481],{},"--mount type=tmpfs,tmpfs-size=64m,target=\u002Frun\u002Fredis",[19,6483,6259],{}," 表达不了大小限制）。",[15,6486,6487,6488,6491,6492,6495,6496,6499,6500,6503,6504,6507],{},"一个必须提前知道的坑：",[19,6489,6490],{},"-v .\u002Fconf:\u002Fetc\u002Fapp"," 若宿主的 ",[19,6493,6494],{},".\u002Fconf"," 不存在，",[31,6497,6498],{},"Docker 会创建它并 chown 给 root","；容器内原本 ",[19,6501,6502],{},"\u002Fetc\u002Fapp"," 里的文件会被",[31,6505,6506],{},"空目录遮住","，于是出现「镜像里有配置，挂载后配置没了」。",[40,6509,6511],{"id":6510},"volumes-from把别人的卷借过来","--volumes-from：把别人的卷借过来",[1158,6513,6515],{"className":1160,"code":6514,"language":1162,"meta":1163,"style":1163},"docker run --rm --volumes-from shop-db -v \"$PWD:\u002Fbackup\" alpine \\\n  tar czf \u002Fbackup\u002Fdb-data-20261004.tar.gz -C \u002Fvar\u002Flib\u002Fmysql .\n",[19,6516,6517,6547],{"__ignoreMap":1163},[1167,6518,6519,6521,6523,6525,6528,6530,6532,6535,6538,6541,6544],{"class":1169,"line":1170},[1167,6520,559],{"class":1173},[1167,6522,3380],{"class":1183},[1167,6524,3386],{"class":2703},[1167,6526,6527],{"class":2703}," --volumes-from",[1167,6529,5664],{"class":1183},[1167,6531,6285],{"class":2703},[1167,6533,6534],{"class":1183}," \"",[1167,6536,6537],{"class":1216},"$PWD",[1167,6539,6540],{"class":1183},":\u002Fbackup\"",[1167,6542,6543],{"class":1183}," alpine",[1167,6545,6546],{"class":2703}," \\\n",[1167,6548,6549,6552,6555,6558,6561,6564],{"class":1169,"line":1177},[1167,6550,6551],{"class":1183},"  tar",[1167,6553,6554],{"class":1183}," czf",[1167,6556,6557],{"class":1183}," \u002Fbackup\u002Fdb-data-20261004.tar.gz",[1167,6559,6560],{"class":2703}," -C",[1167,6562,6563],{"class":1183}," \u002Fvar\u002Flib\u002Fmysql",[1167,6565,2710],{"class":1183},[15,6567,6568,6571,6572,6575],{},[19,6569,6570],{},"--volumes-from"," 复用另一个容器声明过的所有挂载（包括匿名卷），是「备份\u002F恢复容器数据」的标准套路。注意它继承的是",[31,6573,6574],{},"那个容器创建时的挂载组合","，改了原容器的挂载配置要重建才生效。",[40,6577,6579],{"id":6578},"卷在宿主上的实际位置和怎么看清它","卷在宿主上的实际位置，和怎么看清它",[1158,6581,6583],{"className":1160,"code":6582,"language":1162,"meta":1163,"style":1163},"docker volume ls; docker volume inspect db-data     # inspect 里的 Mountpoint 是宿主真实路径\nsudo du -sh \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F* | sort -h | tail\ndocker volume rm db-data; docker volume prune       # prune 会删掉所有没被任何容器引用的卷\n",[19,6584,6585,6608,6636],{"__ignoreMap":1163},[1167,6586,6587,6589,6592,6594,6596,6598,6600,6602,6605],{"class":1169,"line":1170},[1167,6588,559],{"class":1173},[1167,6590,6591],{"class":1183}," volume",[1167,6593,4689],{"class":1183},[1167,6595,3629],{"class":1216},[1167,6597,559],{"class":1173},[1167,6599,6591],{"class":1183},[1167,6601,3370],{"class":1183},[1167,6603,6604],{"class":1183}," db-data",[1167,6606,6607],{"class":3604},"     # inspect 里的 Mountpoint 是宿主真实路径\n",[1167,6609,6610,6612,6615,6618,6621,6624,6626,6628,6631,6633],{"class":1169,"line":1177},[1167,6611,905],{"class":1173},[1167,6613,6614],{"class":1183}," du",[1167,6616,6617],{"class":2703}," -sh",[1167,6619,6620],{"class":1183}," \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F",[1167,6622,6623],{"class":2703},"*",[1167,6625,3506],{"class":3505},[1167,6627,5191],{"class":1173},[1167,6629,6630],{"class":2703}," -h",[1167,6632,3506],{"class":3505},[1167,6634,6635],{"class":1173}," tail\n",[1167,6637,6638,6640,6642,6644,6646,6648,6650,6652,6654],{"class":1169,"line":1202},[1167,6639,559],{"class":1173},[1167,6641,6591],{"class":1183},[1167,6643,1196],{"class":1183},[1167,6645,6604],{"class":1183},[1167,6647,3629],{"class":1216},[1167,6649,559],{"class":1173},[1167,6651,6591],{"class":1183},[1167,6653,3695],{"class":1183},[1167,6655,6656],{"class":3604},"       # prune 会删掉所有没被任何容器引用的卷\n",[15,6658,6659,951,6661,6664,6665,1962,6668,6671,6672,6675,6676,2593,6678,6681],{},[19,6660,4358],{},[19,6662,6663],{},"Mountpoint"," 就是宿主真实路径（rootless 下是 ",[19,6666,6667],{},"~\u002F.local\u002Fshare\u002Fdocker\u002Fvolumes\u002F...",[19,6669,6670],{},"docker volume prune"," 会删掉",[31,6673,6674],{},"所有没被任何容器引用的卷","——包括你上周测试留下的数据库，所以先 ",[19,6677,4333],{},[19,6679,6680],{},"rm","，别直接 prune（第十四章把这条列入危险命令）。",[15,6683,6684],{},"两种备份\u002F迁移方式：",[1158,6686,6688],{"className":1160,"code":6687,"language":1162,"meta":1163,"style":1163},"docker run --rm --volumes-from shop-db -v \"$PWD:\u002Fbackup\" alpine tar czf \u002Fbackup\u002Fdb.tar.gz \u002Fvar\u002Flib\u002Fmysql\ndocker run -d --name shop-db-new -v db-data-new:\u002Fvar\u002Flib\u002Fmysql postgres:16.4\ndocker run --rm -v \"$PWD:\u002Fbackup\" -v db-data-new:\u002Fvar\u002Flib\u002Fmysql alpine tar xzf \u002Fbackup\u002Fdb.tar.gz -C \u002F\n",[19,6689,6690,6723,6743],{"__ignoreMap":1163},[1167,6691,6692,6694,6696,6698,6700,6702,6704,6706,6708,6710,6712,6715,6717,6720],{"class":1169,"line":1170},[1167,6693,559],{"class":1173},[1167,6695,3380],{"class":1183},[1167,6697,3386],{"class":2703},[1167,6699,6527],{"class":2703},[1167,6701,5664],{"class":1183},[1167,6703,6285],{"class":2703},[1167,6705,6534],{"class":1183},[1167,6707,6537],{"class":1216},[1167,6709,6540],{"class":1183},[1167,6711,6543],{"class":1183},[1167,6713,6714],{"class":1183}," tar",[1167,6716,6554],{"class":1183},[1167,6718,6719],{"class":1183}," \u002Fbackup\u002Fdb.tar.gz",[1167,6721,6722],{"class":1183}," \u002Fvar\u002Flib\u002Fmysql\n",[1167,6724,6725,6727,6729,6731,6733,6736,6738,6741],{"class":1169,"line":1177},[1167,6726,559],{"class":1173},[1167,6728,3380],{"class":1183},[1167,6730,5611],{"class":2703},[1167,6732,3349],{"class":2703},[1167,6734,6735],{"class":1183}," shop-db-new",[1167,6737,6285],{"class":2703},[1167,6739,6740],{"class":1183}," db-data-new:\u002Fvar\u002Flib\u002Fmysql",[1167,6742,6291],{"class":1183},[1167,6744,6745,6747,6749,6751,6753,6755,6757,6759,6761,6763,6765,6767,6770,6772,6774],{"class":1169,"line":1202},[1167,6746,559],{"class":1173},[1167,6748,3380],{"class":1183},[1167,6750,3386],{"class":2703},[1167,6752,6285],{"class":2703},[1167,6754,6534],{"class":1183},[1167,6756,6537],{"class":1216},[1167,6758,6540],{"class":1183},[1167,6760,6285],{"class":2703},[1167,6762,6740],{"class":1183},[1167,6764,6543],{"class":1183},[1167,6766,6714],{"class":1183},[1167,6768,6769],{"class":1183}," xzf",[1167,6771,6719],{"class":1183},[1167,6773,6560],{"class":2703},[1167,6775,6776],{"class":1183}," \u002F\n",[15,6778,6779,6780,6783,6784,6787],{},"第二种把卷挂进一个临时容器再解包，好处是",[31,6781,6782],{},"不依赖原容器存在","，跨机器迁移时只需要那个 tar。生产上更常见的是走数据库自身的备份（",[19,6785,6786],{},"pg_dump","），卷快照只作灾备——文件级备份对运行中的数据库常常是不一致的。",[40,6789,6791],{"id":6790},"bind-mount-的权限坑uid-对不上是最常见故障","bind mount 的权限坑：UID 对不上是最常见故障",[15,6793,6794,6795,6798,6799,6802],{},"现象：挂载成功、目录可见，但写进去报 ",[19,6796,6797],{},"EACCES: permission denied","。原因是",[31,6800,6801],{},"容器内的 UID 和宿主目录属主对不上","——权限检查用的是数字 UID，不看用户名。",[1158,6804,6806],{"className":1160,"code":6805,"language":1162,"meta":1163,"style":1163},"docker exec shop-api id                # uid=1001(app) gid=1001(app)\nls -ld \u002Fsrv\u002Fshop\u002Fuploads               # drwxr-xr-x 2 root root ... \u002Fsrv\u002Fshop\u002Fuploads\ndocker exec shop-api touch \u002Fsrv\u002Fshop\u002Fuploads\u002Ft   # permission denied → sudo chown -R 1001:1001 对齐数字 UID\n",[19,6807,6808,6822,6835],{"__ignoreMap":1163},[1167,6809,6810,6812,6814,6816,6819],{"class":1169,"line":1170},[1167,6811,559],{"class":1173},[1167,6813,4629],{"class":1183},[1167,6815,4461],{"class":1183},[1167,6817,6818],{"class":1183}," id",[1167,6820,6821],{"class":3604},"                # uid=1001(app) gid=1001(app)\n",[1167,6823,6824,6826,6829,6832],{"class":1169,"line":1177},[1167,6825,4333],{"class":1173},[1167,6827,6828],{"class":2703}," -ld",[1167,6830,6831],{"class":1183}," \u002Fsrv\u002Fshop\u002Fuploads",[1167,6833,6834],{"class":3604},"               # drwxr-xr-x 2 root root ... \u002Fsrv\u002Fshop\u002Fuploads\n",[1167,6836,6837,6839,6841,6843,6846,6849],{"class":1169,"line":1202},[1167,6838,559],{"class":1173},[1167,6840,4629],{"class":1183},[1167,6842,4461],{"class":1183},[1167,6844,6845],{"class":1183}," touch",[1167,6847,6848],{"class":1183}," \u002Fsrv\u002Fshop\u002Fuploads\u002Ft",[1167,6850,6851],{"class":3604},"   # permission denied → sudo chown -R 1001:1001 对齐数字 UID\n",[15,6853,6854],{},"四种解法与取舍：",[45,6856,6857,6869],{},[48,6858,6859],{},[51,6860,6861,6864,6866],{},[54,6862,6863],{},"解法",[54,6865,4914],{},[54,6867,6868],{},"备注",[67,6870,6871,6890,6906,6925],{},[51,6872,6873,6876,6882],{},[72,6874,6875],{},"改宿主目录属主",[72,6877,6878,6881],{},[19,6879,6880],{},"chown 1001:1001","（数字 UID）",[72,6883,6884,6885,6887,6888,1075],{},"最直接；要知道镜像里应用跑在哪个 UID（",[19,6886,4756],{}," 或 Dockerfile 的 ",[19,6889,1389],{},[51,6891,6892,6898,6903],{},[72,6893,3476,6894,6897],{},[19,6895,6896],{},"--user"," 对齐",[72,6899,6900],{},[19,6901,6902],{},"--user \"$(id -u):$(id -g)\"",[72,6904,6905],{},"开发机神器：容器进程就是你的宿主用户，写出的文件属主正确",[51,6907,6908,6911,6922],{},[72,6909,6910],{},"改组权限",[72,6912,6913,888,6916,888,6919],{},[19,6914,6915],{},"chgrp",[19,6917,6918],{},"chmod g+w",[19,6920,6921],{},"--group-add",[72,6923,6924],{},"需要共享写时更协作友好",[51,6926,6927,6930,6933],{},[72,6928,6929],{},"改用命名卷",[72,6931,6932],{},"让 Docker 自己管理目录",[72,6934,6935,6936,6939],{},"卷首次挂到空目录时会",[31,6937,6938],{},"把镜像里该目录的内容复制进卷","（bind mount 不会！）——这条差异能救「配置丢了」的故障",[15,6941,6942,6943,6946,6947,6950,6951,6954,6955,6958,6959,6962,6963,6966],{},"rootless 模式再多一层：",[31,6944,6945],{},"subuid 映射","。容器里的 UID 会被平移，",[19,6948,6949],{},"docker exec"," 看到 ",[19,6952,6953],{},"uid=0"," 实际是宿主的 ",[19,6956,6957],{},"100000","，宿主目录属主要按 ",[19,6960,6961],{},"\u002Fetc\u002Fsubuid"," 的偏移算（",[19,6964,6965],{},"grep $(whoami) \u002Fetc\u002Fsubuid","）。这也是 rootless + bind mount 反复出权限问题的原因，第十五章还会遇到它。",[40,6968,6970],{"id":6969},"为什么数据库必须用卷镜像里为什么不该有数据","为什么数据库必须用卷，镜像里为什么不该有数据",[15,6972,6973],{},"三个理由：",[1318,6975,6976,6982,6995],{},[584,6977,6978,6981],{},[31,6979,6980],{},"性能","：往镜像层所在的 overlayfs 写随机 IO 明显慢于专用文件系统目录（卷或 bind）。数据库这种写密集负载，overlay2 的 copy-up 会让每次首次写都变成「整文件复制」。",[584,6983,6984,6987,6988,6990,6991,6994],{},[31,6985,6986],{},"生命周期","：容器是可丢弃的，数据库不是。挂卷之后 ",[19,6989,151],{}," 掉容器，重新 ",[19,6992,6993],{},"run"," 同名卷，数据回来了。",[584,6996,6997,7000,7001,7004,7005,7007],{},[31,6998,6999],{},"可移植与备份","：卷能被 ",[19,7002,7003],{},"volume inspect","、被快照、被 ",[19,7006,6570],{}," 备份；容器可写层这三样都做不好。",[15,7009,7010,7011,7014,7015,7018,7019,7021,7022,7025,7026,7028,7029,7031,7032,7035],{},"而「镜像里的 ",[19,7012,7013],{},"\u002Fvar\u002Flib\u002Fmysql"," 为什么不该写进镜像」：镜像层是",[31,7016,7017],{},"只读","的，运行时对它的一切写入都落在容器层；把初始化数据打进镜像会导致镜像体积暴涨、每个容器各存一份、",[19,7020,1395],{}," 声明处还会丢弃写入（第四章）。更关键的是",[31,7023,7024],{},"镜像会泄密","——初始化 SQL 里往往带着默认密码、示例账号，任何能 ",[19,7027,887],{}," 的人都能 ",[19,7030,1508],{}," 后翻出来。正确做法是首次启动时用 ",[19,7033,7034],{},"POSTGRES_*"," 环境变量或 initdb 钩子建库（第十三章）。",[15,7037,7038,7039,7041],{},"示例 09 用一次完整流程演示这四种存储、",[19,7040,4570],{}," 发现写入位置、以及那个权限坑的现场与修法。",[1405,7043],{"height":5320,"src":7044,"title":7045},"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F09-volumes.html","示例 09：卷与 bind mount——数据去哪了与权限坑",[15,7047,7048],{},"数据落盘的问题解决了，下一章解决「镜像从哪来、到哪去」：仓库、tag 规范与私有 registry。",[10,7050,7052],{"id":7051},"十镜像仓库与-tag-规范拉推锁定与私有-registry","十、镜像仓库与 tag 规范：拉、推、锁定与私有 registry",[40,7054,7055],{"id":7055},"一个镜像的完整名字长什么样",[1158,7057,7059],{"className":1160,"code":7058,"language":1162,"meta":1163,"style":1163},"docker pull nginx:1.27-alpine\ndocker pull registry-1.docker.io\u002Flibrary\u002Fnginx:1.27-alpine      # 上面那行的完整展开\ndocker pull ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker pull localhost:5000\u002Fshop-api:1.8.3\n",[19,7060,7061,7070,7082,7090],{"__ignoreMap":1163},[1167,7062,7063,7065,7068],{"class":1169,"line":1170},[1167,7064,559],{"class":1173},[1167,7066,7067],{"class":1183}," pull",[1167,7069,6331],{"class":1183},[1167,7071,7072,7074,7076,7079],{"class":1169,"line":1177},[1167,7073,559],{"class":1173},[1167,7075,7067],{"class":1183},[1167,7077,7078],{"class":1183}," registry-1.docker.io\u002Flibrary\u002Fnginx:1.27-alpine",[1167,7080,7081],{"class":3604},"      # 上面那行的完整展开\n",[1167,7083,7084,7086,7088],{"class":1169,"line":1202},[1167,7085,559],{"class":1173},[1167,7087,7067],{"class":1183},[1167,7089,3430],{"class":1183},[1167,7091,7092,7094,7096],{"class":1169,"line":1208},[1167,7093,559],{"class":1173},[1167,7095,7067],{"class":1183},[1167,7097,7098],{"class":1183}," localhost:5000\u002Fshop-api:1.8.3\n",[15,7100,7101,7102,7105,7106,7108,7109,7112,7113,7115,7116,7119,7120,7126],{},"名字被解析成三段：",[19,7103,7104],{},"\u003Cregistry>\u002F\u003Cnamespace>\u002F\u003Crepo>:\u003Ctag>","。省略 registry 时默认 ",[19,7107,134],{},"，省略 namespace 时默认 ",[19,7110,7111],{},"library","（官方镜像命名空间）。所以 ",[19,7114,113],{}," 的真实身份是 ",[19,7117,7118],{},"docker.io\u002Flibrary\u002Fnginx:latest","——",[31,7121,7122,7123],{},"不写 tag 就是 ",[19,7124,7125],{},"latest","，这一点在下一节会付出代价。",[15,7128,7129,7132,7133,7136,7137,7140,7141,7144],{},[19,7130,7131],{},"docker pull"," 时屏幕上打印的 ",[19,7134,7135],{},"1.27-alpine: Pulling from library\u002Fnginx"," 后面跟着一串 ",[19,7138,7139],{},"Layer ID"," 的下载进度；同一层已在本地会显示 ",[19,7142,7143],{},"Already exists","（第三章的分层共享）。",[40,7146,7148],{"id":7147},"tag-不是版本是可变的指针","tag 不是版本，是「可变的指针」",[45,7150,7151,7161],{},[48,7152,7153],{},[51,7154,7155,7158],{},[54,7156,7157],{},"事实",[54,7159,7160],{},"后果",[67,7162,7163,7175,7185],{},[51,7164,7165,7168],{},[72,7166,7167],{},"tag 可以反复指向不同的镜像（覆盖）",[72,7169,7170,7171,7174],{},"今天的 ",[19,7172,7173],{},"shop-api:1.8.3"," 和昨天的可能不是同一个东西",[51,7176,7177,7182],{},[72,7178,7179,7181],{},[19,7180,7125],{}," 永远指向「最后一次 push 的」",[72,7183,7184],{},"无法回滚、CI 不可复现、A\u002FB 两台机器可能跑不同版本",[51,7186,7187,7194],{},[72,7188,7189,7190,7193],{},"digest（",[19,7191,7192],{},"sha256:...","）由内容算出，永不变",[72,7195,7196],{},"唯一可靠的锁定手段",[15,7198,7199,7200,616],{},"生产规范：",[31,7201,7202],{},"用不可变 tag + digest 双锁",[1158,7204,7206],{"className":1160,"code":7205,"language":1162,"meta":1163,"style":1163},"docker pull nginx@sha256:2bc60bb1c9d0c4b7aa220796840eab3d18d613059be13b63bea490dc8a32fc36\ndocker image inspect nginx:1.27-alpine --format '{{index .RepoDigests 0}}'\ndocker run -d --name shop-web nginx:1.27-alpine@sha256:2bc60bb1c9d0...\n",[19,7207,7208,7217,7233],{"__ignoreMap":1163},[1167,7209,7210,7212,7214],{"class":1169,"line":1170},[1167,7211,559],{"class":1173},[1167,7213,7067],{"class":1183},[1167,7215,7216],{"class":1183}," nginx@sha256:2bc60bb1c9d0c4b7aa220796840eab3d18d613059be13b63bea490dc8a32fc36\n",[1167,7218,7219,7221,7224,7226,7228,7230],{"class":1169,"line":1177},[1167,7220,559],{"class":1173},[1167,7222,7223],{"class":1183}," image",[1167,7225,3370],{"class":1183},[1167,7227,3601],{"class":1183},[1167,7229,4777],{"class":2703},[1167,7231,7232],{"class":1183}," '{{index .RepoDigests 0}}'\n",[1167,7234,7235,7237,7239,7241,7243,7245],{"class":1169,"line":1202},[1167,7236,559],{"class":1173},[1167,7238,3380],{"class":1183},[1167,7240,5611],{"class":2703},[1167,7242,3349],{"class":2703},[1167,7244,3598],{"class":1183},[1167,7246,7247],{"class":1183}," nginx:1.27-alpine@sha256:2bc60bb1c9d0...\n",[15,7249,7250,7251,7254,7255,114,7258,7261],{},"Compose 里同样支持 ",[19,7252,7253],{},"image: nginx:1.27-alpine@sha256:2bc6...","。落地建议：tag 给人读（语义版本 ",[19,7256,7257],{},"1.8.3",[19,7259,7260],{},"1.8.3-build.20261004","），digest 给机器锁（部署清单里记 digest，回滚 = 用旧 digest 再起一次）。",[15,7263,7264,7266,7267,7270,7271,7274,7275,616],{},[19,7265,7125],{}," 唯一合理的使用场景是本地开发随手试。另一种常见误用是 ",[19,7268,7269],{},"1.8","（大版本 tag）：它会被上游重新指向 ",[19,7272,7273],{},"1.8.4","，你的生产库一夜之间换了小版本。",[31,7276,7277],{},"要固定就固定到具体 patch 版本 + digest",[40,7279,7281],{"id":7280},"多架构与-manifest","多架构与 manifest",[1158,7283,7285],{"className":1160,"code":7284,"language":1162,"meta":1163,"style":1163},"docker manifest inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker manifest inspect -v nginx:1.27-alpine | grep -A3 '\"platform\"'\ndocker pull --platform linux\u002Farm64 busybox\n",[19,7286,7287,7297,7319],{"__ignoreMap":1163},[1167,7288,7289,7291,7293,7295],{"class":1169,"line":1170},[1167,7290,559],{"class":1173},[1167,7292,3425],{"class":1183},[1167,7294,3370],{"class":1183},[1167,7296,3430],{"class":1183},[1167,7298,7299,7301,7303,7305,7307,7309,7311,7313,7316],{"class":1169,"line":1177},[1167,7300,559],{"class":1173},[1167,7302,3425],{"class":1183},[1167,7304,3370],{"class":1183},[1167,7306,6285],{"class":2703},[1167,7308,3601],{"class":1183},[1167,7310,3506],{"class":3505},[1167,7312,5825],{"class":1173},[1167,7314,7315],{"class":2703}," -A3",[1167,7317,7318],{"class":1183}," '\"platform\"'\n",[1167,7320,7321,7323,7325,7327,7330],{"class":1169,"line":1202},[1167,7322,559],{"class":1173},[1167,7324,7067],{"class":1183},[1167,7326,3406],{"class":2703},[1167,7328,7329],{"class":1183}," linux\u002Farm64",[1167,7331,7332],{"class":1183}," busybox\n",[15,7334,7335,7336,7338,7339,7342,7343,7345,7346,7349,7350,7353,7354,616],{},"单个名字背后可以是一个 manifest list（每种架构一个子镜像），",[19,7337,7131],{}," 会按宿主平台自动挑。看不到 ",[19,7340,7341],{},"manifests"," 数组说明这是单架构镜像——把它跑在别的架构上就是 ",[19,7344,463],{},"（第七章）。国内机器上 ",[19,7347,7348],{},"docker manifest inspect"," 常因 registry 认证\u002F代理失败，加 ",[19,7351,7352],{},"--insecure"," 或换 ",[19,7355,7356],{},"docker buildx imagetools inspect",[40,7358,7360],{"id":7359},"私有-registry起一个认证以及-insecure-registries-的取舍","私有 registry：起一个、认证、以及 insecure-registries 的取舍",[1158,7362,7364],{"className":1160,"code":7363,"language":1162,"meta":1163,"style":1163},"docker run -d --name registry --restart unless-stopped -p 127.0.0.1:5000:5000 \\\n  -v registry-data:\u002Fvar\u002Flib\u002Fregistry registry:2\ndocker tag shop-api:1.8.3 localhost:5000\u002Fshop-api:1.8.3\ndocker push localhost:5000\u002Fshop-api:1.8.3\ncurl -s http:\u002F\u002Flocalhost:5000\u002Fv2\u002F_catalog\n",[19,7365,7366,7392,7403,7414,7423],{"__ignoreMap":1163},[1167,7367,7368,7370,7372,7374,7376,7379,7382,7385,7387,7390],{"class":1169,"line":1170},[1167,7369,559],{"class":1173},[1167,7371,3380],{"class":1183},[1167,7373,5611],{"class":2703},[1167,7375,3349],{"class":2703},[1167,7377,7378],{"class":1183}," registry",[1167,7380,7381],{"class":2703}," --restart",[1167,7383,7384],{"class":1183}," unless-stopped",[1167,7386,4695],{"class":2703},[1167,7388,7389],{"class":1183}," 127.0.0.1:5000:5000",[1167,7391,6546],{"class":2703},[1167,7393,7394,7397,7400],{"class":1169,"line":1177},[1167,7395,7396],{"class":2703},"  -v",[1167,7398,7399],{"class":1183}," registry-data:\u002Fvar\u002Flib\u002Fregistry",[1167,7401,7402],{"class":1183}," registry:2\n",[1167,7404,7405,7407,7410,7412],{"class":1169,"line":1202},[1167,7406,559],{"class":1173},[1167,7408,7409],{"class":1183}," tag",[1167,7411,2733],{"class":1183},[1167,7413,7098],{"class":1183},[1167,7415,7416,7418,7421],{"class":1169,"line":1208},[1167,7417,559],{"class":1173},[1167,7419,7420],{"class":1183}," push",[1167,7422,7098],{"class":1183},[1167,7424,7425,7428,7430],{"class":1169,"line":1238},[1167,7426,7427],{"class":1173},"curl",[1167,7429,3666],{"class":2703},[1167,7431,7432],{"class":1183}," http:\u002F\u002Flocalhost:5000\u002Fv2\u002F_catalog\n",[15,7434,7435,7436,7439,7440,7443],{},"推到 ",[19,7437,7438],{},"myreg.internal:5000"," 这种域名时，Docker 要求 HTTPS；没证书就得在 ",[19,7441,7442],{},"\u002Fetc\u002Fdocker\u002Fdaemon.json"," 里声明：",[1158,7445,7449],{"className":7446,"code":7447,"language":7448,"meta":1163,"style":1163},"language-json shiki shiki-themes github-dark","{ \"insecure-registries\": [\"myreg.internal:5000\"] }\n","json",[19,7450,7451],{"__ignoreMap":1163},[1167,7452,7453,7456,7459,7462,7465],{"class":1169,"line":1170},[1167,7454,7455],{"class":1216},"{ ",[1167,7457,7458],{"class":2703},"\"insecure-registries\"",[1167,7460,7461],{"class":1216},": [",[1167,7463,7464],{"class":1183},"\"myreg.internal:5000\"",[1167,7466,7467],{"class":1216},"] }\n",[15,7469,7470,7471,616,7474,7477,7478,7481,7482,7485],{},"改完 ",[19,7472,7473],{},"systemctl restart docker",[19,7475,7476],{},"insecure-registries"," 的含义是",[31,7479,7480],{},"明文 HTTP 传输","：镜像内容与认证 token 都在网上裸奔，只适合封闭内网 + 有物理隔离保证的环境；一旦跨公网就必须上 TLS（用 Caddy\u002Fnginx 反代 + Let's Encrypt，或 registry 自带 ",[19,7483,7484],{},"tls"," 配置）。",[15,7487,7488],{},"认证相关：",[45,7490,7491,7500],{},[48,7492,7493],{},[51,7494,7495,7498],{},[54,7496,7497],{},"操作",[54,7499,1563],{},[67,7501,7502,7516,7526,7551],{},[51,7503,7504,7507],{},[72,7505,7506],{},"登录",[72,7508,7509,1372,7512,7515],{},[19,7510,7511],{},"docker login ghcr.io -U \u003Cuser> --password-stdin",[31,7513,7514],{},"别在命令行里明文写密码","，会进 shell history）",[51,7517,7518,7521],{},[72,7519,7520],{},"登出",[72,7522,7523],{},[19,7524,7525],{},"docker logout ghcr.io",[51,7527,7528,7531],{},[72,7529,7530],{},"凭证存放",[72,7532,7533,7536,7537,7540,7541,728,7544,7547,7548],{},[19,7534,7535],{},"~\u002F.docker\u002Fconfig.json"," 里的 ",[19,7538,7539],{},"auths","；接 ",[19,7542,7543],{},"pass",[19,7545,7546],{},"secretservice"," 时用 ",[19,7549,7550],{},"credsStore",[51,7552,7553,7556],{},[72,7554,7555],{},"CI 里",[72,7557,7558,7559,7562],{},"用 secret 变量喂 ",[19,7560,7561],{},"--password-stdin","，或云厂商的短期 token",[40,7564,7565],{"id":7565},"镜像加速与代理",[15,7567,7568,7569,7572],{},"国内机器直连 Docker Hub 经常超时或限速，",[19,7570,7571],{},"daemon.json"," 里配 mirror：",[1158,7574,7576],{"className":7446,"code":7575,"language":7448,"meta":1163,"style":1163},"{\n  \"registry-mirrors\": [\"https:\u002F\u002Fdocker.m.daocloud.io\"],\n  \"dns\": [\"114.114.114.114\", \"8.8.8.8\"],\n  \"log-driver\": \"json-file\",\n  \"log-opts\": { \"max-size\": \"20m\", \"max-file\": \"3\" }\n}\n",[19,7577,7578,7583,7596,7613,7627,7656],{"__ignoreMap":1163},[1167,7579,7580],{"class":1169,"line":1170},[1167,7581,7582],{"class":1216},"{\n",[1167,7584,7585,7588,7590,7593],{"class":1169,"line":1177},[1167,7586,7587],{"class":2703},"  \"registry-mirrors\"",[1167,7589,7461],{"class":1216},[1167,7591,7592],{"class":1183},"\"https:\u002F\u002Fdocker.m.daocloud.io\"",[1167,7594,7595],{"class":1216},"],\n",[1167,7597,7598,7601,7603,7606,7608,7611],{"class":1169,"line":1202},[1167,7599,7600],{"class":2703},"  \"dns\"",[1167,7602,7461],{"class":1216},[1167,7604,7605],{"class":1183},"\"114.114.114.114\"",[1167,7607,1223],{"class":1216},[1167,7609,7610],{"class":1183},"\"8.8.8.8\"",[1167,7612,7595],{"class":1216},[1167,7614,7615,7618,7621,7624],{"class":1169,"line":1208},[1167,7616,7617],{"class":2703},"  \"log-driver\"",[1167,7619,7620],{"class":1216},": ",[1167,7622,7623],{"class":1183},"\"json-file\"",[1167,7625,7626],{"class":1216},",\n",[1167,7628,7629,7632,7635,7638,7640,7643,7645,7648,7650,7653],{"class":1169,"line":1238},[1167,7630,7631],{"class":2703},"  \"log-opts\"",[1167,7633,7634],{"class":1216},": { ",[1167,7636,7637],{"class":2703},"\"max-size\"",[1167,7639,7620],{"class":1216},[1167,7641,7642],{"class":1183},"\"20m\"",[1167,7644,1223],{"class":1216},[1167,7646,7647],{"class":2703},"\"max-file\"",[1167,7649,7620],{"class":1216},[1167,7651,7652],{"class":1183},"\"3\"",[1167,7654,7655],{"class":1216}," }\n",[1167,7657,7658],{"class":1169,"line":1257},[1167,7659,7660],{"class":1216},"}\n",[15,7662,7663,7664,7667,7668,939,7670,7673,7674,7676,7677,7679,7680,951,7682,7685],{},"要点：mirror 只是",[31,7665,7666],{},"缓存代理","，只加速公开镜像的 ",[19,7669,887],{},[19,7671,7672],{},"push"," 与私有镜像不走它；配完 ",[19,7675,677],{}," 里能看到 ",[19,7678,781],{}," 才算生效。企业环境走 HTTP 代理用 ",[19,7681,7535],{},[19,7683,7684],{},"proxies","（只影响客户端）或 systemd drop-in（影响 daemon，拉镜像真正吃代理的是 daemon）：",[1158,7687,7689],{"className":1160,"code":7688,"language":1162,"meta":1163,"style":1163},"sudo mkdir -p \u002Fetc\u002Fsystemd\u002Fsystem\u002Fdocker.service.d\nprintf '[Service]\\nEnvironment=\"HTTPS_PROXY=http:\u002F\u002F10.0.0.9:3128\"\\nEnvironment=\"NO_PROXY=localhost,127.0.0.1\"\\n' \\\n  | sudo tee \u002Fetc\u002Fsystemd\u002Fsystem\u002Fdocker.service.d\u002Fproxy.conf\nsudo systemctl daemon-reload && sudo systemctl restart docker\n",[19,7690,7691,7703,7712,7726],{"__ignoreMap":1163},[1167,7692,7693,7695,7698,7700],{"class":1169,"line":1170},[1167,7694,905],{"class":1173},[1167,7696,7697],{"class":1183}," mkdir",[1167,7699,4695],{"class":2703},[1167,7701,7702],{"class":1183}," \u002Fetc\u002Fsystemd\u002Fsystem\u002Fdocker.service.d\n",[1167,7704,7705,7707,7710],{"class":1169,"line":1177},[1167,7706,3490],{"class":2703},[1167,7708,7709],{"class":1183}," '[Service]\\nEnvironment=\"HTTPS_PROXY=http:\u002F\u002F10.0.0.9:3128\"\\nEnvironment=\"NO_PROXY=localhost,127.0.0.1\"\\n'",[1167,7711,6546],{"class":2703},[1167,7713,7714,7717,7720,7723],{"class":1169,"line":1202},[1167,7715,7716],{"class":3505},"  |",[1167,7718,7719],{"class":1173}," sudo",[1167,7721,7722],{"class":1183}," tee",[1167,7724,7725],{"class":1183}," \u002Fetc\u002Fsystemd\u002Fsystem\u002Fdocker.service.d\u002Fproxy.conf\n",[1167,7727,7728,7730,7733,7736,7739,7741,7743,7746],{"class":1169,"line":1208},[1167,7729,905],{"class":1173},[1167,7731,7732],{"class":1183}," systemctl",[1167,7734,7735],{"class":1183}," daemon-reload",[1167,7737,7738],{"class":1216}," && ",[1167,7740,905],{"class":1173},[1167,7742,7732],{"class":1183},[1167,7744,7745],{"class":1183}," restart",[1167,7747,7748],{"class":1183}," docker\n",[40,7750,7752],{"id":7751},"离线分发save-load","离线分发：save \u002F load",[1158,7754,7756],{"className":1160,"code":7755,"language":1162,"meta":1163,"style":1163},"docker save -o shop-images.tar ghcr.io\u002Fexample\u002Fshop-api:1.8.3 postgres:16.4 redis:7.2-alpine\nscp u@build:\u002Ftmp\u002Fshop-images.tar prod:\u002Ftmp\u002F\ndocker load -i \u002Ftmp\u002Fshop-images.tar\n",[19,7757,7758,7779,7790],{"__ignoreMap":1163},[1167,7759,7760,7762,7765,7768,7771,7773,7776],{"class":1169,"line":1170},[1167,7761,559],{"class":1173},[1167,7763,7764],{"class":1183}," save",[1167,7766,7767],{"class":2703}," -o",[1167,7769,7770],{"class":1183}," shop-images.tar",[1167,7772,2707],{"class":1183},[1167,7774,7775],{"class":1183}," postgres:16.4",[1167,7777,7778],{"class":1183}," redis:7.2-alpine\n",[1167,7780,7781,7784,7787],{"class":1169,"line":1177},[1167,7782,7783],{"class":1173},"scp",[1167,7785,7786],{"class":1183}," u@build:\u002Ftmp\u002Fshop-images.tar",[1167,7788,7789],{"class":1183}," prod:\u002Ftmp\u002F\n",[1167,7791,7792,7794,7797,7800],{"class":1169,"line":1202},[1167,7793,559],{"class":1173},[1167,7795,7796],{"class":1183}," load",[1167,7798,7799],{"class":2703}," -i",[1167,7801,7802],{"class":1183}," \u002Ftmp\u002Fshop-images.tar\n",[15,7804,7805,7806,7809,7810,7813,7814,7817,7818,7821,7822,7825,7826,7829],{},"内网\u002F气隔环境的常规手段。三个提醒：",[19,7807,7808],{},"save"," 出的 tar 是",[31,7811,7812],{},"未压缩","的（体积按解压后算），用 ",[19,7815,7816],{},"docker save img | gzip > img.tar.gz"," 减一半；tar ",[31,7819,7820],{},"绑定架构","，别指望 amd64 的 tar 能在 ARM 上跑；",[19,7823,7824],{},"load"," 之后镜像没有 registry 前缀的 RepoDigest，",[19,7827,7828],{},"pull_policy"," 相关行为要注意（第十二章）。",[40,7831,7833],{"id":7832},"docker-hub-的限流","Docker Hub 的限流",[15,7835,7836],{},"未登录 6 小时内每 IP 约 100 次 pull，免费账号 200 次；超了报：",[1158,7838,7841],{"className":7839,"code":7840,"language":3148},[3146],"toomanyrequests: You have reached your pull rate limit. You may enable and increase limits by upgrading to Team or Enterprise.\n",[19,7842,7840],{"__ignoreMap":1163},[15,7844,7845,7846,7849,7850,7853,7854,7856,7857,7860],{},"CI 里最容易撞上。缓解手段按有效性排：",[31,7847,7848],{},"把公共基础镜像同步到自己的 registry","（最稳，",[19,7851,7852],{},"skopeo copy"," 或 CI 里一次性 mirror）、CI 用云厂商提供的免费 mirror、给构建加缓存层（避免每次重新解析 ",[19,7855,7125],{},"）、",[19,7858,7859],{},"docker login","（把限额从匿名 IP 池换成账号）。",[15,7862,7863],{},"下一章把「一条一条 docker run」升级成「一个文件描述整套服务」：Compose。",[10,7865,7867],{"id":7866},"十一docker-compose-入门composeyaml-的骨架","十一、Docker Compose 入门：compose.yaml 的骨架",[40,7869,7871],{"id":7870},"为什么需要-compose","为什么需要 Compose",[15,7873,7874,7875,7877,7878,910,7881,7884],{},"手工 ",[19,7876,555],{}," 起四个服务，你要记住每条命令的 20 个参数、顺序（谁先起）、网络名、卷名，还要在改代码后逐个重建。Compose 把这些写进一个文件：",[31,7879,7880],{},"声明式地描述一组服务",[19,7882,7883],{},"up"," 一下就把状态对齐。它是「单机多容器」的事实标准，也是学 Swarm\u002FK8s 的概念入口。",[1158,7886,7888],{"className":1160,"code":7887,"language":1162,"meta":1163,"style":1163},"docker compose version     # v2 是 docker 子命令，注意不是 docker-compose\ndocker compose up -d\n",[19,7889,7890,7903],{"__ignoreMap":1163},[1167,7891,7892,7894,7897,7900],{"class":1169,"line":1170},[1167,7893,559],{"class":1173},[1167,7895,7896],{"class":1183}," compose",[1167,7898,7899],{"class":1183}," version",[1167,7901,7902],{"class":3604},"     # v2 是 docker 子命令，注意不是 docker-compose\n",[1167,7904,7905,7907,7909,7912],{"class":1169,"line":1177},[1167,7906,559],{"class":1173},[1167,7908,7896],{"class":1183},[1167,7910,7911],{"class":1183}," up",[1167,7913,7914],{"class":2703}," -d\n",[40,7916,7918],{"id":7917},"顶层结构五个键","顶层结构：五个键",[1158,7920,7924],{"className":7921,"code":7922,"language":7923,"meta":1163,"style":1163},"language-yaml shiki shiki-themes github-dark","services:            # 必填；每个键是一个容器（或一组副本）\n  web: { image: nginx:1.27-alpine }\nnetworks:            # 自定义网络；不写则自动有一个 \u003Cproject>_default\n  shop-net: { driver: bridge }\nvolumes:             # 命名卷；不写则匿名卷或带项目前缀的自动卷\n  db-data:\nconfigs:             # 文本配置以文件形式注入容器\n  nginx-conf: { file: .\u002Fnginx\u002Fconf.d\u002Fsite.conf }\nsecrets:             # 密钥（Compose 里默认是 file driver）\n  db_password: { file: .\u002Fsecrets\u002Fdb_password.txt }\n","yaml",[19,7925,7926,7938,7955,7965,7981,7992,8000,8010,8027,8037],{"__ignoreMap":1163},[1167,7927,7928,7932,7935],{"class":1169,"line":1170},[1167,7929,7931],{"class":7930},"s4JwU","services",[1167,7933,7934],{"class":1216},":            ",[1167,7936,7937],{"class":3604},"# 必填；每个键是一个容器（或一组副本）\n",[1167,7939,7940,7943,7945,7948,7950,7953],{"class":1169,"line":1177},[1167,7941,7942],{"class":7930},"  web",[1167,7944,7634],{"class":1216},[1167,7946,7947],{"class":7930},"image",[1167,7949,7620],{"class":1216},[1167,7951,7952],{"class":1183},"nginx:1.27-alpine",[1167,7954,7655],{"class":1216},[1167,7956,7957,7960,7962],{"class":1169,"line":1202},[1167,7958,7959],{"class":7930},"networks",[1167,7961,7934],{"class":1216},[1167,7963,7964],{"class":3604},"# 自定义网络；不写则自动有一个 \u003Cproject>_default\n",[1167,7966,7967,7970,7972,7975,7977,7979],{"class":1169,"line":1208},[1167,7968,7969],{"class":7930},"  shop-net",[1167,7971,7634],{"class":1216},[1167,7973,7974],{"class":7930},"driver",[1167,7976,7620],{"class":1216},[1167,7978,5361],{"class":1183},[1167,7980,7655],{"class":1216},[1167,7982,7983,7986,7989],{"class":1169,"line":1238},[1167,7984,7985],{"class":7930},"volumes",[1167,7987,7988],{"class":1216},":             ",[1167,7990,7991],{"class":3604},"# 命名卷；不写则匿名卷或带项目前缀的自动卷\n",[1167,7993,7994,7997],{"class":1169,"line":1257},[1167,7995,7996],{"class":7930},"  db-data",[1167,7998,7999],{"class":1216},":\n",[1167,8001,8002,8005,8007],{"class":1169,"line":1274},[1167,8003,8004],{"class":7930},"configs",[1167,8006,7988],{"class":1216},[1167,8008,8009],{"class":3604},"# 文本配置以文件形式注入容器\n",[1167,8011,8012,8015,8017,8020,8022,8025],{"class":1169,"line":1290},[1167,8013,8014],{"class":7930},"  nginx-conf",[1167,8016,7634],{"class":1216},[1167,8018,8019],{"class":7930},"file",[1167,8021,7620],{"class":1216},[1167,8023,8024],{"class":1183},".\u002Fnginx\u002Fconf.d\u002Fsite.conf",[1167,8026,7655],{"class":1216},[1167,8028,8029,8032,8034],{"class":1169,"line":1304},[1167,8030,8031],{"class":7930},"secrets",[1167,8033,7988],{"class":1216},[1167,8035,8036],{"class":3604},"# 密钥（Compose 里默认是 file driver）\n",[1167,8038,8039,8042,8044,8046,8048,8051],{"class":1169,"line":1310},[1167,8040,8041],{"class":7930},"  db_password",[1167,8043,7634],{"class":1216},[1167,8045,8019],{"class":7930},[1167,8047,7620],{"class":1216},[1167,8049,8050],{"class":1183},".\u002Fsecrets\u002Fdb_password.txt",[1167,8052,7655],{"class":1216},[15,8054,8055,8057,8058,8065,8066,1962],{},[19,8056,7931],{}," 之外四个都可选，但",[31,8059,8060,8061,3177,8063],{},"生产风格的文件会显式声明 ",[19,8062,7959],{},[19,8064,7985],{},"，因为隐式资源的名字带项目前缀、难以在外部复用（第十二章的 ",[19,8067,8068],{},"external",[40,8070,8072],{"id":8071},"service-的常用字段","service 的常用字段",[45,8074,8075,8086],{},[48,8076,8077],{},[51,8078,8079,8081,8083],{},[54,8080,687],{},[54,8082,4183],{},[54,8084,8085],{},"关键细节",[67,8087,8088,8107,8134,8153,8165,8188,8204,8222,8238,8254,8270,8291,8313,8333],{},[51,8089,8090,8094,8097],{},[72,8091,8092],{},[19,8093,7947],{},[72,8095,8096],{},"直接用现成镜像",[72,8098,8099,8100,8103,8104,8106],{},"与 ",[19,8101,8102],{},"build"," 互斥或共存（共存时 ",[19,8105,7883],{}," 用构建产物）",[51,8108,8109,8113,8116],{},[72,8110,8111],{},[19,8112,8102],{},[72,8114,8115],{},"现场构建",[72,8117,8118,114,8121,114,8123,114,8126,114,8129,114,8132],{},[19,8119,8120],{},"context",[19,8122,1432],{},[19,8124,8125],{},"args",[19,8127,8128],{},"target",[19,8130,8131],{},"shm_size",[19,8133,887],{},[51,8135,8136,8141,8144],{},[72,8137,8138],{},[19,8139,8140],{},"ports",[72,8142,8143],{},"宿主→容器",[72,8145,8146,8147,8150,8151],{},"列表，",[19,8148,8149],{},"\"127.0.0.1:8080:80\""," 完全等价于 ",[19,8152,4743],{},[51,8154,8155,8159,8162],{},[72,8156,8157],{},[19,8158,2478],{},[72,8160,8161],{},"仅文档",[72,8163,8164],{},"只告诉别人「这个端口对内开放」，不产生任何映射",[51,8166,8167,8172,8175],{},[72,8168,8169],{},[19,8170,8171],{},"environment",[72,8173,8174],{},"环境变量",[72,8176,8177,8178,1372,8181,8184,8185,1075],{},"map 或 list 两种写法，值",[31,8179,8180],{},"必须是字符串",[19,8182,8183],{},"\"true\""," 不是 ",[19,8186,8187],{},"true",[51,8189,8190,8195,8198],{},[72,8191,8192],{},[19,8193,8194],{},"env_file",[72,8196,8197],{},"从文件注入",[72,8199,8200,8201,8203],{},"列表按顺序，后者覆盖前者；与 ",[19,8202,3167],{}," 是两回事（第十三章）",[51,8205,8206,8210,8212],{},[72,8207,8208],{},[19,8209,7985],{},[72,8211,3972],{},[72,8213,8214,8215,8218,8219],{},"短语法 ",[19,8216,8217],{},"\"db-data:\u002Fvar\u002Flib\u002Fmysql\""," 或长语法 ",[19,8220,8221],{},"type\u002Ftarget",[51,8223,8224,8229,8232],{},[72,8225,8226],{},[19,8227,8228],{},"depends_on",[72,8230,8231],{},"启动顺序",[72,8233,8234,8237],{},[31,8235,8236],{},"只保证顺序，不保证就绪","（下一节）",[51,8239,8240,8245,8248],{},[72,8241,8242],{},[19,8243,8244],{},"healthcheck",[72,8246,8247],{},"健康检查",[72,8249,8250,8251,8253],{},"等价于 ",[19,8252,2502],{},"，可在 Compose 里覆盖镜像的",[51,8255,8256,8261,8264],{},[72,8257,8258],{},[19,8259,8260],{},"restart",[72,8262,8263],{},"重启策略",[72,8265,8266,8269],{},[19,8267,8268],{},"unless-stopped"," 是单机默认答案（第十六章）",[51,8271,8272,8280,8283],{},[72,8273,8274,728,8277],{},[19,8275,8276],{},"command",[19,8278,8279],{},"entrypoint",[72,8281,8282],{},"覆盖镜像",[72,8284,8285,8286,728,8288,8290],{},"分别覆盖 ",[19,8287,847],{},[19,8289,1377],{},"（第四章四组合）",[51,8292,8293,8300,8302],{},[72,8294,8295,728,8298],{},[19,8296,8297],{},"network_mode",[19,8299,7959],{},[72,8301,4008],{},[72,8303,8304,8305,939,8307,8310,8311],{},"优先用 ",[19,8306,7959],{},[19,8308,8309],{},"network_mode: host"," 会忽略 ",[19,8312,8140],{},[51,8314,8315,8320,8323],{},[72,8316,8317],{},[19,8318,8319],{},"container_name",[72,8321,8322],{},"固定名字",[72,8324,8325,8328,8329,8332],{},[31,8326,8327],{},"慎用","：与 ",[19,8330,8331],{},"--scale"," 冲突，多实例必炸",[51,8334,8335,8340,8343],{},[72,8336,8337],{},[19,8338,8339],{},"profiles",[72,8341,8342],{},"按需启动",[72,8344,8345],{},"第十二章",[40,8347,8348],{"id":8348},"一段完整可用的例子",[1158,8350,8352],{"className":7921,"code":8351,"language":7923,"meta":1163,"style":1163},"services:\n  api:\n    build: { context: ., dockerfile: Dockerfile, args: { NODE_ENV: production } }\n    image: ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n    ports: [\"127.0.0.1:8080:3000\"]\n    environment: { DATABASE_URL: postgres:\u002F\u002Fshop:secret@db:5432\u002Fshop, REDIS_URL: redis:\u002F\u002Fcache:6379\u002F0 }\n    volumes: [uploads:\u002Fsrv\u002Fshop\u002Fuploads]\n    depends_on: { db: { condition: service_healthy }, cache: { condition: service_started } }\n    healthcheck:\n      test: [\"CMD\", \"node\", \"-e\", \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"]\n      interval: 15s\n      retries: 3\n      start_period: 20s\n    restart: unless-stopped\n  web:\n    image: nginx:1.27-alpine\n    ports: [\"80:80\"]\n    volumes: [\".\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro\"]\n    depends_on: [api]\n  db:\n    image: postgres:16.4\n    environment: { POSTGRES_USER: shop, POSTGRES_DB: shop }\n    volumes: [db-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata]\n    healthcheck: { test: [\"CMD-SHELL\", \"pg_isready -U shop\"], interval: 10s, retries: 5 }\n  cache:\n    image: redis:7.2-alpine\n    command: [\"redis-server\", \"--appendonly\", \"yes\"]\n    volumes: [cache-data:\u002Fdata]\nvolumes: { db-data: {}, cache-data: {}, uploads: {} }\n",[19,8353,8354,8360,8367,8405,8415,8428,8455,8467,8504,8511,8537,8547,8557,8567,8577,8583,8592,8604,8616,8628,8636,8646,8672,8684,8727,8735,8745,8768,8780],{"__ignoreMap":1163},[1167,8355,8356,8358],{"class":1169,"line":1170},[1167,8357,7931],{"class":7930},[1167,8359,7999],{"class":1216},[1167,8361,8362,8365],{"class":1169,"line":1177},[1167,8363,8364],{"class":7930},"  api",[1167,8366,7999],{"class":1216},[1167,8368,8369,8372,8374,8376,8378,8380,8382,8384,8386,8389,8391,8393,8395,8397,8399,8402],{"class":1169,"line":1202},[1167,8370,8371],{"class":7930},"    build",[1167,8373,7634],{"class":1216},[1167,8375,8120],{"class":7930},[1167,8377,7620],{"class":1216},[1167,8379,2872],{"class":2703},[1167,8381,1223],{"class":1216},[1167,8383,1432],{"class":7930},[1167,8385,7620],{"class":1216},[1167,8387,8388],{"class":1183},"Dockerfile",[1167,8390,1223],{"class":1216},[1167,8392,8125],{"class":7930},[1167,8394,7634],{"class":1216},[1167,8396,2249],{"class":7930},[1167,8398,7620],{"class":1216},[1167,8400,8401],{"class":1183},"production",[1167,8403,8404],{"class":1216}," } }\n",[1167,8406,8407,8410,8412],{"class":1169,"line":1208},[1167,8408,8409],{"class":7930},"    image",[1167,8411,7620],{"class":1216},[1167,8413,8414],{"class":1183},"ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n",[1167,8416,8417,8420,8422,8425],{"class":1169,"line":1238},[1167,8418,8419],{"class":7930},"    ports",[1167,8421,7461],{"class":1216},[1167,8423,8424],{"class":1183},"\"127.0.0.1:8080:3000\"",[1167,8426,8427],{"class":1216},"]\n",[1167,8429,8430,8433,8435,8438,8440,8443,8445,8448,8450,8453],{"class":1169,"line":1257},[1167,8431,8432],{"class":7930},"    environment",[1167,8434,7634],{"class":1216},[1167,8436,8437],{"class":7930},"DATABASE_URL",[1167,8439,7620],{"class":1216},[1167,8441,8442],{"class":1183},"postgres:\u002F\u002Fshop:secret@db:5432\u002Fshop",[1167,8444,1223],{"class":1216},[1167,8446,8447],{"class":7930},"REDIS_URL",[1167,8449,7620],{"class":1216},[1167,8451,8452],{"class":1183},"redis:\u002F\u002Fcache:6379\u002F0",[1167,8454,7655],{"class":1216},[1167,8456,8457,8460,8462,8465],{"class":1169,"line":1274},[1167,8458,8459],{"class":7930},"    volumes",[1167,8461,7461],{"class":1216},[1167,8463,8464],{"class":1183},"uploads:\u002Fsrv\u002Fshop\u002Fuploads",[1167,8466,8427],{"class":1216},[1167,8468,8469,8472,8474,8477,8479,8482,8484,8487,8490,8493,8495,8497,8499,8502],{"class":1169,"line":1290},[1167,8470,8471],{"class":7930},"    depends_on",[1167,8473,7634],{"class":1216},[1167,8475,8476],{"class":7930},"db",[1167,8478,7634],{"class":1216},[1167,8480,8481],{"class":7930},"condition",[1167,8483,7620],{"class":1216},[1167,8485,8486],{"class":1183},"service_healthy",[1167,8488,8489],{"class":1216}," }, ",[1167,8491,8492],{"class":7930},"cache",[1167,8494,7634],{"class":1216},[1167,8496,8481],{"class":7930},[1167,8498,7620],{"class":1216},[1167,8500,8501],{"class":1183},"service_started",[1167,8503,8404],{"class":1216},[1167,8505,8506,8509],{"class":1169,"line":1304},[1167,8507,8508],{"class":7930},"    healthcheck",[1167,8510,7999],{"class":1216},[1167,8512,8513,8516,8518,8521,8523,8525,8527,8530,8532,8535],{"class":1169,"line":1310},[1167,8514,8515],{"class":7930},"      test",[1167,8517,7461],{"class":1216},[1167,8519,8520],{"class":1183},"\"CMD\"",[1167,8522,1223],{"class":1216},[1167,8524,1220],{"class":1183},[1167,8526,1223],{"class":1216},[1167,8528,8529],{"class":1183},"\"-e\"",[1167,8531,1223],{"class":1216},[1167,8533,8534],{"class":1183},"\"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"",[1167,8536,8427],{"class":1216},[1167,8538,8539,8542,8544],{"class":1169,"line":3259},[1167,8540,8541],{"class":7930},"      interval",[1167,8543,7620],{"class":1216},[1167,8545,8546],{"class":1183},"15s\n",[1167,8548,8549,8552,8554],{"class":1169,"line":3264},[1167,8550,8551],{"class":7930},"      retries",[1167,8553,7620],{"class":1216},[1167,8555,8556],{"class":2703},"3\n",[1167,8558,8559,8562,8564],{"class":1169,"line":3269},[1167,8560,8561],{"class":7930},"      start_period",[1167,8563,7620],{"class":1216},[1167,8565,8566],{"class":1183},"20s\n",[1167,8568,8569,8572,8574],{"class":1169,"line":3275},[1167,8570,8571],{"class":7930},"    restart",[1167,8573,7620],{"class":1216},[1167,8575,8576],{"class":1183},"unless-stopped\n",[1167,8578,8579,8581],{"class":1169,"line":3280},[1167,8580,7942],{"class":7930},[1167,8582,7999],{"class":1216},[1167,8584,8585,8587,8589],{"class":1169,"line":3285},[1167,8586,8409],{"class":7930},[1167,8588,7620],{"class":1216},[1167,8590,8591],{"class":1183},"nginx:1.27-alpine\n",[1167,8593,8595,8597,8599,8602],{"class":1169,"line":8594},17,[1167,8596,8419],{"class":7930},[1167,8598,7461],{"class":1216},[1167,8600,8601],{"class":1183},"\"80:80\"",[1167,8603,8427],{"class":1216},[1167,8605,8607,8609,8611,8614],{"class":1169,"line":8606},18,[1167,8608,8459],{"class":7930},[1167,8610,7461],{"class":1216},[1167,8612,8613],{"class":1183},"\".\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro\"",[1167,8615,8427],{"class":1216},[1167,8617,8619,8621,8623,8626],{"class":1169,"line":8618},19,[1167,8620,8471],{"class":7930},[1167,8622,7461],{"class":1216},[1167,8624,8625],{"class":1183},"api",[1167,8627,8427],{"class":1216},[1167,8629,8631,8634],{"class":1169,"line":8630},20,[1167,8632,8633],{"class":7930},"  db",[1167,8635,7999],{"class":1216},[1167,8637,8639,8641,8643],{"class":1169,"line":8638},21,[1167,8640,8409],{"class":7930},[1167,8642,7620],{"class":1216},[1167,8644,8645],{"class":1183},"postgres:16.4\n",[1167,8647,8649,8651,8653,8656,8658,8661,8663,8666,8668,8670],{"class":1169,"line":8648},22,[1167,8650,8432],{"class":7930},[1167,8652,7634],{"class":1216},[1167,8654,8655],{"class":7930},"POSTGRES_USER",[1167,8657,7620],{"class":1216},[1167,8659,8660],{"class":1183},"shop",[1167,8662,1223],{"class":1216},[1167,8664,8665],{"class":7930},"POSTGRES_DB",[1167,8667,7620],{"class":1216},[1167,8669,8660],{"class":1183},[1167,8671,7655],{"class":1216},[1167,8673,8675,8677,8679,8682],{"class":1169,"line":8674},23,[1167,8676,8459],{"class":7930},[1167,8678,7461],{"class":1216},[1167,8680,8681],{"class":1183},"db-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata",[1167,8683,8427],{"class":1216},[1167,8685,8687,8689,8691,8694,8696,8699,8701,8704,8707,8710,8712,8715,8717,8720,8722,8725],{"class":1169,"line":8686},24,[1167,8688,8508],{"class":7930},[1167,8690,7634],{"class":1216},[1167,8692,8693],{"class":7930},"test",[1167,8695,7461],{"class":1216},[1167,8697,8698],{"class":1183},"\"CMD-SHELL\"",[1167,8700,1223],{"class":1216},[1167,8702,8703],{"class":1183},"\"pg_isready -U shop\"",[1167,8705,8706],{"class":1216},"], ",[1167,8708,8709],{"class":7930},"interval",[1167,8711,7620],{"class":1216},[1167,8713,8714],{"class":1183},"10s",[1167,8716,1223],{"class":1216},[1167,8718,8719],{"class":7930},"retries",[1167,8721,7620],{"class":1216},[1167,8723,8724],{"class":2703},"5",[1167,8726,7655],{"class":1216},[1167,8728,8730,8733],{"class":1169,"line":8729},25,[1167,8731,8732],{"class":7930},"  cache",[1167,8734,7999],{"class":1216},[1167,8736,8738,8740,8742],{"class":1169,"line":8737},26,[1167,8739,8409],{"class":7930},[1167,8741,7620],{"class":1216},[1167,8743,8744],{"class":1183},"redis:7.2-alpine\n",[1167,8746,8748,8751,8753,8756,8758,8761,8763,8766],{"class":1169,"line":8747},27,[1167,8749,8750],{"class":7930},"    command",[1167,8752,7461],{"class":1216},[1167,8754,8755],{"class":1183},"\"redis-server\"",[1167,8757,1223],{"class":1216},[1167,8759,8760],{"class":1183},"\"--appendonly\"",[1167,8762,1223],{"class":1216},[1167,8764,8765],{"class":1183},"\"yes\"",[1167,8767,8427],{"class":1216},[1167,8769,8771,8773,8775,8778],{"class":1169,"line":8770},28,[1167,8772,8459],{"class":7930},[1167,8774,7461],{"class":1216},[1167,8776,8777],{"class":1183},"cache-data:\u002Fdata",[1167,8779,8427],{"class":1216},[1167,8781,8783,8785,8787,8790,8793,8796,8798,8801],{"class":1169,"line":8782},29,[1167,8784,7985],{"class":7930},[1167,8786,7634],{"class":1216},[1167,8788,8789],{"class":7930},"db-data",[1167,8791,8792],{"class":1216},": {}, ",[1167,8794,8795],{"class":7930},"cache-data",[1167,8797,8792],{"class":1216},[1167,8799,8800],{"class":7930},"uploads",[1167,8802,8803],{"class":1216},": {} }\n",[15,8805,8806,8808,8809,8812,8813,8816,8817,8819,8820,8822,8823,8826,8827,2881,8830,8833],{},[19,8807,8476],{}," 那段里 ",[19,8810,8811],{},"\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata"," 这类目标路径",[31,8814,8815],{},"必须抄官方文档，不能凭印象","。写错成 ",[19,8818,7013],{}," 时 Compose 不会报错，只会在镜像里建一个空目录，数据静默落在容器可写层，",[19,8821,7883],{}," 看着正常、",[19,8824,8825],{},"down"," 之后全丢——这是教程里最难自查的一类错误。用 ",[19,8828,8829],{},"docker diff shop-db",[19,8831,8832],{},"docker exec shop-db ls \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata"," 一眼就能看出数据没进卷。",[40,8835,8837],{"id":8836},"depends_on-只保证顺序不保证就绪","depends_on 只保证顺序，不保证就绪",[15,8839,8840,8841,8844,8845,8848],{},"这是 Compose 最坑人的一个字段。",[19,8842,8843],{},"depends_on: [db]"," 的含义只是「先创建并启动 db 容器」，而容器启动 ≠ 数据库能接受连接。Postgres 初始化要几秒，你的 API 在此期间 ",[19,8846,8847],{},"ECONNREFUSED","，然后崩溃退出。",[15,8850,8851,8852,888,8854,8856,8857,8859],{},"正确写法是 ",[19,8853,8228],{},[19,8855,8481],{},"，而 ",[19,8858,8481],{}," 依赖健康检查：",[1158,8861,8863],{"className":7921,"code":8862,"language":7923,"meta":1163,"style":1163},"    depends_on:\n      db: { condition: service_healthy }        # 等 db 的 healthcheck 通过才起 api\n      cache: { condition: service_started }     # 只要起来了（等价于旧写法）\n",[19,8864,8865,8871,8890],{"__ignoreMap":1163},[1167,8866,8867,8869],{"class":1169,"line":1170},[1167,8868,8471],{"class":7930},[1167,8870,7999],{"class":1216},[1167,8872,8873,8876,8878,8880,8882,8884,8887],{"class":1169,"line":1177},[1167,8874,8875],{"class":7930},"      db",[1167,8877,7634],{"class":1216},[1167,8879,8481],{"class":7930},[1167,8881,7620],{"class":1216},[1167,8883,8486],{"class":1183},[1167,8885,8886],{"class":1216}," }        ",[1167,8888,8889],{"class":3604},"# 等 db 的 healthcheck 通过才起 api\n",[1167,8891,8892,8895,8897,8899,8901,8903,8906],{"class":1169,"line":1202},[1167,8893,8894],{"class":7930},"      cache",[1167,8896,7634],{"class":1216},[1167,8898,8481],{"class":7930},[1167,8900,7620],{"class":1216},[1167,8902,8501],{"class":1183},[1167,8904,8905],{"class":1216}," }     ",[1167,8907,8908],{"class":3604},"# 只要起来了（等价于旧写法）\n",[15,8910,8911,8912,8914,8915,8917,8918,8920,8921,8924],{},"可选值有三种：",[19,8913,8501],{},"（默认）、",[19,8916,8486],{},"（要求被依赖方有 ",[19,8919,8244],{},"，否则 Compose 报错）、",[19,8922,8923],{},"service_completed_successfully","（等对方退出码 0，适合 migration\u002Finit 容器）。",[15,8926,8927,8929,8930,8933,8934,8936],{},[19,8928,8481],{}," 解决的是「应用启动时的瞬时失败」，但",[31,8931,8932],{},"运行期依赖方挂了依然要你自己的重连逻辑","。别指望 ",[19,8935,8228],{}," 提供高可用。",[40,8938,8940],{"id":8939},"日常命令与那个危险的-v","日常命令与那个危险的 -v",[1158,8942,8944],{"className":1160,"code":8943,"language":1162,"meta":1163,"style":1163},"docker compose config            # 校验并打印合并渲染后的最终配置（排错第一步）\ndocker compose up -d             # 对齐期望状态：缺的建、变的换\ndocker compose up -d --build     # 顺带重新构建 build 的服务\ndocker compose up -d --force-recreate api   # 配置没变但想换镜像\u002F环境变量时强制重建\ndocker compose ps                # 只看本项目的容器（含 health 状态）\ndocker compose logs -f --tail 100 api\ndocker compose exec api sh       # 进容器（exec 不是 run，不会新建）\ndocker compose run --rm api node scripts\u002Fmigrate.js   # 起一次性容器，可覆盖 command\ndocker compose scale api=3\ndocker compose stop \u002F start \u002F restart\ndocker compose down              # 停并删容器和网络（卷和数据不动）\ndocker compose down -v          # 连命名卷一起删 —— 数据库清空\n",[19,8945,8946,8958,8971,8987,9005,9016,9033,9048,9069,9083,9101,9113],{"__ignoreMap":1163},[1167,8947,8948,8950,8952,8955],{"class":1169,"line":1170},[1167,8949,559],{"class":1173},[1167,8951,7896],{"class":1183},[1167,8953,8954],{"class":1183}," config",[1167,8956,8957],{"class":3604},"            # 校验并打印合并渲染后的最终配置（排错第一步）\n",[1167,8959,8960,8962,8964,8966,8968],{"class":1169,"line":1177},[1167,8961,559],{"class":1173},[1167,8963,7896],{"class":1183},[1167,8965,7911],{"class":1183},[1167,8967,5611],{"class":2703},[1167,8969,8970],{"class":3604},"             # 对齐期望状态：缺的建、变的换\n",[1167,8972,8973,8975,8977,8979,8981,8984],{"class":1169,"line":1202},[1167,8974,559],{"class":1173},[1167,8976,7896],{"class":1183},[1167,8978,7911],{"class":1183},[1167,8980,5611],{"class":2703},[1167,8982,8983],{"class":2703}," --build",[1167,8985,8986],{"class":3604},"     # 顺带重新构建 build 的服务\n",[1167,8988,8989,8991,8993,8995,8997,9000,9002],{"class":1169,"line":1208},[1167,8990,559],{"class":1173},[1167,8992,7896],{"class":1183},[1167,8994,7911],{"class":1183},[1167,8996,5611],{"class":2703},[1167,8998,8999],{"class":2703}," --force-recreate",[1167,9001,5626],{"class":1183},[1167,9003,9004],{"class":3604},"   # 配置没变但想换镜像\u002F环境变量时强制重建\n",[1167,9006,9007,9009,9011,9013],{"class":1169,"line":1238},[1167,9008,559],{"class":1173},[1167,9010,7896],{"class":1183},[1167,9012,4819],{"class":1183},[1167,9014,9015],{"class":3604},"                # 只看本项目的容器（含 health 状态）\n",[1167,9017,9018,9020,9022,9024,9026,9028,9030],{"class":1169,"line":1257},[1167,9019,559],{"class":1173},[1167,9021,7896],{"class":1183},[1167,9023,4444],{"class":1183},[1167,9025,2719],{"class":2703},[1167,9027,4449],{"class":2703},[1167,9029,4452],{"class":2703},[1167,9031,9032],{"class":1183}," api\n",[1167,9034,9035,9037,9039,9041,9043,9045],{"class":1169,"line":1274},[1167,9036,559],{"class":1173},[1167,9038,7896],{"class":1183},[1167,9040,4629],{"class":1183},[1167,9042,5626],{"class":1183},[1167,9044,4637],{"class":1183},[1167,9046,9047],{"class":3604},"       # 进容器（exec 不是 run，不会新建）\n",[1167,9049,9050,9052,9054,9056,9058,9060,9063,9066],{"class":1169,"line":1290},[1167,9051,559],{"class":1173},[1167,9053,7896],{"class":1183},[1167,9055,3380],{"class":1183},[1167,9057,3386],{"class":2703},[1167,9059,5626],{"class":1183},[1167,9061,9062],{"class":1183}," node",[1167,9064,9065],{"class":1183}," scripts\u002Fmigrate.js",[1167,9067,9068],{"class":3604},"   # 起一次性容器，可覆盖 command\n",[1167,9070,9071,9073,9075,9078,9081],{"class":1169,"line":1304},[1167,9072,559],{"class":1173},[1167,9074,7896],{"class":1183},[1167,9076,9077],{"class":1183}," scale",[1167,9079,9080],{"class":1183}," api=",[1167,9082,8556],{"class":2703},[1167,9084,9085,9087,9089,9091,9094,9096,9098],{"class":1169,"line":1310},[1167,9086,559],{"class":1173},[1167,9088,7896],{"class":1183},[1167,9090,3646],{"class":1183},[1167,9092,9093],{"class":1183}," \u002F",[1167,9095,3612],{"class":1183},[1167,9097,9093],{"class":1183},[1167,9099,9100],{"class":1183}," restart\n",[1167,9102,9103,9105,9107,9110],{"class":1169,"line":3259},[1167,9104,559],{"class":1173},[1167,9106,7896],{"class":1183},[1167,9108,9109],{"class":1183}," down",[1167,9111,9112],{"class":3604},"              # 停并删容器和网络（卷和数据不动）\n",[1167,9114,9115,9117,9119,9121,9123],{"class":1169,"line":3264},[1167,9116,559],{"class":1173},[1167,9118,7896],{"class":1183},[1167,9120,9109],{"class":1183},[1167,9122,6285],{"class":2703},[1167,9124,9125],{"class":3604},"          # 连命名卷一起删 —— 数据库清空\n",[1318,9127,9128,9143,9171,9177],{},[584,9129,9130,9136,9137,9139,9140,616],{},[31,9131,9132,9135],{},[19,9133,9134],{},"down -v"," 会删除本项目声明的所有命名卷","，不可恢复。习惯是永远写 ",[19,9138,8825],{},"，要清数据时明确地用 ",[19,9141,9142],{},"docker volume rm \u003C精确名字>",[584,9144,9145,9147,9148,9154,9155,9157,9158,9161,9162,9164,9165,888,9168,616],{},[19,9146,7883],{}," 的变更判定很保守：",[31,9149,9150,9151,9153],{},"只改镜像 tag、只改 ",[19,9152,8171],{}," 的值都会重建容器","；只改 ",[19,9156,8140],{}," 的宿主侧也一样。但它",[31,9159,9160],{},"不会","因为 ",[19,9163,7947],{}," 内容变了（同 tag 重新 push）自动重建——这时要 ",[19,9166,9167],{},"--pull always",[19,9169,9170],{},"--force-recreate",[584,9172,9173,9176],{},[19,9174,9175],{},"config"," 是理解 Compose 的第一工具：插值、多文件叠加、锚点展开的最终结果都在它输出里，出问题先看它。",[584,9178,9179,9180,114,9183,9186,9187,793,9189,9192,9193,9196,9197,9192,9200,9203],{},"项目名决定资源前缀（",[19,9181,9182],{},"shop_api_1",[19,9184,9185],{},"shop_db-data","）；来源优先级是 ",[19,9188,4743],{},[19,9190,9191],{},"--project-name"," > ",[19,9194,9195],{},"COMPOSE_PROJECT_NAME"," > 顶层 ",[19,9198,9199],{},"name:",[31,9201,9202],{},"当前目录名","。同一个目录不同路径跑起来会变成两个项目，这是「明明 up 了但 ps 看不到」的常见原因。",[15,9205,9206,9207,9210,9211,4727,9213,9216,9217,9220],{},"示例 11 是 ",[19,9208,9209],{},"up -d"," 的完整输出流：从网络创建、卷创建、拉镜像、按依赖顺序起四个容器，到 ",[19,9212,325],{},[19,9214,9215],{},"health: starting"," 转 ",[19,9218,9219],{},"healthy"," 的全过程。",[1405,9222],{"height":5320,"src":9223,"title":9224},"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F11-compose-up.html","示例 11：Compose up 的输出流——从 yaml 到四个容器",[15,9226,9227],{},"一个文件能描述服务，但还不够「生产」。下一章处理多环境、变量、锚点与那些必须显式声明的开关。",[10,9229,9231],{"id":9230},"十二compose-进阶变量插值多文件叠加与生产风格配置","十二、Compose 进阶：变量插值、多文件叠加与生产风格配置",[40,9233,9235],{"id":9234},"变量与-env插值语法","变量与 .env：插值语法",[1158,9237,9239],{"className":7921,"code":9238,"language":7923,"meta":1163,"style":1163},"services:\n  api:\n    image: ghcr.io\u002Fexample\u002Fshop-api:${APP_VERSION:-1.8.3}\n    ports:\n      - \"${HTTP_PORT:-8080}:3000\"\n    environment:\n      DATABASE_URL: postgres:\u002F\u002F${DB_USER:-shop}:${DB_PASSWORD:?set DB_PASSWORD in .env}@db:5432\u002Fshop\n      REPLICAS: ${SHARD_COUNT}\n",[19,9240,9241,9247,9253,9262,9268,9276,9282,9292],{"__ignoreMap":1163},[1167,9242,9243,9245],{"class":1169,"line":1170},[1167,9244,7931],{"class":7930},[1167,9246,7999],{"class":1216},[1167,9248,9249,9251],{"class":1169,"line":1177},[1167,9250,8364],{"class":7930},[1167,9252,7999],{"class":1216},[1167,9254,9255,9257,9259],{"class":1169,"line":1202},[1167,9256,8409],{"class":7930},[1167,9258,7620],{"class":1216},[1167,9260,9261],{"class":1183},"ghcr.io\u002Fexample\u002Fshop-api:${APP_VERSION:-1.8.3}\n",[1167,9263,9264,9266],{"class":1169,"line":1208},[1167,9265,8419],{"class":7930},[1167,9267,7999],{"class":1216},[1167,9269,9270,9273],{"class":1169,"line":1238},[1167,9271,9272],{"class":1216},"      - ",[1167,9274,9275],{"class":1183},"\"${HTTP_PORT:-8080}:3000\"\n",[1167,9277,9278,9280],{"class":1169,"line":1257},[1167,9279,8432],{"class":7930},[1167,9281,7999],{"class":1216},[1167,9283,9284,9287,9289],{"class":1169,"line":1274},[1167,9285,9286],{"class":7930},"      DATABASE_URL",[1167,9288,7620],{"class":1216},[1167,9290,9291],{"class":1183},"postgres:\u002F\u002F${DB_USER:-shop}:${DB_PASSWORD:?set DB_PASSWORD in .env}@db:5432\u002Fshop\n",[1167,9293,9294,9297,9299],{"class":1169,"line":1290},[1167,9295,9296],{"class":7930},"      REPLICAS",[1167,9298,7620],{"class":1216},[1167,9300,9301],{"class":1183},"${SHARD_COUNT}\n",[45,9303,9304,9314],{},[48,9305,9306],{},[51,9307,9308,9311],{},[54,9309,9310],{},"语法",[54,9312,9313],{},"行为",[67,9315,9316,9326,9336,9346,9360,9370],{},[51,9317,9318,9323],{},[72,9319,9320],{},[19,9321,9322],{},"${VAR}",[72,9324,9325],{},"未定义时替换为空字符串",[51,9327,9328,9333],{},[72,9329,9330],{},[19,9331,9332],{},"${VAR:-默认}",[72,9334,9335],{},"未定义或为空 → 用默认（值不进容器，只在 Compose 层展开）",[51,9337,9338,9343],{},[72,9339,9340],{},[19,9341,9342],{},"${VAR-default}",[72,9344,9345],{},"仅未定义时用默认（空串保留）",[51,9347,9348,9353],{},[72,9349,9350],{},[19,9351,9352],{},"${VAR:?错误信息}",[72,9354,9355,9356,9359],{},"未定义或为空 → ",[31,9357,9358],{},"直接报错退出","（强制必填，很好的守卫）",[51,9361,9362,9367],{},[72,9363,9364],{},[19,9365,9366],{},"${VAR?错误信息}",[72,9368,9369],{},"仅未定义时报错",[51,9371,9372,9380],{},[72,9373,9374,3177,9377],{},[19,9375,9376],{},"${VAR:?}",[19,9378,9379],{},"$$",[72,9381,9382,9384,9385,9388,9389,9391],{},[19,9383,9379],{}," 是转义，输出一个 ",[19,9386,9387],{},"$","（防止应用密钥里带 ",[19,9390,9387],{}," 被二次展开）",[15,9393,9394,9396,9397,9400,9401,9404,9405,9408,9409,9412],{},[19,9395,3167],{},"（与 ",[19,9398,9399],{},"compose.yaml"," 同目录）里的值供给上面的",[31,9402,9403],{},"插值","，作用范围是整个 Compose 文件；",[19,9406,9407],{},"env_file:"," 注入的是",[31,9410,9411],{},"容器内环境变量","，不参与文件插值。两者都叫「环境变量」但层次完全不同，这是第十三章的第一条主线。",[15,9414,9415,9416,9419,9420,616],{},"插值发生在 Compose 层，因此 ",[19,9417,9418],{},"docker compose config"," 输出里你看到的一定是展开后的值——",[31,9421,9422,9423],{},"检查变量到底吃到没有，看 ",[19,9424,9175],{},[40,9426,9428],{"id":9427},"多文件叠加base-override","多文件叠加：base + override",[1158,9430,9432],{"className":1160,"code":9431,"language":1162,"meta":1163,"style":1163},"docker compose -f compose.yaml -f compose.prod.yaml up -d\ndocker compose -f compose.yaml -f compose.override.yaml config   # 看合并结果\n",[19,9433,9434,9454],{"__ignoreMap":1163},[1167,9435,9436,9438,9440,9442,9445,9447,9450,9452],{"class":1169,"line":1170},[1167,9437,559],{"class":1173},[1167,9439,7896],{"class":1183},[1167,9441,2719],{"class":2703},[1167,9443,9444],{"class":1183}," compose.yaml",[1167,9446,2719],{"class":2703},[1167,9448,9449],{"class":1183}," compose.prod.yaml",[1167,9451,7911],{"class":1183},[1167,9453,7914],{"class":2703},[1167,9455,9456,9458,9460,9462,9464,9466,9469,9471],{"class":1169,"line":1177},[1167,9457,559],{"class":1173},[1167,9459,7896],{"class":1183},[1167,9461,2719],{"class":2703},[1167,9463,9444],{"class":1183},[1167,9465,2719],{"class":2703},[1167,9467,9468],{"class":1183}," compose.override.yaml",[1167,9470,8954],{"class":1183},[1167,9472,9473],{"class":3604},"   # 看合并结果\n",[15,9475,9476,9477,888,9479,9482,9483,9485,9486,9489],{},"同名文件自动叠加规则（Compose v2）：",[19,9478,9399],{},[19,9480,9481],{},"compose.override.yaml","（开发用，自动加载）；显式 ",[19,9484,2772],{}," 时按给定顺序合并，",[31,9487,9488],{},"后者覆盖前者","。合并语义很关键：",[45,9491,9492,9501],{},[48,9493,9494],{},[51,9495,9496,9498],{},[54,9497,6148],{},[54,9499,9500],{},"合并规则",[67,9502,9503,9515,9530,9548,9557,9570],{},[51,9504,9505,9512],{},[72,9506,9507,9508,114,9510,1075],{},"标量（",[19,9509,7947],{},[19,9511,8260],{},[72,9513,9514],{},"后面的直接覆盖",[51,9516,9517,9524],{},[72,9518,9519,728,9521],{},[19,9520,8171],{},[19,9522,9523],{},"labels",[72,9525,9526,9529],{},[31,9527,9528],{},"按键合并","，同名覆盖",[51,9531,9532,9541],{},[72,9533,9534,728,9536,728,9538,9540],{},[19,9535,8140],{},[19,9537,7985],{},[19,9539,8276],{}," 等列表",[72,9542,9543,9544,9547],{},"一般",[31,9545,9546],{},"整体替换","（不是追加！）——想追加要写全",[51,9549,9550,9555],{},[72,9551,9552],{},[19,9553,9554],{},"build.args",[72,9556,9528],{},[51,9558,9559,9563],{},[72,9560,9561],{},[19,9562,8244],{},[72,9564,9565,9566,9569],{},"子字段逐个覆盖；",[19,9567,9568],{},"disable: true"," 可关掉",[51,9571,9572,9577],{},[72,9573,9574],{},[19,9575,9576],{},"deploy",[72,9578,9579],{},"深度合并",[15,9581,9582,9583,9585,9586,9588],{},"「列表是替换不是追加」这点最容易被误解：override 文件里写一条 ",[19,9584,8140],{}," 并不会给 base 加一个端口，而是替换掉整个 ",[19,9587,8140],{}," 列表。",[15,9590,9591,9592,9594,9595,9597,9598,9600,9601,9604,9605,9607,9608,9617],{},"典型分层：",[19,9593,9399],{},"（公共）+ ",[19,9596,9481],{},"（本地开发：bind mount 源码、开调试端口、",[19,9599,8276],{}," 换成 nodemon）+ ",[19,9602,9603],{},"compose.prod.yaml","（生产：镜像 digest、",[19,9606,8260],{},"、资源限制、只挂卷）。",[31,9609,9610,9611,9613,9614,9616],{},"把 ",[19,9612,9481],{}," 加进 ",[19,9615,3100],{}," 或不提交","，避免开发者机器上的配置跑到生产。",[40,9619,9621],{"id":9620},"扩展字段与锚点复用又不产生隐式行为","扩展字段与锚点：复用又不产生隐式行为",[1158,9623,9625],{"className":7921,"code":9624,"language":7923,"meta":1163,"style":1163},"x-common: &common\n  restart: unless-stopped\n  logging: { driver: json-file, options: { max-size: \"20m\", max-file: \"3\" } }\n  security_opt: [\"no-new-privileges:true\"]\n  networks: [shop-net]\n\nservices:\n  api:\n    \u003C\u003C: *common\n    environment: { TZ: Asia\u002FShanghai, NODE_ENV: production, PORT: \"3000\" }\n  cache:\n    \u003C\u003C: *common\n    image: redis:7.2-alpine\n",[19,9626,9627,9640,9649,9688,9700,9712,9718,9724,9730,9741,9774,9780,9790],{"__ignoreMap":1163},[1167,9628,9629,9632,9634,9637],{"class":1169,"line":1170},[1167,9630,9631],{"class":7930},"x-common",[1167,9633,7620],{"class":1216},[1167,9635,9636],{"class":3505},"&",[1167,9638,9639],{"class":1173},"common\n",[1167,9641,9642,9645,9647],{"class":1169,"line":1177},[1167,9643,9644],{"class":7930},"  restart",[1167,9646,7620],{"class":1216},[1167,9648,8576],{"class":1183},[1167,9650,9651,9654,9656,9658,9660,9663,9665,9668,9670,9673,9675,9677,9679,9682,9684,9686],{"class":1169,"line":1202},[1167,9652,9653],{"class":7930},"  logging",[1167,9655,7634],{"class":1216},[1167,9657,7974],{"class":7930},[1167,9659,7620],{"class":1216},[1167,9661,9662],{"class":1183},"json-file",[1167,9664,1223],{"class":1216},[1167,9666,9667],{"class":7930},"options",[1167,9669,7634],{"class":1216},[1167,9671,9672],{"class":7930},"max-size",[1167,9674,7620],{"class":1216},[1167,9676,7642],{"class":1183},[1167,9678,1223],{"class":1216},[1167,9680,9681],{"class":7930},"max-file",[1167,9683,7620],{"class":1216},[1167,9685,7652],{"class":1183},[1167,9687,8404],{"class":1216},[1167,9689,9690,9693,9695,9698],{"class":1169,"line":1208},[1167,9691,9692],{"class":7930},"  security_opt",[1167,9694,7461],{"class":1216},[1167,9696,9697],{"class":1183},"\"no-new-privileges:true\"",[1167,9699,8427],{"class":1216},[1167,9701,9702,9705,9707,9710],{"class":1169,"line":1238},[1167,9703,9704],{"class":7930},"  networks",[1167,9706,7461],{"class":1216},[1167,9708,9709],{"class":1183},"shop-net",[1167,9711,8427],{"class":1216},[1167,9713,9714],{"class":1169,"line":1257},[1167,9715,9717],{"emptyLinePlaceholder":9716},true,"\n",[1167,9719,9720,9722],{"class":1169,"line":1274},[1167,9721,7931],{"class":7930},[1167,9723,7999],{"class":1216},[1167,9725,9726,9728],{"class":1169,"line":1290},[1167,9727,8364],{"class":7930},[1167,9729,7999],{"class":1216},[1167,9731,9732,9735,9737,9739],{"class":1169,"line":1304},[1167,9733,9734],{"class":2703},"    \u003C\u003C",[1167,9736,7620],{"class":1216},[1167,9738,6623],{"class":3505},[1167,9740,9639],{"class":1216},[1167,9742,9743,9745,9747,9749,9751,9754,9756,9758,9760,9762,9764,9767,9769,9772],{"class":1169,"line":1310},[1167,9744,8432],{"class":7930},[1167,9746,7634],{"class":1216},[1167,9748,2255],{"class":7930},[1167,9750,7620],{"class":1216},[1167,9752,9753],{"class":1183},"Asia\u002FShanghai",[1167,9755,1223],{"class":1216},[1167,9757,2249],{"class":7930},[1167,9759,7620],{"class":1216},[1167,9761,8401],{"class":1183},[1167,9763,1223],{"class":1216},[1167,9765,9766],{"class":7930},"PORT",[1167,9768,7620],{"class":1216},[1167,9770,9771],{"class":1183},"\"3000\"",[1167,9773,7655],{"class":1216},[1167,9775,9776,9778],{"class":1169,"line":3259},[1167,9777,8732],{"class":7930},[1167,9779,7999],{"class":1216},[1167,9781,9782,9784,9786,9788],{"class":1169,"line":3264},[1167,9783,9734],{"class":2703},[1167,9785,7620],{"class":1216},[1167,9787,6623],{"class":3505},[1167,9789,9639],{"class":1216},[1167,9791,9792,9794,9796],{"class":1169,"line":3269},[1167,9793,8409],{"class":7930},[1167,9795,7620],{"class":1216},[1167,9797,8744],{"class":1183},[15,9799,9800,9803,9804,9807,9808,9811,9812,9815,9816,9819,9820,9823,9824,9826,9827,9830],{},[19,9801,9802],{},"x-"," 前缀的顶层键是",[31,9805,9806],{},"扩展字段","，Compose 忽略它，但允许在里面放可复用片段；",[19,9809,9810],{},"&anchor"," 定义锚点、",[19,9813,9814],{},"*alias"," 引用、",[19,9817,9818],{},"\u003C\u003C:"," 做映射合并。收益是少抄 20 行，代价是",[31,9821,9822],{},"改一处会同时影响多个服务","（生产上这常常正是你想要的），以及覆盖顺序的心智负担（",[19,9825,9818],{}," 中已显式写出的键优先）。锚点是 YAML 特性，别指望它做复杂逻辑——真要参数化就换 ",[19,9828,9829],{},"envsubst"," 模板或 Helm。",[40,9832,9834],{"id":9833},"profiles按需启动","profiles：按需启动",[1158,9836,9838],{"className":7921,"code":9837,"language":7923,"meta":1163,"style":1163},"services:\n  adminer: { image: adminer:4, profiles: [\"debug\"] }\n  migrate: { build: ., command: [\"node\", \"scripts\u002Fmigrate.js\"], profiles: [\"ops\"] }\n",[19,9839,9840,9846,9871],{"__ignoreMap":1163},[1167,9841,9842,9844],{"class":1169,"line":1170},[1167,9843,7931],{"class":7930},[1167,9845,7999],{"class":1216},[1167,9847,9848,9851,9853,9855,9857,9860,9862,9864,9866,9869],{"class":1169,"line":1177},[1167,9849,9850],{"class":7930},"  adminer",[1167,9852,7634],{"class":1216},[1167,9854,7947],{"class":7930},[1167,9856,7620],{"class":1216},[1167,9858,9859],{"class":1183},"adminer:4",[1167,9861,1223],{"class":1216},[1167,9863,8339],{"class":7930},[1167,9865,7461],{"class":1216},[1167,9867,9868],{"class":1183},"\"debug\"",[1167,9870,7467],{"class":1216},[1167,9872,9873,9876,9878,9880,9882,9884,9886,9888,9890,9892,9894,9897,9899,9901,9903,9906],{"class":1169,"line":1202},[1167,9874,9875],{"class":7930},"  migrate",[1167,9877,7634],{"class":1216},[1167,9879,8102],{"class":7930},[1167,9881,7620],{"class":1216},[1167,9883,2872],{"class":2703},[1167,9885,1223],{"class":1216},[1167,9887,8276],{"class":7930},[1167,9889,7461],{"class":1216},[1167,9891,1220],{"class":1183},[1167,9893,1223],{"class":1216},[1167,9895,9896],{"class":1183},"\"scripts\u002Fmigrate.js\"",[1167,9898,8706],{"class":1216},[1167,9900,8339],{"class":7930},[1167,9902,7461],{"class":1216},[1167,9904,9905],{"class":1183},"\"ops\"",[1167,9907,7467],{"class":1216},[1158,9909,9911],{"className":1160,"code":9910,"language":1162,"meta":1163,"style":1163},"docker compose --profile debug up -d\ndocker compose --profile ops run migrate\n",[19,9912,9913,9929],{"__ignoreMap":1163},[1167,9914,9915,9917,9919,9922,9925,9927],{"class":1169,"line":1170},[1167,9916,559],{"class":1173},[1167,9918,7896],{"class":1183},[1167,9920,9921],{"class":2703}," --profile",[1167,9923,9924],{"class":1183}," debug",[1167,9926,7911],{"class":1183},[1167,9928,7914],{"class":2703},[1167,9930,9931,9933,9935,9937,9940,9942],{"class":1169,"line":1177},[1167,9932,559],{"class":1173},[1167,9934,7896],{"class":1183},[1167,9936,9921],{"class":2703},[1167,9938,9939],{"class":1183}," ops",[1167,9941,3380],{"class":1183},[1167,9943,9944],{"class":1183}," migrate\n",[15,9946,9947,9948,9951,9952,9954],{},"没有 profile 的服务总是启动；有 profile 的只有匹配 ",[19,9949,9950],{},"--profile"," 才启动。用途是把「调试面板、一次性任务、测试桩」从常规 ",[19,9953,7883],{}," 里摘出去，而不是靠注释代码。",[40,9956,9957],{"id":9957},"网络与外部资源",[1158,9959,9961],{"className":7921,"code":9960,"language":7923,"meta":1163,"style":1163},"networks:\n  shop-net:\n    name: shop-net           # 显式命名，不带项目前缀，便于项目外复用\n  edge:\n    external: true           # 引用已存在的网络，up\u002Fdown 都不许删它\n  db-only:\n    internal: true           # 出不去外网，只能容器间互访\n",[19,9962,9963,9969,9975,9987,9994,10006,10013],{"__ignoreMap":1163},[1167,9964,9965,9967],{"class":1169,"line":1170},[1167,9966,7959],{"class":7930},[1167,9968,7999],{"class":1216},[1167,9970,9971,9973],{"class":1169,"line":1177},[1167,9972,7969],{"class":7930},[1167,9974,7999],{"class":1216},[1167,9976,9977,9980,9982,9984],{"class":1169,"line":1202},[1167,9978,9979],{"class":7930},"    name",[1167,9981,7620],{"class":1216},[1167,9983,9709],{"class":1183},[1167,9985,9986],{"class":3604},"           # 显式命名，不带项目前缀，便于项目外复用\n",[1167,9988,9989,9992],{"class":1169,"line":1208},[1167,9990,9991],{"class":7930},"  edge",[1167,9993,7999],{"class":1216},[1167,9995,9996,9999,10001,10003],{"class":1169,"line":1238},[1167,9997,9998],{"class":7930},"    external",[1167,10000,7620],{"class":1216},[1167,10002,8187],{"class":2703},[1167,10004,10005],{"class":3604},"           # 引用已存在的网络，up\u002Fdown 都不许删它\n",[1167,10007,10008,10011],{"class":1169,"line":1257},[1167,10009,10010],{"class":7930},"  db-only",[1167,10012,7999],{"class":1216},[1167,10014,10015,10018,10020,10022],{"class":1169,"line":1274},[1167,10016,10017],{"class":7930},"    internal",[1167,10019,7620],{"class":1216},[1167,10021,8187],{"class":2703},[1167,10023,10024],{"class":3604},"           # 出不去外网，只能容器间互访\n",[15,10026,10027,1372,10030,10033,10034,10036,10037,10040,10041,10044,10045,10048,10049,1962],{},[31,10028,10029],{},"默认网络",[19,10031,10032],{},"\u003Cproject>_default","）由 Compose 自动创建与销毁，服务默认全接进去、互相可按名字访问。显式自定义网络的好处是：能用 ",[19,10035,9199],{}," 固定名字给项目外用、能 ",[19,10038,10039],{},"internal","、能用多张网做隔离（前端接 ",[19,10042,10043],{},"edge","、数据库只接 ",[19,10046,10047],{},"db-only","，于是 web 根本 ping 不到 db——",[31,10050,10051],{},"网络层强制了架构分层",[15,10053,10054,10057,10058,10060,10061,10067],{},[19,10055,10056],{},"external: true"," 把资源和容器生命周期解耦，",[19,10059,8825],{}," 不会删它。",[31,10062,10063,10064,10066],{},"生产里数据库卷推荐显式声明（带 ",[19,10065,9199],{}," 或 external）","，防止换项目名（换目录）后「数据凭空消失」（其实还在旧卷里，这类事故很常见）。",[40,10069,10070],{"id":10070},"那些必须显式写的开关",[45,10072,10073,10084],{},[48,10074,10075],{},[51,10076,10077,10079,10081],{},[54,10078,687],{},[54,10080,1566],{},[54,10082,10083],{},"为什么要显式写",[67,10085,10086,10114,10127,10140,10159,10177,10192,10207,10232],{},[51,10087,10088,10092,10106],{},[72,10089,10090],{},[19,10091,7828],{},[72,10093,10094,10097,10098,728,10101,728,10103],{},[19,10095,10096],{},"missing","（默认）\u002F ",[19,10099,10100],{},"always",[19,10102,8102],{},[19,10104,10105],{},"never",[72,10107,10108,10109,10111,10112],{},"同 tag 覆盖镜像时，",[19,10110,10100],{}," 才能确保拉到新的；离线环境用 ",[19,10113,10105],{},[51,10115,10116,10121,10124],{},[72,10117,10118],{},[19,10119,10120],{},"init: true",[72,10122,10123],{},"容器里跑 tini 当 PID 1",[72,10125,10126],{},"否则僵尸进程不收尸、SIGTERM 转发有问题（第十四、十六章）",[51,10128,10129,10134,10137],{},[72,10130,10131],{},[19,10132,10133],{},"stop_grace_period: 30s",[72,10135,10136],{},"SIGTERM 后等多久发 SIGKILL",[72,10138,10139],{},"默认 10s；长任务\u002F大流量服务要加长",[51,10141,10142,10147,10153],{},[72,10143,10144],{},[19,10145,10146],{},"shm_size: 256m",[72,10148,10149,10152],{},[19,10150,10151],{},"\u002Fdev\u002Fshm"," 大小",[72,10154,10155,10156],{},"默认 64MB，Chrome\u002FPuppeteer、Postgres 并行扫描会撑爆并报 ",[19,10157,10158],{},"No space left on device",[51,10160,10161,10166,10174],{},[72,10162,10163],{},[19,10164,10165],{},"ulimits",[72,10167,10168,114,10171],{},[19,10169,10170],{},"nofile: 65536",[19,10172,10173],{},"nproc",[72,10175,10176],{},"宿主默认 1024 文件描述符，高并发必调",[51,10178,10179,10184,10189],{},[72,10180,10181],{},[19,10182,10183],{},"extra_hosts",[72,10185,10186],{},[19,10187,10188],{},"host.docker.internal:host-gateway",[72,10190,10191],{},"Linux 上访问宿主服务的前提（第八章）",[51,10193,10194,10201,10204],{},[72,10195,10196,888,10199],{},[19,10197,10198],{},"readonly: true",[19,10200,6231],{},[72,10202,10203],{},"根文件系统只读",[72,10205,10206],{},"第十五章安全基线",[51,10208,10209,10214,10222],{},[72,10210,10211],{},[19,10212,10213],{},"deploy.resources.limits",[72,10215,10216,114,10219],{},[19,10217,10218],{},"memory: 512M",[19,10220,10221],{},"cpus: \"1.5\"",[72,10223,10224,10225,10227,10228,10231],{},"Compose v2 在单机 ",[19,10226,7883],{}," 时也会应用 limits（K8s 迁移友好，但 ",[19,10229,10230],{},"replicas"," 等仍需 Swarm）",[51,10233,10234,10239,10245],{},[72,10235,10236],{},[19,10237,10238],{},"logging",[72,10240,10241,728,10243],{},[19,10242,9672],{},[19,10244,7974],{},[72,10246,10247,10248,10251],{},"默认 json-file ",[31,10249,10250],{},"不轮转","，磁盘必爆（第十六章）",[40,10253,10254],{"id":10254},"一个生产风格的完整例子",[1158,10256,10258],{"className":7921,"code":10257,"language":7923,"meta":1163,"style":1163},"name: shop\nx-service: &service\n  restart: unless-stopped\n  init: true\n  stop_grace_period: 30s\n  security_opt: [\"no-new-privileges:true\"]\n  cap_drop: [\"ALL\"]\n  read_only: true\n  tmpfs: [\"\u002Ftmp\"]\n  logging: { driver: json-file, options: { max-size: \"20m\", max-file: \"3\" } }\n  ulimits: { nofile: 65536 }\n  networks: [shop-net]\nservices:\n  web:\n    \u003C\u003C: *service\n    image: nginx:1.27-alpine@sha256:2bc60bb1c9d0\n    ports: [\"80:80\", \"127.0.0.1:443:443\"]\n    volumes: [\".\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro\", \".\u002Fnginx\u002Fcerts:\u002Fetc\u002Fnginx\u002Fcerts:ro\"]\n    depends_on: { api: { condition: service_healthy } }\n  api:\n    \u003C\u003C: *service\n    image: ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0ea7b2d4\n    expose: [\"3000\"]\n    environment: { NODE_ENV: production, TZ: Asia\u002FShanghai }\n    configs: [{ source: app-conf, target: \u002Fetc\u002Fshop\u002Fconfig.yaml }]\n    secrets: [database_url]\n    healthcheck:\n      test: [\"CMD\", \"node\", \"-e\", \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"]\n      interval: 15s\n      timeout: 3s\n      retries: 3\n      start_period: 20s\n    deploy: { resources: { limits: { memory: 512M, cpus: \"1.5\" } } }\n    depends_on: { db: { condition: service_healthy }, cache: { condition: service_started } }\n  db:\n    \u003C\u003C: *service\n    image: postgres:16.4@sha256:6d7df8c5b0f8\n    environment: { POSTGRES_PASSWORD_FILE: \u002Frun\u002Fsecrets\u002Fdb_password }\n    volumes: [db-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata]\n    secrets: [db_password]\n    networks: [shop-net, db-only]\n    healthcheck: { test: [\"CMD-SHELL\", \"pg_isready -U shop\"], interval: 10s, retries: 5 }\n  cache:\n    \u003C\u003C: *service\n    image: redis:7.2-alpine@sha256:1a2b3c4d5e6f\n    command: [\"redis-server\", \"--appendonly\", \"yes\", \"--maxmemory\", \"256mb\"]\n    volumes: [cache-data:\u002Fdata]\n    healthcheck: { test: [\"CMD\", \"redis-cli\", \"ping\"], interval: 10s, retries: 3 }\nnetworks:\n  shop-net: {}\n  db-only: { internal: true }\nvolumes: { db-data: {}, cache-data: {} }\nconfigs: { app-conf: { file: .\u002Fconfig\u002Fapp.prod.yaml } }\nsecrets:\n  db_password: { file: .\u002Fsecrets\u002Fdb_password.txt }\n  database_url: { file: .\u002Fsecrets\u002Fdatabase_url.txt }\n",[19,10259,10260,10269,10281,10289,10299,10309,10319,10331,10340,10352,10386,10403,10413,10419,10425,10435,10444,10459,10474,10492,10498,10508,10517,10528,10550,10578,10590,10596,10618,10626,10637,10646,10655,10694,10725,10732,10743,10753,10770,10781,10793,10809,10844,10851,10862,10872,10901,10912,10954,10961,10969,10984,10999,11019,11026,11041],{"__ignoreMap":1163},[1167,10261,10262,10264,10266],{"class":1169,"line":1170},[1167,10263,6406],{"class":7930},[1167,10265,7620],{"class":1216},[1167,10267,10268],{"class":1183},"shop\n",[1167,10270,10271,10274,10276,10278],{"class":1169,"line":1177},[1167,10272,10273],{"class":7930},"x-service",[1167,10275,7620],{"class":1216},[1167,10277,9636],{"class":3505},[1167,10279,10280],{"class":1173},"service\n",[1167,10282,10283,10285,10287],{"class":1169,"line":1202},[1167,10284,9644],{"class":7930},[1167,10286,7620],{"class":1216},[1167,10288,8576],{"class":1183},[1167,10290,10291,10294,10296],{"class":1169,"line":1208},[1167,10292,10293],{"class":7930},"  init",[1167,10295,7620],{"class":1216},[1167,10297,10298],{"class":2703},"true\n",[1167,10300,10301,10304,10306],{"class":1169,"line":1238},[1167,10302,10303],{"class":7930},"  stop_grace_period",[1167,10305,7620],{"class":1216},[1167,10307,10308],{"class":1183},"30s\n",[1167,10310,10311,10313,10315,10317],{"class":1169,"line":1257},[1167,10312,9692],{"class":7930},[1167,10314,7461],{"class":1216},[1167,10316,9697],{"class":1183},[1167,10318,8427],{"class":1216},[1167,10320,10321,10324,10326,10329],{"class":1169,"line":1274},[1167,10322,10323],{"class":7930},"  cap_drop",[1167,10325,7461],{"class":1216},[1167,10327,10328],{"class":1183},"\"ALL\"",[1167,10330,8427],{"class":1216},[1167,10332,10333,10336,10338],{"class":1169,"line":1290},[1167,10334,10335],{"class":7930},"  read_only",[1167,10337,7620],{"class":1216},[1167,10339,10298],{"class":2703},[1167,10341,10342,10345,10347,10350],{"class":1169,"line":1304},[1167,10343,10344],{"class":7930},"  tmpfs",[1167,10346,7461],{"class":1216},[1167,10348,10349],{"class":1183},"\"\u002Ftmp\"",[1167,10351,8427],{"class":1216},[1167,10353,10354,10356,10358,10360,10362,10364,10366,10368,10370,10372,10374,10376,10378,10380,10382,10384],{"class":1169,"line":1310},[1167,10355,9653],{"class":7930},[1167,10357,7634],{"class":1216},[1167,10359,7974],{"class":7930},[1167,10361,7620],{"class":1216},[1167,10363,9662],{"class":1183},[1167,10365,1223],{"class":1216},[1167,10367,9667],{"class":7930},[1167,10369,7634],{"class":1216},[1167,10371,9672],{"class":7930},[1167,10373,7620],{"class":1216},[1167,10375,7642],{"class":1183},[1167,10377,1223],{"class":1216},[1167,10379,9681],{"class":7930},[1167,10381,7620],{"class":1216},[1167,10383,7652],{"class":1183},[1167,10385,8404],{"class":1216},[1167,10387,10388,10391,10393,10396,10398,10401],{"class":1169,"line":3259},[1167,10389,10390],{"class":7930},"  ulimits",[1167,10392,7634],{"class":1216},[1167,10394,10395],{"class":7930},"nofile",[1167,10397,7620],{"class":1216},[1167,10399,10400],{"class":2703},"65536",[1167,10402,7655],{"class":1216},[1167,10404,10405,10407,10409,10411],{"class":1169,"line":3264},[1167,10406,9704],{"class":7930},[1167,10408,7461],{"class":1216},[1167,10410,9709],{"class":1183},[1167,10412,8427],{"class":1216},[1167,10414,10415,10417],{"class":1169,"line":3269},[1167,10416,7931],{"class":7930},[1167,10418,7999],{"class":1216},[1167,10420,10421,10423],{"class":1169,"line":3275},[1167,10422,7942],{"class":7930},[1167,10424,7999],{"class":1216},[1167,10426,10427,10429,10431,10433],{"class":1169,"line":3280},[1167,10428,9734],{"class":2703},[1167,10430,7620],{"class":1216},[1167,10432,6623],{"class":3505},[1167,10434,10280],{"class":1216},[1167,10436,10437,10439,10441],{"class":1169,"line":3285},[1167,10438,8409],{"class":7930},[1167,10440,7620],{"class":1216},[1167,10442,10443],{"class":1183},"nginx:1.27-alpine@sha256:2bc60bb1c9d0\n",[1167,10445,10446,10448,10450,10452,10454,10457],{"class":1169,"line":8594},[1167,10447,8419],{"class":7930},[1167,10449,7461],{"class":1216},[1167,10451,8601],{"class":1183},[1167,10453,1223],{"class":1216},[1167,10455,10456],{"class":1183},"\"127.0.0.1:443:443\"",[1167,10458,8427],{"class":1216},[1167,10460,10461,10463,10465,10467,10469,10472],{"class":1169,"line":8606},[1167,10462,8459],{"class":7930},[1167,10464,7461],{"class":1216},[1167,10466,8613],{"class":1183},[1167,10468,1223],{"class":1216},[1167,10470,10471],{"class":1183},"\".\u002Fnginx\u002Fcerts:\u002Fetc\u002Fnginx\u002Fcerts:ro\"",[1167,10473,8427],{"class":1216},[1167,10475,10476,10478,10480,10482,10484,10486,10488,10490],{"class":1169,"line":8618},[1167,10477,8471],{"class":7930},[1167,10479,7634],{"class":1216},[1167,10481,8625],{"class":7930},[1167,10483,7634],{"class":1216},[1167,10485,8481],{"class":7930},[1167,10487,7620],{"class":1216},[1167,10489,8486],{"class":1183},[1167,10491,8404],{"class":1216},[1167,10493,10494,10496],{"class":1169,"line":8630},[1167,10495,8364],{"class":7930},[1167,10497,7999],{"class":1216},[1167,10499,10500,10502,10504,10506],{"class":1169,"line":8638},[1167,10501,9734],{"class":2703},[1167,10503,7620],{"class":1216},[1167,10505,6623],{"class":3505},[1167,10507,10280],{"class":1216},[1167,10509,10510,10512,10514],{"class":1169,"line":8648},[1167,10511,8409],{"class":7930},[1167,10513,7620],{"class":1216},[1167,10515,10516],{"class":1183},"ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0ea7b2d4\n",[1167,10518,10519,10522,10524,10526],{"class":1169,"line":8674},[1167,10520,10521],{"class":7930},"    expose",[1167,10523,7461],{"class":1216},[1167,10525,9771],{"class":1183},[1167,10527,8427],{"class":1216},[1167,10529,10530,10532,10534,10536,10538,10540,10542,10544,10546,10548],{"class":1169,"line":8686},[1167,10531,8432],{"class":7930},[1167,10533,7634],{"class":1216},[1167,10535,2249],{"class":7930},[1167,10537,7620],{"class":1216},[1167,10539,8401],{"class":1183},[1167,10541,1223],{"class":1216},[1167,10543,2255],{"class":7930},[1167,10545,7620],{"class":1216},[1167,10547,9753],{"class":1183},[1167,10549,7655],{"class":1216},[1167,10551,10552,10555,10558,10561,10563,10566,10568,10570,10572,10575],{"class":1169,"line":8729},[1167,10553,10554],{"class":7930},"    configs",[1167,10556,10557],{"class":1216},": [{ ",[1167,10559,10560],{"class":7930},"source",[1167,10562,7620],{"class":1216},[1167,10564,10565],{"class":1183},"app-conf",[1167,10567,1223],{"class":1216},[1167,10569,8128],{"class":7930},[1167,10571,7620],{"class":1216},[1167,10573,10574],{"class":1183},"\u002Fetc\u002Fshop\u002Fconfig.yaml",[1167,10576,10577],{"class":1216}," }]\n",[1167,10579,10580,10583,10585,10588],{"class":1169,"line":8737},[1167,10581,10582],{"class":7930},"    secrets",[1167,10584,7461],{"class":1216},[1167,10586,10587],{"class":1183},"database_url",[1167,10589,8427],{"class":1216},[1167,10591,10592,10594],{"class":1169,"line":8747},[1167,10593,8508],{"class":7930},[1167,10595,7999],{"class":1216},[1167,10597,10598,10600,10602,10604,10606,10608,10610,10612,10614,10616],{"class":1169,"line":8770},[1167,10599,8515],{"class":7930},[1167,10601,7461],{"class":1216},[1167,10603,8520],{"class":1183},[1167,10605,1223],{"class":1216},[1167,10607,1220],{"class":1183},[1167,10609,1223],{"class":1216},[1167,10611,8529],{"class":1183},[1167,10613,1223],{"class":1216},[1167,10615,8534],{"class":1183},[1167,10617,8427],{"class":1216},[1167,10619,10620,10622,10624],{"class":1169,"line":8782},[1167,10621,8541],{"class":7930},[1167,10623,7620],{"class":1216},[1167,10625,8546],{"class":1183},[1167,10627,10629,10632,10634],{"class":1169,"line":10628},30,[1167,10630,10631],{"class":7930},"      timeout",[1167,10633,7620],{"class":1216},[1167,10635,10636],{"class":1183},"3s\n",[1167,10638,10640,10642,10644],{"class":1169,"line":10639},31,[1167,10641,8551],{"class":7930},[1167,10643,7620],{"class":1216},[1167,10645,8556],{"class":2703},[1167,10647,10649,10651,10653],{"class":1169,"line":10648},32,[1167,10650,8561],{"class":7930},[1167,10652,7620],{"class":1216},[1167,10654,8566],{"class":1183},[1167,10656,10658,10661,10663,10666,10668,10671,10673,10676,10678,10681,10683,10686,10688,10691],{"class":1169,"line":10657},33,[1167,10659,10660],{"class":7930},"    deploy",[1167,10662,7634],{"class":1216},[1167,10664,10665],{"class":7930},"resources",[1167,10667,7634],{"class":1216},[1167,10669,10670],{"class":7930},"limits",[1167,10672,7634],{"class":1216},[1167,10674,10675],{"class":7930},"memory",[1167,10677,7620],{"class":1216},[1167,10679,10680],{"class":1183},"512M",[1167,10682,1223],{"class":1216},[1167,10684,10685],{"class":7930},"cpus",[1167,10687,7620],{"class":1216},[1167,10689,10690],{"class":1183},"\"1.5\"",[1167,10692,10693],{"class":1216}," } } }\n",[1167,10695,10697,10699,10701,10703,10705,10707,10709,10711,10713,10715,10717,10719,10721,10723],{"class":1169,"line":10696},34,[1167,10698,8471],{"class":7930},[1167,10700,7634],{"class":1216},[1167,10702,8476],{"class":7930},[1167,10704,7634],{"class":1216},[1167,10706,8481],{"class":7930},[1167,10708,7620],{"class":1216},[1167,10710,8486],{"class":1183},[1167,10712,8489],{"class":1216},[1167,10714,8492],{"class":7930},[1167,10716,7634],{"class":1216},[1167,10718,8481],{"class":7930},[1167,10720,7620],{"class":1216},[1167,10722,8501],{"class":1183},[1167,10724,8404],{"class":1216},[1167,10726,10728,10730],{"class":1169,"line":10727},35,[1167,10729,8633],{"class":7930},[1167,10731,7999],{"class":1216},[1167,10733,10735,10737,10739,10741],{"class":1169,"line":10734},36,[1167,10736,9734],{"class":2703},[1167,10738,7620],{"class":1216},[1167,10740,6623],{"class":3505},[1167,10742,10280],{"class":1216},[1167,10744,10746,10748,10750],{"class":1169,"line":10745},37,[1167,10747,8409],{"class":7930},[1167,10749,7620],{"class":1216},[1167,10751,10752],{"class":1183},"postgres:16.4@sha256:6d7df8c5b0f8\n",[1167,10754,10756,10758,10760,10763,10765,10768],{"class":1169,"line":10755},38,[1167,10757,8432],{"class":7930},[1167,10759,7634],{"class":1216},[1167,10761,10762],{"class":7930},"POSTGRES_PASSWORD_FILE",[1167,10764,7620],{"class":1216},[1167,10766,10767],{"class":1183},"\u002Frun\u002Fsecrets\u002Fdb_password",[1167,10769,7655],{"class":1216},[1167,10771,10773,10775,10777,10779],{"class":1169,"line":10772},39,[1167,10774,8459],{"class":7930},[1167,10776,7461],{"class":1216},[1167,10778,8681],{"class":1183},[1167,10780,8427],{"class":1216},[1167,10782,10784,10786,10788,10791],{"class":1169,"line":10783},40,[1167,10785,10582],{"class":7930},[1167,10787,7461],{"class":1216},[1167,10789,10790],{"class":1183},"db_password",[1167,10792,8427],{"class":1216},[1167,10794,10796,10799,10801,10803,10805,10807],{"class":1169,"line":10795},41,[1167,10797,10798],{"class":7930},"    networks",[1167,10800,7461],{"class":1216},[1167,10802,9709],{"class":1183},[1167,10804,1223],{"class":1216},[1167,10806,10047],{"class":1183},[1167,10808,8427],{"class":1216},[1167,10810,10812,10814,10816,10818,10820,10822,10824,10826,10828,10830,10832,10834,10836,10838,10840,10842],{"class":1169,"line":10811},42,[1167,10813,8508],{"class":7930},[1167,10815,7634],{"class":1216},[1167,10817,8693],{"class":7930},[1167,10819,7461],{"class":1216},[1167,10821,8698],{"class":1183},[1167,10823,1223],{"class":1216},[1167,10825,8703],{"class":1183},[1167,10827,8706],{"class":1216},[1167,10829,8709],{"class":7930},[1167,10831,7620],{"class":1216},[1167,10833,8714],{"class":1183},[1167,10835,1223],{"class":1216},[1167,10837,8719],{"class":7930},[1167,10839,7620],{"class":1216},[1167,10841,8724],{"class":2703},[1167,10843,7655],{"class":1216},[1167,10845,10847,10849],{"class":1169,"line":10846},43,[1167,10848,8732],{"class":7930},[1167,10850,7999],{"class":1216},[1167,10852,10854,10856,10858,10860],{"class":1169,"line":10853},44,[1167,10855,9734],{"class":2703},[1167,10857,7620],{"class":1216},[1167,10859,6623],{"class":3505},[1167,10861,10280],{"class":1216},[1167,10863,10865,10867,10869],{"class":1169,"line":10864},45,[1167,10866,8409],{"class":7930},[1167,10868,7620],{"class":1216},[1167,10870,10871],{"class":1183},"redis:7.2-alpine@sha256:1a2b3c4d5e6f\n",[1167,10873,10875,10877,10879,10881,10883,10885,10887,10889,10891,10894,10896,10899],{"class":1169,"line":10874},46,[1167,10876,8750],{"class":7930},[1167,10878,7461],{"class":1216},[1167,10880,8755],{"class":1183},[1167,10882,1223],{"class":1216},[1167,10884,8760],{"class":1183},[1167,10886,1223],{"class":1216},[1167,10888,8765],{"class":1183},[1167,10890,1223],{"class":1216},[1167,10892,10893],{"class":1183},"\"--maxmemory\"",[1167,10895,1223],{"class":1216},[1167,10897,10898],{"class":1183},"\"256mb\"",[1167,10900,8427],{"class":1216},[1167,10902,10904,10906,10908,10910],{"class":1169,"line":10903},47,[1167,10905,8459],{"class":7930},[1167,10907,7461],{"class":1216},[1167,10909,8777],{"class":1183},[1167,10911,8427],{"class":1216},[1167,10913,10915,10917,10919,10921,10923,10925,10927,10930,10932,10935,10937,10939,10941,10943,10945,10947,10949,10952],{"class":1169,"line":10914},48,[1167,10916,8508],{"class":7930},[1167,10918,7634],{"class":1216},[1167,10920,8693],{"class":7930},[1167,10922,7461],{"class":1216},[1167,10924,8520],{"class":1183},[1167,10926,1223],{"class":1216},[1167,10928,10929],{"class":1183},"\"redis-cli\"",[1167,10931,1223],{"class":1216},[1167,10933,10934],{"class":1183},"\"ping\"",[1167,10936,8706],{"class":1216},[1167,10938,8709],{"class":7930},[1167,10940,7620],{"class":1216},[1167,10942,8714],{"class":1183},[1167,10944,1223],{"class":1216},[1167,10946,8719],{"class":7930},[1167,10948,7620],{"class":1216},[1167,10950,10951],{"class":2703},"3",[1167,10953,7655],{"class":1216},[1167,10955,10957,10959],{"class":1169,"line":10956},49,[1167,10958,7959],{"class":7930},[1167,10960,7999],{"class":1216},[1167,10962,10964,10966],{"class":1169,"line":10963},50,[1167,10965,7969],{"class":7930},[1167,10967,10968],{"class":1216},": {}\n",[1167,10970,10972,10974,10976,10978,10980,10982],{"class":1169,"line":10971},51,[1167,10973,10010],{"class":7930},[1167,10975,7634],{"class":1216},[1167,10977,10039],{"class":7930},[1167,10979,7620],{"class":1216},[1167,10981,8187],{"class":2703},[1167,10983,7655],{"class":1216},[1167,10985,10987,10989,10991,10993,10995,10997],{"class":1169,"line":10986},52,[1167,10988,7985],{"class":7930},[1167,10990,7634],{"class":1216},[1167,10992,8789],{"class":7930},[1167,10994,8792],{"class":1216},[1167,10996,8795],{"class":7930},[1167,10998,8803],{"class":1216},[1167,11000,11002,11004,11006,11008,11010,11012,11014,11017],{"class":1169,"line":11001},53,[1167,11003,8004],{"class":7930},[1167,11005,7634],{"class":1216},[1167,11007,10565],{"class":7930},[1167,11009,7634],{"class":1216},[1167,11011,8019],{"class":7930},[1167,11013,7620],{"class":1216},[1167,11015,11016],{"class":1183},".\u002Fconfig\u002Fapp.prod.yaml",[1167,11018,8404],{"class":1216},[1167,11020,11022,11024],{"class":1169,"line":11021},54,[1167,11023,8031],{"class":7930},[1167,11025,7999],{"class":1216},[1167,11027,11029,11031,11033,11035,11037,11039],{"class":1169,"line":11028},55,[1167,11030,8041],{"class":7930},[1167,11032,7634],{"class":1216},[1167,11034,8019],{"class":7930},[1167,11036,7620],{"class":1216},[1167,11038,8050],{"class":1183},[1167,11040,7655],{"class":1216},[1167,11042,11044,11047,11049,11051,11053,11056],{"class":1169,"line":11043},56,[1167,11045,11046],{"class":7930},"  database_url",[1167,11048,7634],{"class":1216},[1167,11050,8019],{"class":7930},[1167,11052,7620],{"class":1216},[1167,11054,11055],{"class":1183},".\u002Fsecrets\u002Fdatabase_url.txt",[1167,11057,7655],{"class":1216},[15,11059,11060,11061,11063,11064,11066,11067,11069,11070,11073,11074,11077,11078,11080],{},"这份配置里每一条都能在前面章节找到理由：digest 锁版本（第十章）、",[19,11062,2478],{}," 而非 ",[19,11065,8140],{}," 让 api 不对外（第八章）、",[19,11068,10039],{}," 网络隔离数据库、",[19,11071,11072],{},"read_only"," + tmpfs（第十五章）、",[19,11075,11076],{},"init","（本章）、healthcheck + ",[19,11079,8486],{},"（第十一章）、日志轮转（第十六章）、密钥走 secrets 文件（下一章主题）。",[15,11082,11083],{},"下一章就专门讲这条线：配置与密钥怎么进容器。",[10,11085,11087],{"id":11086},"十三配置与密钥优先级secrets-与别把配置打进镜像","十三、配置与密钥：优先级、secrets 与「别把配置打进镜像」",[40,11089,11090],{"id":11090},"环境变量注入的四条路径与优先级",[15,11092,11093],{},"同一个变量在四个地方都能写，最终生效的是优先级最高的那个：",[1158,11095,11097],{"className":1160,"code":11096,"language":1162,"meta":1163,"style":1163},"printf 'LOG_LEVEL=debug\\nPORT=3000\\n' > .env.runtime\ndocker run --rm -e LOG_LEVEL=warn --env-file .env.runtime myimg:1.0 printenv LOG_LEVEL\n",[19,11098,11099,11112],{"__ignoreMap":1163},[1167,11100,11101,11103,11106,11109],{"class":1169,"line":1170},[1167,11102,3490],{"class":2703},[1167,11104,11105],{"class":1183}," 'LOG_LEVEL=debug\\nPORT=3000\\n'",[1167,11107,11108],{"class":3505}," >",[1167,11110,11111],{"class":1183}," .env.runtime\n",[1167,11113,11114,11116,11118,11120,11123,11126,11129,11132,11135,11138],{"class":1169,"line":1177},[1167,11115,559],{"class":1173},[1167,11117,3380],{"class":1183},[1167,11119,3386],{"class":2703},[1167,11121,11122],{"class":2703}," -e",[1167,11124,11125],{"class":1183}," LOG_LEVEL=warn",[1167,11127,11128],{"class":2703}," --env-file",[1167,11130,11131],{"class":1183}," .env.runtime",[1167,11133,11134],{"class":1183}," myimg:1.0",[1167,11136,11137],{"class":1183}," printenv",[1167,11139,11140],{"class":1183}," LOG_LEVEL\n",[45,11142,11143,11156],{},[48,11144,11145],{},[51,11146,11147,11150,11153],{},[54,11148,11149],{},"优先级（低 → 高）",[54,11151,11152],{},"来源",[54,11154,11155],{},"特点",[67,11157,11158,11170,11188,11201],{},[51,11159,11160,11162,11167],{},[72,11161,4267],{},[72,11163,11164,11165],{},"Dockerfile ",[19,11166,1380],{},[72,11168,11169],{},"进镜像，所有环境共享，只能被覆盖不能删",[51,11171,11172,11175,11183],{},[72,11173,11174],{},"2",[72,11176,11177,11180,11181],{},[19,11178,11179],{},"--env-file"," \u002F Compose ",[19,11182,8194],{},[72,11184,11185,11186],{},"文件里的值；Compose 中多个文件",[31,11187,9488],{},[51,11189,11190,11192,11198],{},[72,11191,10951],{},[72,11193,11194,11180,11196],{},[19,11195,3938],{},[19,11197,8171],{},[72,11199,11200],{},"命令行级，覆盖 env_file",[51,11202,11203,11206,11214],{},[72,11204,11205],{},"4",[72,11207,11208,11209,7536,11211,11213],{},"Compose ",[19,11210,8171],{},[19,11212,9322],{}," 插值 + 宿主真实环境",[72,11215,11216,11218],{},[19,11217,3167],{}," 只做文本展开，不影响上面的覆盖次序",[15,11220,11221,11222,11234,11235,11237,11238,11240,11241,7536,11243,11246],{},"一句话记忆：",[31,11223,11224,11226,11227,939,11229,11231,11232],{},[19,11225,8171],{}," 压过 ",[19,11228,8194],{},[19,11230,8194],{}," 压过镜像 ",[19,11233,1380],{},"。而 Compose 的 ",[19,11236,3167],{},"（插值用）和 ",[19,11239,8194],{},"（注入容器用）根本不在同一层——",[19,11242,3167],{},[19,11244,11245],{},"APP_VERSION"," 只会出现在 yaml 里，不会自动进容器。这是新手最常见的「我在 .env 写了怎么容器里读不到」。",[15,11248,11249,11250,11253,11254,616],{},"验证永远是同一条命令：",[19,11251,11252],{},"docker exec shop-api printenv","，或 ",[19,11255,11256],{},"docker inspect --format '{{json .Config.Env}}'",[40,11258,11260],{"id":11259},"_12-factor-在容器里的落地","12-factor 在容器里的落地",[45,11262,11263,11276],{},[48,11264,11265],{},[51,11266,11267,11270,11273],{},[54,11268,11269],{},"原则",[54,11271,11272],{},"容器里的做法",[54,11274,11275],{},"反例",[67,11277,11278,11292,11309,11320,11331,11342],{},[51,11279,11280,11283,11286],{},[72,11281,11282],{},"配置存环境",[72,11284,11285],{},"环境变量 \u002F secrets 文件",[72,11287,11288,11291],{},[19,11289,11290],{},"config.prod.yaml"," 打进镜像",[51,11293,11294,11297,11300],{},[72,11295,11296],{},"日志到标准输出",[72,11298,11299],{},"不写日志文件，交给驱动与采集",[72,11301,11302,11303,1372,11306,11308],{},"容器内 ",[19,11304,11305],{},"\u002Fvar\u002Flog\u002Fapp\u002F*.log",[19,11307,4538],{}," 全空）",[51,11310,11311,11314,11317],{},[72,11312,11313],{},"一个进程一个容器",[72,11315,11316],{},"web\u002Fapi\u002Fworker 分开",[72,11318,11319],{},"一个容器跑 nginx + php-fpm + cron",[51,11321,11322,11325,11328],{},[72,11323,11324],{},"快速启动与优雅停机",[72,11326,11327],{},"就绪探针 + SIGTERM 处理",[72,11329,11330],{},"启动要跑 30 秒初始化脚本",[51,11332,11333,11336,11339],{},[72,11334,11335],{},"无状态",[72,11337,11338],{},"状态放卷\u002F对象存储\u002FDB",[72,11340,11341],{},"session 存进程内存（扩容即掉线）",[51,11343,11344,11347,11350],{},[72,11345,11346],{},"构建一次多环境跑",[72,11348,11349],{},"镜像不变，只换环境变量",[72,11351,11352],{},"生产用「生产版 Dockerfile」重新构建",[15,11354,11355,11356,11359,11360,11362],{},"核心是最后一条：",[31,11357,11358],{},"镜像不可变，环境差异全靠注入","。这也回答了「为什么不要把配置 COPY 进镜像」——一旦 COPY，每个环境都要构建一个镜像，缓存、回滚、审计全部翻倍；而且配置文件里通常藏着连接串和密钥，镜像一旦被 pull 就等于泄密（第十五章演示 ",[19,11361,1593],{}," 捞密钥）。",[40,11364,11366],{"id":11365},"secretsswarm-原生-vs-compose-的-file-driver","secrets：Swarm 原生 vs Compose 的 file driver",[1158,11368,11370],{"className":7921,"code":11369,"language":7923,"meta":1163,"style":1163},"secrets:\n  db_password:\n    file: .\u002Fsecrets\u002Fdb_password.txt     # file driver：本质是把这个文件挂到 \u002Frun\u002Fsecrets\u002Fdb_password\n  jwt_key:\n    source: jwt_key                      # 外部已存在的 secret（Swarm 或外部管理）\n",[19,11371,11372,11378,11384,11396,11403],{"__ignoreMap":1163},[1167,11373,11374,11376],{"class":1169,"line":1170},[1167,11375,8031],{"class":7930},[1167,11377,7999],{"class":1216},[1167,11379,11380,11382],{"class":1169,"line":1177},[1167,11381,8041],{"class":7930},[1167,11383,7999],{"class":1216},[1167,11385,11386,11389,11391,11393],{"class":1169,"line":1202},[1167,11387,11388],{"class":7930},"    file",[1167,11390,7620],{"class":1216},[1167,11392,8050],{"class":1183},[1167,11394,11395],{"class":3604},"     # file driver：本质是把这个文件挂到 \u002Frun\u002Fsecrets\u002Fdb_password\n",[1167,11397,11398,11401],{"class":1169,"line":1208},[1167,11399,11400],{"class":7930},"  jwt_key",[1167,11402,7999],{"class":1216},[1167,11404,11405,11408,11410,11413],{"class":1169,"line":1238},[1167,11406,11407],{"class":7930},"    source",[1167,11409,7620],{"class":1216},[1167,11411,11412],{"class":1183},"jwt_key",[1167,11414,11415],{"class":3604},"                      # 外部已存在的 secret（Swarm 或外部管理）\n",[15,11417,11418,11419,11422,11423,11426,11427,11430,11431,1372,11434,11437,11438,11445,11446,11448,11449,114,11451,114,11454,11457],{},"必须知道的区别：",[31,11420,11421],{},"非 Swarm 环境下 Compose 的 secret 就是 bind mount 一个文件","，落盘、可读、宿主上明文存在；只有 Swarm 才把它放进内存（tmpfs）并加密分发。",[19,11424,11425],{},"docker secret create"," 在非 Swarm 节点会报 ",[19,11428,11429],{},"This node is not a swarm manager","。所以「用了 secrets 就安全」是不成立的——真正的收益是",[31,11432,11433],{},"约定路径",[19,11435,11436],{},"\u002Frun\u002Fsecrets\u002F\u003Cname>","）与",[31,11439,11440,11441,11444],{},"应用用 ",[19,11442,11443],{},"*_FILE"," 变量读","（如 ",[19,11447,10762],{},"），让密钥不进环境变量列表（",[19,11450,4756],{},[19,11452,11453],{},"printenv",[19,11455,11456],{},"\u002Fproc\u002F1\u002Fenviron"," 都能读到环境变量，而 secret 文件需要能访问挂载点才行）。",[15,11459,11460,11461,616],{},"临时与内存型的配置用 tmpfs（第九章）：",[19,11462,11463],{},"--mount type=tmpfs,target=\u002Frun\u002Fsecrets,tmpfs-size=1m",[40,11465,11467],{"id":11466},"挂载配置目录-只读以及-gitignore","挂载配置目录 + 只读，以及 .gitignore",[1158,11469,11471],{"className":1160,"code":11470,"language":1162,"meta":1163,"style":1163},"docker run -d --name shop-web -v \u002Fsrv\u002Fshop\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro nginx:1.27-alpine\n",[19,11472,11473],{"__ignoreMap":1163},[1167,11474,11475,11477,11479,11481,11483,11485,11487,11490],{"class":1169,"line":1170},[1167,11476,559],{"class":1173},[1167,11478,3380],{"class":1183},[1167,11480,5611],{"class":2703},[1167,11482,3349],{"class":2703},[1167,11484,3598],{"class":1183},[1167,11486,6285],{"class":2703},[1167,11488,11489],{"class":1183}," \u002Fsrv\u002Fshop\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro",[1167,11491,6331],{"class":1183},[15,11493,11494,11496,11497,11499,11500,11503,11504,11507,11508,11511,11512,2881,11514,11516],{},[19,11495,6448],{}," 不是可选的礼貌：配置文件被容器改坏（某些镜像会在启动脚本里 ",[19,11498,9829],{}," 写回 ",[19,11501,11502],{},"\u002Fetc\u002F...","）时，只读挂载能让失败在第一次写就暴露，而不是悄悄污染宿主。宿主机上把配置放在 ",[19,11505,11506],{},"\u002Fsrv\u002F\u003Cproject>\u002Fconfig","，用 ",[19,11509,11510],{},"chmod 0640 root:shop"," 收紧，容器以 ",[19,11513,6921],{},[19,11515,6896],{}," 匹配读组。",[15,11518,11519,11521],{},[19,11520,3100],{}," 里和容器相关的必备项：",[1158,11523,11526],{"className":11524,"code":11525,"language":3148},[3146],".env\n.env.*\n!.env.example\nsecrets\u002F\n*.pem\n*.key\ndocker-compose.override.yaml\ncompose.override.yaml\n\u002Fdata\u002F\n",[19,11527,11525],{"__ignoreMap":1163},[15,11529,11530,11533],{},[19,11531,11532],{},".env.example","（只有键名和假值）应该提交，它是给同事的「配置契约」。",[40,11535,11537],{"id":11536},"模板化配置一行-envsubst","模板化配置：一行 envsubst",[15,11539,11540],{},"配置里需要变量替换时（nginx upstream、数据库连接串），把模板留在镜像里、启动时渲染，是 12-factor 与「不重新构建」的折中：",[1158,11542,11544],{"className":1160,"code":11543,"language":1162,"meta":1163,"style":1163},"envsubst '${DB_HOST} ${DB_PORT}' \u003C \u002Fetc\u002Fshop\u002Fapp.yaml.tpl > \u002Fetc\u002Fshop\u002Fapp.yaml\n",[19,11545,11546],{"__ignoreMap":1163},[1167,11547,11548,11550,11553,11556,11559,11561],{"class":1169,"line":1170},[1167,11549,9829],{"class":1173},[1167,11551,11552],{"class":1183}," '${DB_HOST} ${DB_PORT}'",[1167,11554,11555],{"class":3505}," \u003C",[1167,11557,11558],{"class":1183}," \u002Fetc\u002Fshop\u002Fapp.yaml.tpl",[1167,11560,11108],{"class":3505},[1167,11562,11563],{"class":1183}," \u002Fetc\u002Fshop\u002Fapp.yaml\n",[15,11565,11566,11567,11569,11570,11573,11574,11576,11577,11580,11581,11584,11585,11588,11589,11592,11593,11595],{},"官方 ",[19,11568,7952],{}," 已经内置这个机制（",[19,11571,11572],{},"\u002Fdocker-entrypoint.envsubst.sh"," 目录约定）。两个坑：",[19,11575,9829],{}," 不带参数会替换",[31,11578,11579],{},"所有"," ",[19,11582,11583],{},"$VAR","（把应用里的 ",[19,11586,11587],{},"$2","、shell 变量一起吃掉），所以一定限定变量名单；渲染目标是 bind mount 来的路径时会因 ",[19,11590,11591],{},":ro"," 失败——那种情况请挂模板目录、输出到容器内可写路径或 tmpfs（第九章的 ",[19,11594,4570],{}," 可以帮你找出所有需要可写的路径）。",[10,11597,11599],{"id":11598},"十四资源限制与性能内存cpu-与-137-的定位路径","十四、资源限制与性能：内存、CPU 与 137 的定位路径",[40,11601,11603,11604,3177,11606,11609],{"id":11602},"内存-memory-与-memory-swap-的语义陷阱","内存：",[19,11605,394],{},[19,11607,11608],{},"--memory-swap"," 的语义陷阱",[45,11611,11612,11620],{},[48,11613,11614],{},[51,11615,11616,11618],{},[54,11617,6032],{},[54,11619,4183],{},[67,11621,11622,11637,11650,11660,11670],{},[51,11623,11624,11630],{},[72,11625,11626,11629],{},[19,11627,11628],{},"-m 512m","（不写 swap）",[72,11631,11632,11633,11636],{},"内存 512m + ",[31,11634,11635],{},"等量 swap","（实际默认 swap 上限 = 2× 内存，即总 1024m）",[51,11638,11639,11644],{},[72,11640,11641],{},[19,11642,11643],{},"-m 512m --memory-swap 512m",[72,11645,11646,11649],{},[31,11647,11648],{},"禁用 swap","（总可用就是 512m）",[51,11651,11652,11657],{},[72,11653,11654],{},[19,11655,11656],{},"-m 512m --memory-swap 768m",[72,11658,11659],{},"512m 内存 + 256m swap",[51,11661,11662,11667],{},[72,11663,11664],{},[19,11665,11666],{},"-m 512m --memory-swap -1",[72,11668,11669],{},"swap 不限（危险，磁盘 IO 可能拖垮宿主）",[51,11671,11672,11677],{},[72,11673,11674],{},[19,11675,11676],{},"--memory-reservation 256m",[72,11678,11679],{},"软下限，内存紧张时才尝试回收",[15,11681,11682,2873,11684,11687,11688,11691,11692,11694,11695,1372,11698,11700,11701,11703],{},[19,11683,11608],{},[31,11685,11686],{},"总量","（内存 + swap），不是 swap 量——这个反直觉定义让「写 0」有特殊意义：",[19,11689,11690],{},"--memory-swap 0"," 无效，想禁 swap 要写成与 ",[19,11693,4072],{}," 相等。生产建议",[31,11696,11697],{},"显式禁 swap",[19,11699,11608],{}," 等于 ",[19,11702,394],{},"），因为被换出的容器进程会出现「没被 OOM 但响应巨慢」的诡异状态，比直接 OOM 更难诊断。",[15,11705,11706,11707,11710,11711,11714,11715,4359,11717,1962],{},"cgroup v1 与 v2 的差异要知道：v2 下没有独立的 ",[19,11708,11709],{},"memory.memsw.limit_in_bytes","，Docker 用 ",[19,11712,11713],{},"memory.swap.max"," 表示「swap 部分」，同样的参数在不同机器上落点不同（",[19,11716,677],{},[19,11718,731],{},[40,11720,11722],{"id":11721},"cpu-与进程数","CPU 与进程数",[1158,11724,11726],{"className":1160,"code":11725,"language":1162,"meta":1163,"style":1163},"docker run -d --cpus 1.5 --cpuset-cpus 2,3 --cpu-shares 512 --pids-limit 256 shop-api\n",[19,11727,11728],{"__ignoreMap":1163},[1167,11729,11730,11732,11734,11736,11739,11742,11745,11748,11751,11754,11757,11760],{"class":1169,"line":1170},[1167,11731,559],{"class":1173},[1167,11733,3380],{"class":1183},[1167,11735,5611],{"class":2703},[1167,11737,11738],{"class":2703}," --cpus",[1167,11740,11741],{"class":2703}," 1.5",[1167,11743,11744],{"class":2703}," --cpuset-cpus",[1167,11746,11747],{"class":1183}," 2,3",[1167,11749,11750],{"class":2703}," --cpu-shares",[1167,11752,11753],{"class":2703}," 512",[1167,11755,11756],{"class":2703}," --pids-limit",[1167,11758,11759],{"class":2703}," 256",[1167,11761,11762],{"class":1183}," shop-api\n",[45,11764,11765,11776],{},[48,11766,11767],{},[51,11768,11769,11771,11773],{},[54,11770,2745],{},[54,11772,5347],{},[54,11774,11775],{},"要点",[67,11777,11778,11801,11818,11831],{},[51,11779,11780,11784,11790],{},[72,11781,11782],{},[19,11783,4093],{},[72,11785,11786,11787,1075],{},"cgroup quota（",[19,11788,11789],{},"cpu.max",[72,11791,11792,11793,11796,11797,11800],{},"硬上限；",[19,11794,11795],{},"1.5"," = 每秒最多用 1.5 核时间。写 ",[19,11798,11799],{},"0.5"," 表示半核",[51,11802,11803,11809,11812],{},[72,11804,11805,11808],{},[19,11806,11807],{},"--cpu-shares","（默认 1024）",[72,11810,11811],{},"权重",[72,11813,11814,11817],{},[31,11815,11816],{},"只在 CPU 争抢时生效","；空闲时容器能用满宿主。别把它当上限",[51,11819,11820,11825,11828],{},[72,11821,11822],{},[19,11823,11824],{},"--cpuset-cpus",[72,11826,11827],{},"绑核",[72,11829,11830],{},"减少调度迁移与 NUMA 跨节点，低延迟场景用",[51,11832,11833,11838,11841],{},[72,11834,11835],{},[19,11836,11837],{},"--pids-limit",[72,11839,11840],{},"进程\u002F线程总数",[72,11842,11843],{},"防 fork 炸弹打爆宿主（默认 unlimited 是个隐患）",[15,11845,11846,3177,11848,11850,11851,11853],{},[19,11847,530],{},[19,11849,11807],{}," 的区别是「天花板」和「分蛋糕比例」——把后者当限制用的人，会发现 ",[19,11852,390],{}," 里 CPU 偶尔 800%，那不是 bug。",[40,11855,11857],{"id":11856},"oom-与-exit-137-的定位路径","OOM 与 exit 137 的定位路径",[15,11859,11860,11861,11863],{},"看到 ",[19,11862,398],{}," 时的四步（顺序不能乱）：",[1158,11865,11867],{"className":1160,"code":11866,"language":1162,"meta":1163,"style":1163},"docker inspect shop-api --format 'oom={{.State.OOMKilled}} exit={{.State.ExitCode}}'\ndocker stats --no-stream shop-api                   # 看 LIMIT 是不是本来就设小了\ndmesg -T | grep -i -E 'oom|killed process' | tail   # 或 journalctl -k\ndocker events --since 2h --filter event=die\n",[19,11868,11869,11882,11895,11923],{"__ignoreMap":1163},[1167,11870,11871,11873,11875,11877,11879],{"class":1169,"line":1170},[1167,11872,559],{"class":1173},[1167,11874,3370],{"class":1183},[1167,11876,4461],{"class":1183},[1167,11878,4777],{"class":2703},[1167,11880,11881],{"class":1183}," 'oom={{.State.OOMKilled}} exit={{.State.ExitCode}}'\n",[1167,11883,11884,11886,11888,11890,11892],{"class":1169,"line":1177},[1167,11885,559],{"class":1173},[1167,11887,4471],{"class":1183},[1167,11889,4474],{"class":2703},[1167,11891,4461],{"class":1183},[1167,11893,11894],{"class":3604},"                   # 看 LIMIT 是不是本来就设小了\n",[1167,11896,11897,11900,11903,11905,11907,11909,11912,11915,11917,11920],{"class":1169,"line":1202},[1167,11898,11899],{"class":1173},"dmesg",[1167,11901,11902],{"class":2703}," -T",[1167,11904,3506],{"class":3505},[1167,11906,5825],{"class":1173},[1167,11908,7799],{"class":2703},[1167,11910,11911],{"class":2703}," -E",[1167,11913,11914],{"class":1183}," 'oom|killed process'",[1167,11916,3506],{"class":3505},[1167,11918,11919],{"class":1173}," tail",[1167,11921,11922],{"class":3604},"   # 或 journalctl -k\n",[1167,11924,11925,11927,11929,11931,11934,11936],{"class":1169,"line":1208},[1167,11926,559],{"class":1173},[1167,11928,4508],{"class":1183},[1167,11930,4455],{"class":2703},[1167,11932,11933],{"class":1183}," 2h",[1167,11935,4515],{"class":2703},[1167,11937,11938],{"class":1183}," event=die\n",[15,11940,11941,11943,11944,11946,11947,11950,11951,11953,11954,11957,11958,11961,11962,11964,11965,11968],{},[19,11942,5259],{}," → 内存配额不够或应用泄漏，调 ",[19,11945,4072],{}," 或修应用；",[19,11948,11949],{},"OOMKilled=false"," 而码是 137 → 有人（或脚本、",[19,11952,4352],{},"、stop 超时）发了 SIGKILL，用 ",[19,11955,11956],{},"docker events"," 找时间点。注意内核 OOM 也可能杀",[31,11959,11960],{},"宿主","进程：没设 ",[19,11963,4072],{}," 的容器可以一路吃到宿主内存，最后 OOM killer 挑了个 sshd——这就是「慎用 ",[19,11966,11967],{},"--oom-kill-disable","」的理由：它让容器在内存不足时不可被杀，于是内核去杀邻居。",[40,11970,11972],{"id":11971},"容器里看到的免费内存核数是宿主的","容器里看到的免费内存\u002F核数是宿主的",[1158,11974,11976],{"className":1160,"code":11975,"language":1162,"meta":1163,"style":1163},"docker run -it --rm --cpus 1 --memory 256m node:22-bookworm-slim sh\n\u002Fsrv\u002Fshop $ free -h          # 显示宿主总内存\n\u002Fsrv\u002Fshop $ nproc            # 显示宿主核数\n\u002Fsrv\u002Fshop $ cat \u002Fsys\u002Ffs\u002Fcgroup\u002Fmemory.max      # 268435456  ← 真实上限在这\n\u002Fsrv\u002Fshop $ cat \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.max          # 100000 25000 ← quota\u002Fperiod = 4 核？其实是 1 核\n",[19,11977,11978,12004,12018,12029,12044],{"__ignoreMap":1163},[1167,11979,11980,11982,11984,11986,11988,11990,11993,11996,11999,12001],{"class":1169,"line":1170},[1167,11981,559],{"class":1173},[1167,11983,3380],{"class":1183},[1167,11985,4632],{"class":2703},[1167,11987,3386],{"class":2703},[1167,11989,11738],{"class":2703},[1167,11991,11992],{"class":2703}," 1",[1167,11994,11995],{"class":2703}," --memory",[1167,11997,11998],{"class":1183}," 256m",[1167,12000,1298],{"class":1183},[1167,12002,12003],{"class":1183}," sh\n",[1167,12005,12006,12008,12010,12013,12015],{"class":1169,"line":1177},[1167,12007,5166],{"class":1173},[1167,12009,5169],{"class":1216},[1167,12011,12012],{"class":1183},"free",[1167,12014,6630],{"class":2703},[1167,12016,12017],{"class":3604},"          # 显示宿主总内存\n",[1167,12019,12020,12022,12024,12026],{"class":1169,"line":1202},[1167,12021,5166],{"class":1173},[1167,12023,5169],{"class":1216},[1167,12025,10173],{"class":1183},[1167,12027,12028],{"class":3604},"            # 显示宿主核数\n",[1167,12030,12031,12033,12035,12038,12041],{"class":1169,"line":1208},[1167,12032,5166],{"class":1173},[1167,12034,5169],{"class":1216},[1167,12036,12037],{"class":1183},"cat",[1167,12039,12040],{"class":1183}," \u002Fsys\u002Ffs\u002Fcgroup\u002Fmemory.max",[1167,12042,12043],{"class":3604},"      # 268435456  ← 真实上限在这\n",[1167,12045,12046,12048,12050,12052,12055],{"class":1169,"line":1238},[1167,12047,5166],{"class":1173},[1167,12049,5169],{"class":1216},[1167,12051,12037],{"class":1183},[1167,12053,12054],{"class":1183}," \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.max",[1167,12056,12057],{"class":3604},"          # 100000 25000 ← quota\u002Fperiod = 4 核？其实是 1 核\n",[15,12059,12060,12061,3177,12064,12066,12067,12070,12071,616],{},"原因是 ",[19,12062,12063],{},"\u002Fproc",[19,12065,12012],{}," 读的是全局视图，namespace 没隔离它们（",[19,12068,12069],{},"lxcfs"," 就是专门伪造这些文件的方案，K8s 环境偶尔引入）。所以：",[31,12072,12073,12074,114,12077,12080],{},"依赖 ",[19,12075,12076],{},"os.totalmem()",[19,12078,12079],{},"Runtime.availableProcessors()"," 的库在容器里都会算错",[1318,12082,12083,12101,12127],{},[584,12084,12085,12088,12089,12092,12093,12096,12097,12100],{},[31,12086,12087],{},"JVM","：新版默认按 cgroup 限制算堆（",[19,12090,12091],{},"-XX:+UseContainerSupport","），显式配 ",[19,12094,12095],{},"-XX:MaxRAMPercentage=70.0"," 比写死 ",[19,12098,12099],{},"-Xmx"," 更可移植；老 8u 之前的版本会按宿主内存开堆，然后被 OOM Kill。",[584,12102,12103,906,12106,12109,12110,12113,12114,114,12117,12120,12121,12123,12124,616],{},[31,12104,12105],{},"Node",[19,12107,12108],{},"--max-old-space-size"," 要",[31,12111,12112],{},"小于","容器内存（留 20–25% 给 Buffer\u002F线程栈），否则 V8 觉得还有空间、内核先动手；",[19,12115,12116],{},"uv.defaultConcurrency",[19,12118,12119],{},"os.cpus().length"," 决定 worker\u002F线程池大小，按 ",[19,12122,530],{}," 自己算而不是读 ",[19,12125,12126],{},"cpus()",[584,12128,12129,12132,12133,12136],{},[31,12130,12131],{},"Go","：GOMAXPROCS 默认读宿主核数，CPU 限额 0.5 核时会出现大量线程被 throttle，用 ",[19,12134,12135],{},"automaxprocs"," 或显式设。",[40,12138,12140],{"id":12139},"清理与镜像瘦身prune-的杀伤范围","清理与镜像瘦身：prune 的杀伤范围",[1158,12142,12144],{"className":1160,"code":12143,"language":1162,"meta":1163,"style":1163},"docker system df                 # 先看账本，永远先跑这个\ndocker container prune           # 只删已停止容器\ndocker image prune               # 只删悬空镜像（无 tag 的）\ndocker image prune -a            # 删所有「没有被任何容器使用」的镜像 —— 会拉回重下\ndocker builder prune             # 只清构建缓存（BuildKit），层缓存没了但镜像还在\ndocker system prune              # 上述前三类的组合\ndocker system prune -a --volumes  # 加上所有未使用卷 —— 数据库没了\n",[19,12145,12146,12159,12170,12181,12195,12207,12218],{"__ignoreMap":1163},[1167,12147,12148,12150,12153,12156],{"class":1169,"line":1170},[1167,12149,559],{"class":1173},[1167,12151,12152],{"class":1183}," system",[1167,12154,12155],{"class":1183}," df",[1167,12157,12158],{"class":3604},"                 # 先看账本，永远先跑这个\n",[1167,12160,12161,12163,12165,12167],{"class":1169,"line":1177},[1167,12162,559],{"class":1173},[1167,12164,3692],{"class":1183},[1167,12166,3695],{"class":1183},[1167,12168,12169],{"class":3604},"           # 只删已停止容器\n",[1167,12171,12172,12174,12176,12178],{"class":1169,"line":1202},[1167,12173,559],{"class":1173},[1167,12175,7223],{"class":1183},[1167,12177,3695],{"class":1183},[1167,12179,12180],{"class":3604},"               # 只删悬空镜像（无 tag 的）\n",[1167,12182,12183,12185,12187,12189,12192],{"class":1169,"line":1208},[1167,12184,559],{"class":1173},[1167,12186,7223],{"class":1183},[1167,12188,3695],{"class":1183},[1167,12190,12191],{"class":2703}," -a",[1167,12193,12194],{"class":3604},"            # 删所有「没有被任何容器使用」的镜像 —— 会拉回重下\n",[1167,12196,12197,12199,12202,12204],{"class":1169,"line":1238},[1167,12198,559],{"class":1173},[1167,12200,12201],{"class":1183}," builder",[1167,12203,3695],{"class":1183},[1167,12205,12206],{"class":3604},"             # 只清构建缓存（BuildKit），层缓存没了但镜像还在\n",[1167,12208,12209,12211,12213,12215],{"class":1169,"line":1257},[1167,12210,559],{"class":1173},[1167,12212,12152],{"class":1183},[1167,12214,3695],{"class":1183},[1167,12216,12217],{"class":3604},"              # 上述前三类的组合\n",[1167,12219,12220,12222,12224,12226,12228,12231],{"class":1169,"line":1274},[1167,12221,559],{"class":1173},[1167,12223,12152],{"class":1183},[1167,12225,3695],{"class":1183},[1167,12227,12191],{"class":2703},[1167,12229,12230],{"class":2703}," --volumes",[1167,12232,12233],{"class":3604},"  # 加上所有未使用卷 —— 数据库没了\n",[15,12235,12236,12237,906,12240,12243,12244,12247],{},"最后一行是 Docker 世界里的 ",[19,12238,12239],{},"rm -rf",[19,12241,12242],{},"--volumes"," 会删掉所有",[31,12245,12246],{},"当前没被容器引用","的命名卷，包括你昨天测试留下的生产库副本（第九章匿名卷那段）。安全替代：",[1158,12249,12251],{"className":1160,"code":12250,"language":1162,"meta":1163,"style":1163},"docker image prune -a --filter \"until=336h\"     # 只清 7 天前的，留回滚余地\ndocker ps -a --filter status=exited --format '{{.Names}}'   # 先列清单再决定\ndocker volume ls --filter name=shop              # 精确定位，然后 volume rm 具体名字\n",[19,12252,12253,12271,12292],{"__ignoreMap":1163},[1167,12254,12255,12257,12259,12261,12263,12265,12268],{"class":1169,"line":1170},[1167,12256,559],{"class":1173},[1167,12258,7223],{"class":1183},[1167,12260,3695],{"class":1183},[1167,12262,12191],{"class":2703},[1167,12264,4515],{"class":2703},[1167,12266,12267],{"class":1183}," \"until=336h\"",[1167,12269,12270],{"class":3604},"     # 只清 7 天前的，留回滚余地\n",[1167,12272,12273,12275,12277,12279,12281,12284,12286,12289],{"class":1169,"line":1177},[1167,12274,559],{"class":1173},[1167,12276,4819],{"class":1183},[1167,12278,12191],{"class":2703},[1167,12280,4515],{"class":2703},[1167,12282,12283],{"class":1183}," status=exited",[1167,12285,4777],{"class":2703},[1167,12287,12288],{"class":1183}," '{{.Names}}'",[1167,12290,12291],{"class":3604},"   # 先列清单再决定\n",[1167,12293,12294,12296,12298,12300,12302,12305],{"class":1169,"line":1202},[1167,12295,559],{"class":1173},[1167,12297,6591],{"class":1183},[1167,12299,4689],{"class":1183},[1167,12301,4515],{"class":2703},[1167,12303,12304],{"class":1183}," name=shop",[1167,12306,12307],{"class":3604},"              # 精确定位，然后 volume rm 具体名字\n",[15,12309,12310,12311,12314,12315,12317,12318,12321],{},"镜像体积影响的不只是磁盘：",[31,12312,12313],{},"容器首次启动要等层下载+解压完成","（第三章），1GB 与 80MB 的镜像在扩缩容、故障重调度时的恢复时间差一个量级。多阶段构建（第五章）+ slim\u002Falpine 基础镜像 + 把 ",[19,12316,1924],{}," 链起来减少层数（第四章的 ",[19,12319,12320],{},"rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*","），三招通常能砍掉 70%。",[15,12323,12324,12325,12327,12328,12330,12331,12333,12334,12336],{},"示例 14 演示一次完整的定位：容器反复 ",[19,12326,398],{},"，从 ",[19,12329,4358],{}," 到 ",[19,12332,4544],{}," 到内核日志，最后落到 JVM 堆参数与 ",[19,12335,4072],{}," 的错配。",[1405,12338],{"height":4593,"src":12339,"title":12340},"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F14-resources.html","示例 14：资源限制与 137——OOM 的定位路径",[10,12342,12344],{"id":12343},"十五安全实践非-root能力裁剪只读根文件系统与扫描","十五、安全实践：非 root、能力裁剪、只读根文件系统与扫描",[40,12346,12348],{"id":12347},"为什么默认是-rootrootless-能不能真解决","为什么默认是 root，rootless 能不能真解决",[15,12350,12351,12352,12355],{},"镜像默认以 root 跑是历史决定：装包、写系统目录、绑 80 端口都方便。后果是应用一旦被 RCE，攻击者拿到的是",[31,12353,12354],{},"容器内的 root","——它可以调用镜像允许的所有 capability、访问所有挂载。",[15,12357,12358,12359,12362,12363,12366],{},"关键认知：",[31,12360,12361],{},"容器内 root ≠ 宿主 root","，但也",[31,12364,12365],{},"绝不是安全的","。能不能逃逸取决于能力集、seccomp、挂载了什么。rootless 模式（第二章）把「逃逸后拿到的身份」从 root 降级成普通用户，这是真实的收益；但它不改变容器内应用仍以自己的 UID 拥有全部权限这一事实，也不解决 secret 泄漏与网络暴露。两者是不同层次的缓解，都要做。",[40,12368,12370],{"id":12369},"dockerfile-里的-user-与非-root-镜像","Dockerfile 里的 USER 与非 root 镜像",[1158,12372,12374],{"className":1430,"code":12373,"language":1432,"meta":1163,"style":1163},"FROM node:22-bookworm-slim\nRUN groupadd --gid 1001 app \\\n    && useradd --uid 1001 --gid app --shell \u002Fusr\u002Fsbin\u002Fnologin --home-dir \u002Fsrv\u002Fshop --no-create-home app\nWORKDIR \u002Fsrv\u002Fshop\nCOPY --chown=1001:1001 . .\nUSER 1001:1001\n",[19,12375,12376,12380,12385,12390,12394,12399],{"__ignoreMap":1163},[1167,12377,12378],{"class":1169,"line":1170},[1167,12379,2615],{},[1167,12381,12382],{"class":1169,"line":1177},[1167,12383,12384],{},"RUN groupadd --gid 1001 app \\\n",[1167,12386,12387],{"class":1169,"line":1202},[1167,12388,12389],{},"    && useradd --uid 1001 --gid app --shell \u002Fusr\u002Fsbin\u002Fnologin --home-dir \u002Fsrv\u002Fshop --no-create-home app\n",[1167,12391,12392],{"class":1169,"line":1208},[1167,12393,2624],{},[1167,12395,12396],{"class":1169,"line":1238},[1167,12397,12398],{},"COPY --chown=1001:1001 . .\n",[1167,12400,12401],{"class":1169,"line":1257},[1167,12402,2649],{},[15,12404,1718],{},[1318,12406,12407,12426,12433,12449],{},[584,12408,12409,12410,11580,12413,12416,12417,12419,12420,1372,12423,12425],{},"用",[31,12411,12412],{},"数字",[19,12414,12415],{},"USER 1001:1001","，别写 ",[19,12418,2414],{},"——写名字时依赖镜像里恰好有那个用户，基础镜像一换就 ",[19,12421,12422],{},"unable to find user app: no matching entries in passwd file",[19,12424,4322],{}," 家族）。",[584,12427,12428,12429,12432],{},"Alpine 系是 ",[19,12430,12431],{},"addgroup -g 1001 app && adduser -S -D -u 1001 -G app app","，参数名与 Debian 完全不同，抄错就构建失败。",[584,12434,12435,12438,12439,12442,12443,2881,12445,12448],{},[31,12436,12437],{},"先装东西、再切用户","（第四章）。切完之后 ",[19,12440,12441],{},"RUN apt-get"," 必然权限不足；需要写目录时提前 ",[19,12444,2421],{},[19,12446,12447],{},"COPY --chown","，别在运行时用 root 去修。",[584,12450,12451,12452,12454,12455,12457,12458,12461],{},"现成的非 root 镜像更省事：",[19,12453,2048],{}," 自带 ",[19,12456,2048],{}," 用户（UID 1000）、",[19,12459,12460],{},"nginxinc\u002Fnginx-unprivileged:1.27-alpine"," 直接听 8080。",[40,12463,12464],{"id":12464},"能力裁剪与安全选项",[1158,12466,12468],{"className":1160,"code":12467,"language":1162,"meta":1163,"style":1163},"docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE --security-opt no-new-privileges:true shop-api:1.8.3\n",[19,12469,12470],{"__ignoreMap":1163},[1167,12471,12472,12474,12476,12478,12481,12484,12487,12490,12492],{"class":1169,"line":1170},[1167,12473,559],{"class":1173},[1167,12475,3380],{"class":1183},[1167,12477,3386],{"class":2703},[1167,12479,12480],{"class":2703}," --cap-drop=ALL",[1167,12482,12483],{"class":2703}," --cap-add=NET_BIND_SERVICE",[1167,12485,12486],{"class":2703}," --security-opt",[1167,12488,12489],{"class":1183}," no-new-privileges:",[1167,12491,8187],{"class":2703},[1167,12493,12494],{"class":1183}," shop-api:1.8.3\n",[45,12496,12497,12508],{},[48,12498,12499],{},[51,12500,12501,12504,12506],{},[54,12502,12503],{},"选项",[54,12505,1566],{},[54,12507,6369],{},[67,12509,12510,12533,12546,12565,12582,12604],{},[51,12511,12512,12517,12520],{},[72,12513,12514],{},[19,12515,12516],{},"--cap-drop=ALL",[72,12518,12519],{},"去掉所有 Linux capability",[72,12521,12522,12523,114,12526,114,12529,12532],{},"默认容器给了约 14 个（含 ",[19,12524,12525],{},"CHOWN",[19,12527,12528],{},"NET_RAW",[19,12530,12531],{},"SETUID","…），够用但偏宽",[51,12534,12535,12540,12543],{},[72,12536,12537],{},[19,12538,12539],{},"--cap-add=NET_BIND_SERVICE",[72,12541,12542],{},"单独加回「绑 1024 以下端口」",[72,12544,12545],{},"需要 80\u002F443 时唯一该加的；更优做法是监听高位端口由反代转发",[51,12547,12548,12553,12556],{},[72,12549,12550],{},[19,12551,12552],{},"--security-opt=no-new-privileges",[72,12554,12555],{},"禁止 setuid 提权",[72,12557,12558,12559,12561,12562],{},"挡住 ",[19,12560,905],{},"\u002FSUID 程序这条最常见路径，成本几乎为零，",[31,12563,12564],{},"建议默认开",[51,12566,12567,12572,12575],{},[72,12568,12569],{},[19,12570,12571],{},"--security-opt=seccomp=default",[72,12573,12574],{},"系统调用白名单",[72,12576,12577,12578,12581],{},"默认档已屏蔽 ~44 个危险调用；个别应用（老 Docker 里的 ",[19,12579,12580],{},"pthreads","、JVM 某些诊断）需要自定义 profile",[51,12583,12584,12589,12592],{},[72,12585,12586],{},[19,12587,12588],{},"--security-opt=apparmor=docker-default",[72,12590,12591],{},"LSM 强制访问控制",[72,12593,12594,12595,12597,12598,793,12601],{},"有 AppArmor 的发行版默认启用；SELinux 环境用 ",[19,12596,6259],{}," 加 ",[19,12599,12600],{},":z",[19,12602,12603],{},":Z",[51,12605,12606,12611,12616],{},[72,12607,12608],{},[19,12609,12610],{},"--privileged",[72,12612,12613],{},[31,12614,12615],{},"放开一切",[72,12617,12618,12619,12622],{},"加全部 capability、关 seccomp 与 AppArmor、能访问所有设备。",[31,12620,12621],{},"永远不要为了「让它跑起来」而加它","，那是把宿主双手交出",[40,12624,12625],{"id":12625},"只读根文件系统与最小写入面",[1158,12627,12629],{"className":1160,"code":12628,"language":1162,"meta":1163,"style":1163},"docker run --rm --read-only --tmpfs \u002Ftmp --tmpfs \u002Frun --user 1001:1001 shop-api:1.8.3\n",[19,12630,12631],{"__ignoreMap":1163},[1167,12632,12633,12635,12637,12639,12642,12645,12648,12650,12653,12656,12659],{"class":1169,"line":1170},[1167,12634,559],{"class":1173},[1167,12636,3380],{"class":1183},[1167,12638,3386],{"class":2703},[1167,12640,12641],{"class":2703}," --read-only",[1167,12643,12644],{"class":2703}," --tmpfs",[1167,12646,12647],{"class":1183}," \u002Ftmp",[1167,12649,12644],{"class":2703},[1167,12651,12652],{"class":1183}," \u002Frun",[1167,12654,12655],{"class":2703}," --user",[1167,12657,12658],{"class":1183}," 1001:1001",[1167,12660,12494],{"class":1183},[15,12662,12663,12665,12666,12669,12670,12673,12674,12677],{},[19,12664,4577],{}," 让「写文件」这件事只在显式挂载的路径上可能，等于把入侵者的持久化路径（放马、改二进制、写 crontab）砍掉。落地顺序很实际：先用 ",[19,12667,12668],{},"docker diff"," + 一段试运行收集所有写入路径（第九章），再逐条给 ",[19,12671,12672],{},"--tmpfs"," 或卷，最后打开 ",[19,12675,12676],{},"read_only: true","（第十二章）。",[40,12679,12680],{"id":12680},"镜像扫描与最小基础镜像",[1158,12682,12684],{"className":1160,"code":12683,"language":1162,"meta":1163,"style":1163},"trivy image --severity HIGH,CRITICAL ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker scout quickview nginx:1.27-alpine        # Docker Desktop\u002FHub 内置\ndocker image inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --format '{{.Created}} {{len .RootFS.Layers}}'\n",[19,12685,12686,12701,12716],{"__ignoreMap":1163},[1167,12687,12688,12691,12693,12696,12699],{"class":1169,"line":1170},[1167,12689,12690],{"class":1173},"trivy",[1167,12692,7223],{"class":1183},[1167,12694,12695],{"class":2703}," --severity",[1167,12697,12698],{"class":1183}," HIGH,CRITICAL",[1167,12700,3430],{"class":1183},[1167,12702,12703,12705,12708,12711,12713],{"class":1169,"line":1177},[1167,12704,559],{"class":1173},[1167,12706,12707],{"class":1183}," scout",[1167,12709,12710],{"class":1183}," quickview",[1167,12712,3601],{"class":1183},[1167,12714,12715],{"class":3604},"        # Docker Desktop\u002FHub 内置\n",[1167,12717,12718,12720,12722,12724,12726,12728],{"class":1169,"line":1202},[1167,12719,559],{"class":1173},[1167,12721,7223],{"class":1183},[1167,12723,3370],{"class":1183},[1167,12725,2707],{"class":1183},[1167,12727,4777],{"class":2703},[1167,12729,12730],{"class":1183}," '{{.Created}} {{len .RootFS.Layers}}'\n",[15,12732,12733,12734,12736,12737,12740],{},"扫描解决「已知 CVE」，不解决「未知漏洞」与配置错误；把 ",[19,12735,12690],{}," 放进 CI 当门（",[19,12738,12739],{},"--exit-code 1","），比事后在服务器上发现强。基础镜像取舍：",[45,12742,12743,12758],{},[48,12744,12745],{},[51,12746,12747,12750,12753,12756],{},[54,12748,12749],{},"选择",[54,12751,12752],{},"体积",[54,12754,12755],{},"好处",[54,12757,4920],{},[67,12759,12760,12776,12798,12825],{},[51,12761,12762,12767,12770,12773],{},[72,12763,12764],{},[19,12765,12766],{},"*-bookworm-slim",[72,12768,12769],{},"中",[72,12771,12772],{},"glibc 兼容、包全、CVE 少",[72,12774,12775],{},"仍有 apt 与 shell，调试方便",[51,12777,12778,12782,12785,12788],{},[72,12779,12780],{},[19,12781,2130],{},[72,12783,12784],{},"小",[72,12786,12787],{},"极小、apk 快",[72,12789,12790,12793,12794,12797],{},[31,12791,12792],{},"musl 而非 glibc","：需要 glibc 的二进制直接失败；预编译 wheel 可能退回源码编译；历史上 musl 的 DNS（线程栈、",[19,12795,12796],{},"getaddrinfo","）行为与 glibc 有差异，出现「只有生产偶发解析超时」",[51,12799,12800,12804,12807,12810],{},[72,12801,12802],{},[19,12803,4707],{},[72,12805,12806],{},"很小",[72,12808,12809],{},"无 shell、无包管理器，攻击面最小",[72,12811,12812,12818,12819,12822,12823,1075],{},[31,12813,12814,12815,12817],{},"无法 ",[19,12816,3734],{}," 调试","（第七章的方案全换：用 ",[19,12820,12821],{},"docker run --rm --entrypoint"," 或调试专用镜像、",[19,12824,4944],{},[51,12826,12827,12831,12834,12837],{},[72,12828,12829],{},[19,12830,4710],{},[72,12832,12833],{},"最小",[72,12835,12836],{},"只有你的二进制",[72,12838,12839],{},"要自己处理 CA 证书、时区文件、DNS（静态编译才可行）",[15,12841,12842,12845,12846,12849,12850,12853,12854,737,12857,616],{},[19,12843,12844],{},"glibc vs musl"," 的坑在 Python（numpy\u002Fpsycopg 类需编译的包）和任何自带 ",[19,12847,12848],{},".so"," 的 SDK 上出现最多。判断办法：",[19,12851,12852],{},"docker exec c ldd \u002Fusr\u002Flocal\u002Fbin\u002Fapp"," 看链接的是 ",[19,12855,12856],{},"libc.musl*",[19,12858,12859],{},"libc.so.6",[40,12861,12863],{"id":12862},"密钥泄漏为什么-arg-env-run-都不行","密钥泄漏：为什么 ARG \u002F ENV \u002F RUN 都不行",[1158,12865,12867],{"className":1430,"code":12866,"language":1432,"meta":1163,"style":1163},"ARG TOKEN\nRUN curl -H \"Authorization: token ${TOKEN}\" https:\u002F\u002Fapi.github.com\u002F...   # 错误示范\nENV DB_PASSWORD=hunter2                                                   # 错误示范\nRUN git clone https:\u002F\u002Fuser:password@github.com\u002Fexample\u002Fprivate.git        # 错误示范\nRUN npm ci --token=${NPM_TOKEN} && rm -rf \u002Froot\u002F.npmrc                    # rm 也救不了\n",[19,12868,12869,12874,12879,12884,12889],{"__ignoreMap":1163},[1167,12870,12871],{"class":1169,"line":1170},[1167,12872,12873],{},"ARG TOKEN\n",[1167,12875,12876],{"class":1169,"line":1177},[1167,12877,12878],{},"RUN curl -H \"Authorization: token ${TOKEN}\" https:\u002F\u002Fapi.github.com\u002F...   # 错误示范\n",[1167,12880,12881],{"class":1169,"line":1202},[1167,12882,12883],{},"ENV DB_PASSWORD=hunter2                                                   # 错误示范\n",[1167,12885,12886],{"class":1169,"line":1208},[1167,12887,12888],{},"RUN git clone https:\u002F\u002Fuser:password@github.com\u002Fexample\u002Fprivate.git        # 错误示范\n",[1167,12890,12891],{"class":1169,"line":1238},[1167,12892,12893],{},"RUN npm ci --token=${NPM_TOKEN} && rm -rf \u002Froot\u002F.npmrc                    # rm 也救不了\n",[15,12895,12896,12897,12899,12900,12902,12903,12905,12906,12909,12910,12912,12913,12915,12916,12918,12919,12597,12921,616],{},"三条都能被捞出来：",[19,12898,2313],{}," 直接打印指令文本；",[19,12901,1508],{}," 后的层 tar 里文件还在（写时复制：",[19,12904,6680],{}," 只是遮住）；镜像被 pull 走后你无从审计。",[31,12907,12908],{},"「后面删掉」不是修复","，因为它只是在上层放了 whiteout。正确做法只有两条：构建期用 ",[19,12911,2317],{},"（第五章），运行期注入（第十三章）。同理，",[19,12914,1768],{}," 一个带 ",[19,12917,2884],{}," 的目录会把整个提交历史（含密钥）留在镜像里——",[19,12920,1881],{},[19,12922,2884],{},[15,12924,12925],{},"其余几条硬规则：",[1318,12927,12928,12940,12959],{},[584,12929,12930,12936,12937,12939],{},[31,12931,12932,728,12934],{},[19,12933,4152],{},[19,12935,10120],{},"：PID 1 负责收尸，否则僵尸进程累积到 ",[19,12938,11837],{}," 就起新进程失败。",[584,12941,12942,12951,12952,12954,12955,12958],{},[31,12943,12944,728,12947,12950],{},[19,12945,12946],{},"--pid=container:x",[19,12948,12949],{},"--ipc=container:x"," 共享","：共享 PID 意味着能看见甚至影响邻居的进程（",[19,12953,3714],{},"、读 ",[19,12956,12957],{},"\u002Fproc\u002F*\u002Fenviron"," 里的密钥）；共享 IPC 意味着能读写对方共享内存。除非明确需要（如 sidecar 调试），不要共享。",[584,12960,12961,12967,12968,12971,12972,12975],{},[31,12962,12963,12964,12966],{},"挂载 ",[19,12965,563],{}," 等于交出宿主 root","：容器里一句 ",[19,12969,12970],{},"docker run -v \u002F:\u002Fhost alpine chroot \u002Fhost"," 就能拿到宿主。真需要「容器里管容器」（CI、Portainer、watchtower），用只读 socket 代理（",[19,12973,12974],{},"tecnativa\u002Fdocker-socket-proxy","）白名单最小 API，或者用 rootless daemon。",[10,12977,12979],{"id":12978},"十六生产运行重启策略健康检查优雅停机与备份","十六、生产运行：重启策略、健康检查、优雅停机与备份",[40,12981,12983,12986],{"id":12982},"restart-四种策略怎么选",[19,12984,12985],{},"--restart"," 四种策略怎么选",[45,12988,12989,13009],{},[48,12990,12991],{},[51,12992,12993,12996,12999,13002],{},[54,12994,12995],{},"策略",[54,12997,12998],{},"daemon 重启后",[54,13000,13001],{},"应用崩溃",[54,13003,13004,13005,13008],{},"你手动 ",[19,13006,13007],{},"docker stop"," 后",[67,13010,13011,13025,13042,13055],{},[51,13012,13013,13018,13021,13023],{},[72,13014,13015,5362],{},[19,13016,13017],{},"no",[72,13019,13020],{},"不拉起",[72,13022,13020],{},[72,13024,13020],{},[51,13026,13027,13032,13037,13040],{},[72,13028,13029],{},[19,13030,13031],{},"on-failure:5",[72,13033,13034],{},[31,13035,13036],{},"拉起",[72,13038,13039],{},"拉起，最多 5 次",[72,13041,13020],{},[51,13043,13044,13048,13051,13053],{},[72,13045,13046],{},[19,13047,8268],{},[72,13049,13050],{},"拉起（除非它此前是被手动停的）",[72,13052,13036],{},[72,13054,13020],{},[51,13056,13057,13061,13063,13065],{},[72,13058,13059],{},[19,13060,10100],{},[72,13062,13036],{},[72,13064,13036],{},[72,13066,13067,13070],{},[31,13068,13069],{},"也会拉起","（daemon 重启时）",[15,13072,13073,13074,13076,13077,13079,13080,13083],{},"单机生产的答案基本都是 ",[19,13075,8268],{},"：它尊重你的「我就是要停掉它」这个意图，",[19,13078,10100],{}," 则会在你重启 Docker 之后把已经手动停掉的容器又拉起来（升级维护时这会很难堪）。",[19,13081,13082],{},"on-failure"," 适合一次性任务与迁移脚本（退出码非 0 才重试）。",[15,13085,13086,13087,13095,13096,13099],{},"要注意的盲区：",[31,13088,13089,13091,13092],{},[19,13090,12985],{}," 不处理 ",[19,13093,13094],{},"unhealthy","。健康检查连续失败只会让状态变成 ",[19,13097,13098],{},"(unhealthy)","，容器照跑（第六章）。自愈要么靠应用自己崩溃退出（配合 restart），要么外部加 watcher，要么上编排（第十七章）。",[40,13101,13103],{"id":13102},"healthcheck-与就绪判断","HEALTHCHECK 与就绪判断",[1158,13105,13107],{"className":1160,"code":13106,"language":1162,"meta":1163,"style":1163},"docker inspect --format '{{.State.Health.Status}}' shop-api\ndocker inspect --format '{{json .State.Health}}' shop-api | jq '.Log[-3:]'   # 最近三次检查记录\n",[19,13108,13109,13122],{"__ignoreMap":1163},[1167,13110,13111,13113,13115,13117,13120],{"class":1169,"line":1170},[1167,13112,559],{"class":1173},[1167,13114,3370],{"class":1183},[1167,13116,4777],{"class":2703},[1167,13118,13119],{"class":1183}," '{{.State.Health.Status}}'",[1167,13121,11762],{"class":1183},[1167,13123,13124,13126,13128,13130,13133,13135,13137,13140,13143],{"class":1169,"line":1177},[1167,13125,559],{"class":1173},[1167,13127,3370],{"class":1183},[1167,13129,4777],{"class":2703},[1167,13131,13132],{"class":1183}," '{{json .State.Health}}'",[1167,13134,4461],{"class":1183},[1167,13136,3506],{"class":3505},[1167,13138,13139],{"class":1173}," jq",[1167,13141,13142],{"class":1183}," '.Log[-3:]'",[1167,13144,13145],{"class":3604},"   # 最近三次检查记录\n",[15,13147,13148,13150,13151,13154,13155,13158,13159,13162,13163,13165,13166,13168,13169,13172],{},[19,13149,2502],{}," 的四个参数各自管什么：",[19,13152,13153],{},"--interval"," 检查频率、",[19,13156,13157],{},"--timeout"," 单次超时、",[19,13160,13161],{},"--retries"," 连续失败几次算 unhealthy、",[19,13164,4229],{}," 启动宽限期（期间失败不计、状态是 ",[19,13167,9215],{},"）。生产最常见的事故是",[31,13170,13171],{},"没设 start-period","：应用冷启动要 25 秒，前两次检查失败被判 unhealthy，编排层不停重建，永远起不来。",[15,13174,13175,951,13177,13180,13181,114,13184,114,13187,793,13190,13193,13194,13197],{},[19,13176,4358],{},[19,13178,13179],{},".State.Health.Log"," 里有每次执行的 ",[19,13182,13183],{},"Exit",[19,13185,13186],{},"Output",[19,13188,13189],{},"Start",[19,13191,13192],{},"End","，这是判断「到底是超时还是返回码不对」的唯一依据。另外 Compose 的 ",[19,13195,13196],{},"condition: service_healthy","（第十一章）依赖它。",[40,13199,13201],{"id":13200},"优雅停机信号怎么传pid-1-问题与解法","优雅停机：信号怎么传，PID 1 问题与解法",[15,13203,13204,13206,13207,13214,13215,616],{},[19,13205,13007],{}," 的完整时序是：",[31,13208,13209,13210,13213],{},"发 SIGTERM → 等 ",[19,13211,13212],{},"--stop-timeout","（默认 10s）→ 还没退出就 SIGKILL","。应用要在窗口内做完：停止接受新连接 → 等手头请求结束 → 关连接池 → flush 日志 → ",[19,13216,857],{},[15,13218,13219],{},"失效的原因在第四章埋的伏笔：",[1158,13221,13223],{"className":1430,"code":13222,"language":1432,"meta":1163,"style":1163},"CMD [\"sh\", \"-c\", \"node app.js\"]      # PID 1 = sh，node 是它的子进程\n",[19,13224,13225],{"__ignoreMap":1163},[1167,13226,13227],{"class":1169,"line":1170},[1167,13228,13222],{},[15,13230,13231,13234,13235,13237,13238,13241,13242,13245],{},[19,13232,13233],{},"sh -c"," 起的子进程",[31,13236,409],{}," PID 1，而 shell 默认不转发收到的 SIGTERM（它只等子进程结束），于是：容器收到信号 → sh 无动作 → 10 秒后 Docker 发 SIGKILL → node 被硬杀 → 退出码 137、请求被截断、事务未提交。同样地，shell 形式 ",[19,13239,13240],{},"CMD node app.js","（不带 JSON 数组）也是经 ",[19,13243,13244],{},"\u002Fbin\u002Fsh -c"," 执行，症状一致。",[15,13247,13248],{},"三种解法：",[1158,13250,13252],{"className":1430,"code":13251,"language":1432,"meta":1163,"style":1163},"CMD [\"node\", \"app.js\"]                 # ① exec 形式：node 就是 PID 1\nENTRYPOINT [\"tini\", \"--\"]              # ② 装 tini（或直接 --init）\nCMD [\"node\", \"src\u002Fserver.js\"]\n",[19,13253,13254,13259,13264],{"__ignoreMap":1163},[1167,13255,13256],{"class":1169,"line":1170},[1167,13257,13258],{},"CMD [\"node\", \"app.js\"]                 # ① exec 形式：node 就是 PID 1\n",[1167,13260,13261],{"class":1169,"line":1177},[1167,13262,13263],{},"ENTRYPOINT [\"tini\", \"--\"]              # ② 装 tini（或直接 --init）\n",[1167,13265,13266],{"class":1169,"line":1202},[1167,13267,2659],{},[1158,13269,13271],{"className":1160,"code":13270,"language":1162,"meta":1163,"style":1163},"docker run -d --init --stop-timeout 30 shop-api:1.8.3    # ③ --init 由 Docker 注入 tini\n",[19,13272,13273],{"__ignoreMap":1163},[1167,13274,13275,13277,13279,13281,13284,13287,13290,13292],{"class":1169,"line":1170},[1167,13276,559],{"class":1173},[1167,13278,3380],{"class":1183},[1167,13280,5611],{"class":2703},[1167,13282,13283],{"class":2703}," --init",[1167,13285,13286],{"class":2703}," --stop-timeout",[1167,13288,13289],{"class":2703}," 30",[1167,13291,2733],{"class":1183},[1167,13293,13294],{"class":3604},"    # ③ --init 由 Docker 注入 tini\n",[1318,13296,13297,13312,13326],{},[584,13298,13299,13300,13303,13304,13307,13308,13311],{},"exec 形式最省事，但要求应用自己处理 SIGTERM（Node：",[19,13301,13302],{},"process.on('SIGTERM', …)","；Python：",[19,13305,13306],{},"signal.signal","；Go：",[19,13309,13310],{},"signal.NotifyContext","；Java：ShutdownHook）。",[584,13313,13314,728,13316,13319,13320,13323,13324,1962],{},[19,13315,4152],{},[19,13317,13318],{},"tini"," 除了转发信号还负责收尸（僵尸进程），是",[31,13321,13322],{},"通用建议默认开","的选项（第十二章 ",[19,13325,10120],{},[584,13327,13328,13329,13331,13332,7119,13335,13337],{},"若一定要在 CMD 里做变量替换，用 ",[19,13330,3734],{}," 内联替换当前 shell：",[19,13333,13334],{},"CMD [\"sh\",\"-c\",\"exec node $ENTRY_FILE\"]",[19,13336,3734],{}," 让 node 顶替 sh 成为 PID 1。",[15,13339,13340,13341,13343,13344,13347,13348,793,13351,13354,13355,13358,13359,13362],{},"验证是否真的收到信号：",[19,13342,13007],{}," 时看日志有没有你的 graceful-shutdown 行，以及 ",[19,13345,13346],{},"docker inspect --format '{{.State.ExitCode}}'"," 是 ",[19,13349,13350],{},"143",[19,13352,13353],{},"137","（没处理）还是 ",[19,13356,13357],{},"0","（处理了）。示例 16 把这套对比做成终端演示：同一个镜像，shell 形式 CMD 与 exec 形式 CMD 在 ",[19,13360,13361],{},"docker stop -t 15"," 下的不同结局。",[1405,13364],{"height":5320,"src":13365,"title":13366},"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F16-graceful-stop.html","示例 16：优雅停机——信号到不到 PID 1",[40,13368,13370],{"id":13369},"日志json-file-的轮转必须设","日志：json-file 的轮转必须设",[15,13372,13373,13374,13376,13377,13380,13381,13384,13385,13388],{},"默认 ",[19,13375,9662],{}," 驱动",[31,13378,13379],{},"不限制大小","：一台机器跑三个月，",[19,13382,13383],{},"\u002Fvar\u002Flib\u002Fdocker\u002Fcontainers\u002F\u003Cid>\u002F\u003Cid>-json.log"," 能长到几十 GB，把磁盘写满（第十八章的 ",[19,13386,13387],{},"no space left on device"," 一大半是这个）。",[1158,13390,13392],{"className":7446,"code":13391,"language":7448,"meta":1163,"style":1163},"{ \"log-driver\": \"json-file\", \"log-opts\": { \"max-size\": \"20m\", \"max-file\": \"3\", \"compress\": \"true\" } }\n",[19,13393,13394],{"__ignoreMap":1163},[1167,13395,13396,13398,13401,13403,13405,13407,13410,13412,13414,13416,13418,13420,13422,13424,13426,13428,13431,13433,13435],{"class":1169,"line":1170},[1167,13397,7455],{"class":1216},[1167,13399,13400],{"class":2703},"\"log-driver\"",[1167,13402,7620],{"class":1216},[1167,13404,7623],{"class":1183},[1167,13406,1223],{"class":1216},[1167,13408,13409],{"class":2703},"\"log-opts\"",[1167,13411,7634],{"class":1216},[1167,13413,7637],{"class":2703},[1167,13415,7620],{"class":1216},[1167,13417,7642],{"class":1183},[1167,13419,1223],{"class":1216},[1167,13421,7647],{"class":2703},[1167,13423,7620],{"class":1216},[1167,13425,7652],{"class":1183},[1167,13427,1223],{"class":1216},[1167,13429,13430],{"class":2703},"\"compress\"",[1167,13432,7620],{"class":1216},[1167,13434,8183],{"class":1183},[1167,13436,8404],{"class":1216},[15,13438,13439,13440,13442,13443,13445,13446,13449,13450,616],{},"写进 ",[19,13441,7442],{}," 是全局兜底；单个服务在 Compose 里写 ",[19,13444,10238],{},"（第十二章）。改完只影响",[31,13447,13448],{},"之后创建的容器","，已有容器要 ",[19,13451,13452],{},"docker compose up -d --force-recreate",[15,13454,13455,13456,1372,13459,13462,13463,13466,13467,13470,13471,13474,13475,13478,13479,13481,13482,3177,13485,13487,13488,1962],{},"其他驱动与取舍：",[19,13457,13458],{},"syslog",[19,13460,13461],{},"--log-opt syslog-address=udp:\u002F\u002F10.0.0.7:514 --log-opt tag={{.Name}}","，走集中式，注意 UDP 会丢）、",[19,13464,13465],{},"journald","（能 ",[19,13468,13469],{},"journalctl -u docker"," 一起看）、",[19,13472,13473],{},"local","（Docker 自带的轻量二进制日志，daemon 重启不丢，但生态工具读不了）、",[19,13476,13477],{},"fluentd","（采集侧最常用，配合 Fluent Bit 直接读 json-file 也行）、",[19,13480,5396],{},"（一次性任务用）。采集侧的通用做法：Agent 以 DaemonSet\u002Fsidecar 方式挂 ",[19,13483,13484],{},"\u002Fvar\u002Flib\u002Fdocker\u002Fcontainers:ro",[19,13486,563],{},"，按 label 打标（第六章 ",[19,13489,13490],{},"--label",[40,13492,13494],{"id":13493},"时区locale-与镜像版本固定","时区、locale 与镜像版本固定",[1158,13496,13498],{"className":1430,"code":13497,"language":1432,"meta":1163,"style":1163},"FROM node:22-bookworm-slim\nENV TZ=Asia\u002FShanghai\nRUN apt-get update && apt-get install -y --no-install-recommends tzdata && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[19,13499,13500,13504,13509],{"__ignoreMap":1163},[1167,13501,13502],{"class":1169,"line":1170},[1167,13503,2615],{},[1167,13505,13506],{"class":1169,"line":1177},[1167,13507,13508],{},"ENV TZ=Asia\u002FShanghai\n",[1167,13510,13511],{"class":1169,"line":1202},[1167,13512,13513],{},"RUN apt-get update && apt-get install -y --no-install-recommends tzdata && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[15,13515,13516,13518,13519,3819,13522,13524,13525,13528,13529,3177,13532,13535,13536,13539,13540,13543],{},[19,13517,2255],{}," 环境变量在 glibc\u002Fmusl 上都生效，但",[31,13520,13521],{},"镜像里必须有 tzdata",[19,13523,2255],{}," 无效退化成 UTC（alpine 要 ",[19,13526,13527],{},"apk add tzdata","）。更稳的做法是把宿主 ",[19,13530,13531],{},"\u002Fetc\u002Flocaltime",[19,13533,13534],{},"\u002Fusr\u002Fshare\u002Fzoneinfo"," 只读挂进去。日志时间戳与数据库时区不一致，九成是这个原因。locale 上 slim\u002Falpine 默认 ",[19,13537,13538],{},"C.UTF-8","，够 JSON 与 ASCII 用；要中文排序\u002F",[19,13541,13542],{},"strcoll"," 才需要生成 locale（或换 Debian 完整版）——别为了一个报错在镜像里装全套语言包。",[15,13545,13546,13547,939,13550,13552],{},"版本固定与回滚的思路（第十章的 tag 语义在这里落地）：部署清单里记 ",[31,13548,13549],{},"digest",[19,13551,9209],{}," 前先拉新再切；单机土法滚动：",[1158,13554,13556],{"className":1160,"code":13555,"language":1162,"meta":1163,"style":1163},"docker compose -f compose.yaml -f compose.prod.yaml up -d --no-recreate api   # 新 api 起在 8081\ndocker inspect --format '{{.State.Health.Status}}' shop-api-2                  # 等 healthy\ndocker exec shop-web nginx -s reload                                          # 上游切到新实例\ndocker stop shop-api && docker rm shop-api\n",[19,13557,13558,13584,13600,13619],{"__ignoreMap":1163},[1167,13559,13560,13562,13564,13566,13568,13570,13572,13574,13576,13579,13581],{"class":1169,"line":1170},[1167,13561,559],{"class":1173},[1167,13563,7896],{"class":1183},[1167,13565,2719],{"class":2703},[1167,13567,9444],{"class":1183},[1167,13569,2719],{"class":2703},[1167,13571,9449],{"class":1183},[1167,13573,7911],{"class":1183},[1167,13575,5611],{"class":2703},[1167,13577,13578],{"class":2703}," --no-recreate",[1167,13580,5626],{"class":1183},[1167,13582,13583],{"class":3604},"   # 新 api 起在 8081\n",[1167,13585,13586,13588,13590,13592,13594,13597],{"class":1169,"line":1177},[1167,13587,559],{"class":1173},[1167,13589,3370],{"class":1183},[1167,13591,4777],{"class":2703},[1167,13593,13119],{"class":1183},[1167,13595,13596],{"class":1183}," shop-api-2",[1167,13598,13599],{"class":3604},"                  # 等 healthy\n",[1167,13601,13602,13604,13606,13608,13611,13613,13616],{"class":1169,"line":1202},[1167,13603,559],{"class":1173},[1167,13605,4629],{"class":1183},[1167,13607,3598],{"class":1183},[1167,13609,13610],{"class":1183}," nginx",[1167,13612,3666],{"class":2703},[1167,13614,13615],{"class":1183}," reload",[1167,13617,13618],{"class":3604},"                                          # 上游切到新实例\n",[1167,13620,13621,13623,13625,13627,13629,13631,13633],{"class":1169,"line":1208},[1167,13622,559],{"class":1173},[1167,13624,3646],{"class":1183},[1167,13626,4461],{"class":1183},[1167,13628,7738],{"class":1216},[1167,13630,559],{"class":1173},[1167,13632,1196],{"class":1183},[1167,13634,11762],{"class":1183},[15,13636,13637,13638,13640,13641,13644,13645,13647],{},"回滚就是把旧 digest 再 ",[19,13639,7883],{}," 一次——前提是",[31,13642,13643],{},"镜像还留着、且数据库迁移是向后兼容的","（迁移脚本要能回滚或双写，这是应用层设计，Docker 帮不了你）。蓝绿在单机 Docker 下的实质就是「两套端口 + 反代切换」，别指望 ",[19,13646,12985],{}," 帮你做发布。",[40,13649,13650],{"id":13650},"备份与监控",[15,13652,13653],{},"单机备份三件套，缺一不可：",[1158,13655,13657],{"className":1160,"code":13656,"language":1162,"meta":1163,"style":1163},"tar czf \u002Fbackup\u002Fshop-${DATE}.tar.gz -C \u002Fsrv\u002Fshop compose.yaml compose.prod.yaml config nginx secrets\u002Fdb_password.txt\ndocker run --rm --volumes-from shop-db -v \u002Fbackup:\u002Fbackup alpine tar czf \u002Fbackup\u002Fdb-${DATE}.tar.gz \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\ndocker save ghcr.io\u002Fexample\u002Fshop-api:1.8.3 | gzip > \u002Fbackup\u002Fshop-api-1.8.3.tar.gz\n",[19,13658,13659,13690,13723],{"__ignoreMap":1163},[1167,13660,13661,13664,13666,13669,13672,13675,13677,13679,13681,13683,13685,13687],{"class":1169,"line":1170},[1167,13662,13663],{"class":1173},"tar",[1167,13665,6554],{"class":1183},[1167,13667,13668],{"class":1183}," \u002Fbackup\u002Fshop-",[1167,13670,13671],{"class":1216},"${DATE}",[1167,13673,13674],{"class":1183},".tar.gz",[1167,13676,6560],{"class":2703},[1167,13678,1282],{"class":1183},[1167,13680,9444],{"class":1183},[1167,13682,9449],{"class":1183},[1167,13684,8954],{"class":1183},[1167,13686,13610],{"class":1183},[1167,13688,13689],{"class":1183}," secrets\u002Fdb_password.txt\n",[1167,13691,13692,13694,13696,13698,13700,13702,13704,13707,13709,13711,13713,13716,13718,13720],{"class":1169,"line":1177},[1167,13693,559],{"class":1173},[1167,13695,3380],{"class":1183},[1167,13697,3386],{"class":2703},[1167,13699,6527],{"class":2703},[1167,13701,5664],{"class":1183},[1167,13703,6285],{"class":2703},[1167,13705,13706],{"class":1183}," \u002Fbackup:\u002Fbackup",[1167,13708,6543],{"class":1183},[1167,13710,6714],{"class":1183},[1167,13712,6554],{"class":1183},[1167,13714,13715],{"class":1183}," \u002Fbackup\u002Fdb-",[1167,13717,13671],{"class":1216},[1167,13719,13674],{"class":1183},[1167,13721,13722],{"class":1183}," \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n",[1167,13724,13725,13727,13729,13731,13733,13736,13738],{"class":1169,"line":1202},[1167,13726,559],{"class":1173},[1167,13728,7764],{"class":1183},[1167,13730,2707],{"class":1183},[1167,13732,3506],{"class":3505},[1167,13734,13735],{"class":1173}," gzip",[1167,13737,11108],{"class":3505},[1167,13739,13740],{"class":1183}," \u002Fbackup\u002Fshop-api-1.8.3.tar.gz\n",[15,13742,13743,13744,13746,13747,13749],{},"配置（文本，进 git 或对象存储）+ 数据（卷，",[19,13745,6786],{}," 优先于文件级）+ 镜像（tar，保证能重建当时的运行态）。恢复演练比备份本身重要：季度性地在一台干净机器上「只用备份」把站起回来（第十章的 ",[19,13748,1540],{}," 在这里用）。",[15,13751,13752,13753,13755,13756,906],{},"监控侧：",[19,13754,390],{}," 是即时快照、无历史、无告警（第十四章），单机最小方案是 ",[31,13757,13758],{},"cAdvisor（容器指标）+ node_exporter（宿主）+ Prometheus + Grafana",[1158,13760,13762],{"className":1160,"code":13761,"language":1162,"meta":1163,"style":1163},"docker run -d --name cadvisor --restart unless-stopped \\\n  --volume=\u002F:\u002Frootfs:ro --volume=\u002Fvar\u002Frun:\u002Fvar\u002Frun:ro \\\n  --volume=\u002Fsys:\u002Fsys:ro --volume=\u002Fvar\u002Flib\u002Fdocker\u002F:\u002Fvar\u002Flib\u002Fdocker:ro \\\n  --publish 127.0.0.1:8081:8080 --detach=true gcr.io\u002Fcadvisor\u002Fcadvisor:v0.49.1\n",[19,13763,13764,13783,13793,13803],{"__ignoreMap":1163},[1167,13765,13766,13768,13770,13772,13774,13777,13779,13781],{"class":1169,"line":1170},[1167,13767,559],{"class":1173},[1167,13769,3380],{"class":1183},[1167,13771,5611],{"class":2703},[1167,13773,3349],{"class":2703},[1167,13775,13776],{"class":1183}," cadvisor",[1167,13778,7381],{"class":2703},[1167,13780,7384],{"class":1183},[1167,13782,6546],{"class":2703},[1167,13784,13785,13788,13791],{"class":1169,"line":1177},[1167,13786,13787],{"class":2703},"  --volume=\u002F:\u002Frootfs:ro",[1167,13789,13790],{"class":2703}," --volume=\u002Fvar\u002Frun:\u002Fvar\u002Frun:ro",[1167,13792,6546],{"class":2703},[1167,13794,13795,13798,13801],{"class":1169,"line":1202},[1167,13796,13797],{"class":2703},"  --volume=\u002Fsys:\u002Fsys:ro",[1167,13799,13800],{"class":2703}," --volume=\u002Fvar\u002Flib\u002Fdocker\u002F:\u002Fvar\u002Flib\u002Fdocker:ro",[1167,13802,6546],{"class":2703},[1167,13804,13805,13808,13811,13814],{"class":1169,"line":1208},[1167,13806,13807],{"class":2703},"  --publish",[1167,13809,13810],{"class":1183}," 127.0.0.1:8081:8080",[1167,13812,13813],{"class":2703}," --detach=true",[1167,13815,13816],{"class":1183}," gcr.io\u002Fcadvisor\u002Fcadvisor:v0.49.1\n",[15,13818,13819,13820,114,13823,13825,13826,13828,13829,13832,13833,13835,13836,13838],{},"cAdvisor 需要看到宿主的 ",[19,13821,13822],{},"\u002Fsys",[19,13824,1055],{}," 与 docker socket——这正好是上面「挂 socket 等于交出 root」的现实权衡：监控系统要有边界（只绑回环、单独网络、",[19,13827,4577],{}," 应用不到它但 ",[19,13830,13831],{},"no-new-privileges"," 要加）。告警至少配三条：容器 OOMKilled 次数、",[19,13834,13094],{}," 状态持续、",[19,13837,1055],{}," 使用率。",[10,13840,13842],{"id":13841},"十七编排与生态compose-的天花板swarm-与-kubernetes","十七、编排与生态：Compose 的天花板、Swarm 与 Kubernetes",[40,13844,13846],{"id":13845},"单机-compose-的天花板在哪","单机 Compose 的天花板在哪",[15,13848,13849],{},"Compose 是「一台机器上的应用描述器」。它的限制不是功能少，而是模型里没有「集群」这个概念：",[45,13851,13852,13865],{},[48,13853,13854],{},[51,13855,13856,13859,13862],{},[54,13857,13858],{},"想做",[54,13860,13861],{},"Compose 单机能做到吗",[54,13863,13864],{},"缺口",[67,13866,13867,13877,13888,13899,13911,13925,13936],{},[51,13868,13869,13872,13874],{},[72,13870,13871],{},"跨机器调度",[72,13873,1796],{},[72,13875,13876],{},"没有节点资源视图与放置逻辑",[51,13878,13879,13882,13885],{},[72,13880,13881],{},"副本自动重分布",[72,13883,13884],{},"✗（机器挂了服务就挂）",[72,13886,13887],{},"无健康重建、无 failover",[51,13889,13890,13893,13896],{},[72,13891,13892],{},"滚动更新与自动回滚",[72,13894,13895],{},"土法（第十六章）",[72,13897,13898],{},"无 rollout 状态机",[51,13900,13901,13904,13906],{},[72,13902,13903],{},"端口冲突（两机同端口）",[72,13905,1796],{},[72,13907,13908,13910],{},[19,13909,8140],{}," 是宿主级的，扩容即冲突",[51,13912,13913,13916,13922],{},[72,13914,13915],{},"密钥加密分发",[72,13917,13918,13919,13921],{},"✗（",[19,13920,8019],{}," driver 落盘）",[72,13923,13924],{},"第十三章那条区别在这里最要命",[51,13926,13927,13930,13933],{},[72,13928,13929],{},"资源配额与优先级",[72,13931,13932],{},"部分（limits）",[72,13934,13935],{},"无抢占、无队列",[51,13937,13938,13941,13944],{},[72,13939,13940],{},"服务发现跨机",[72,13942,13943],{},"靠外部 LB",[72,13945,13946],{},"无内置 VIP\u002F负载均衡",[15,13948,13949,13950,13953],{},"所以判断标准很直接：",[31,13951,13952],{},"一台机器能装下、且你能接受它挂了就手动恢复","，Compose 够；否则要编排。",[40,13955,13957],{"id":13956},"swarm-的最小可用集","Swarm 的最小可用集",[15,13959,13960],{},"Swarm 是 Docker 原生的编排，命令风格与 Compose 连续，上手成本远低于 K8s：",[1158,13962,13964],{"className":1160,"code":13963,"language":1162,"meta":1163,"style":1163},"docker swarm init --advertise-addr 10.0.0.11\ndocker swarm join-token worker          # 拿到 join 命令给其他节点\ndocker node ls\ndocker node update --availability drain node-2        # 下线维护（先迁移副本）\n",[19,13965,13966,13982,13997,14006],{"__ignoreMap":1163},[1167,13967,13968,13970,13973,13976,13979],{"class":1169,"line":1170},[1167,13969,559],{"class":1173},[1167,13971,13972],{"class":1183}," swarm",[1167,13974,13975],{"class":1183}," init",[1167,13977,13978],{"class":2703}," --advertise-addr",[1167,13980,13981],{"class":2703}," 10.0.0.11\n",[1167,13983,13984,13986,13988,13991,13994],{"class":1169,"line":1177},[1167,13985,559],{"class":1173},[1167,13987,13972],{"class":1183},[1167,13989,13990],{"class":1183}," join-token",[1167,13992,13993],{"class":1183}," worker",[1167,13995,13996],{"class":3604},"          # 拿到 join 命令给其他节点\n",[1167,13998,13999,14001,14003],{"class":1169,"line":1202},[1167,14000,559],{"class":1173},[1167,14002,9062],{"class":1183},[1167,14004,14005],{"class":1183}," ls\n",[1167,14007,14008,14010,14012,14015,14018,14021,14024],{"class":1169,"line":1208},[1167,14009,559],{"class":1173},[1167,14011,9062],{"class":1183},[1167,14013,14014],{"class":1183}," update",[1167,14016,14017],{"class":2703}," --availability",[1167,14019,14020],{"class":1183}," drain",[1167,14022,14023],{"class":1183}," node-2",[1167,14025,14026],{"class":3604},"        # 下线维护（先迁移副本）\n",[1158,14028,14030],{"className":1160,"code":14029,"language":1162,"meta":1163,"style":1163},"docker network create --driver overlay --attachable shop-net\ndocker secret create db_password .\u002Fsecrets\u002Fdb_password.txt\ndocker service create --name shop-api --replicas 3 -p 8080:3000 \\\n  --network shop-net --mount type=volume,source=uploads,target=\u002Fsrv\u002Fshop\u002Fuploads \\\n  --limit-cpu 1 --limit-memory 512M --update-parallelism 1 --update-delay 10s \\\n  --constraint node.labels.zone==cn-east-a \\\n  ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0ea7b2d4\n\ndocker service scale shop-api=6\ndocker service update --image ghcr.io\u002Fexample\u002Fshop-api:1.9.0 shop-api\ndocker service ps shop-api --no-trunc           # 每个副本在哪台、为什么失败\ndocker service logs -f shop-api\ndocker service inspect shop-api --pretty\n",[19,14031,14032,14050,14065,14091,14105,14131,14141,14146,14150,14164,14180,14196,14208],{"__ignoreMap":1163},[1167,14033,14034,14036,14038,14040,14042,14045,14048],{"class":1169,"line":1170},[1167,14035,559],{"class":1173},[1167,14037,5521],{"class":1183},[1167,14039,3346],{"class":1183},[1167,14041,3355],{"class":2703},[1167,14043,14044],{"class":1183}," overlay",[1167,14046,14047],{"class":2703}," --attachable",[1167,14049,5543],{"class":1183},[1167,14051,14052,14054,14057,14059,14062],{"class":1169,"line":1177},[1167,14053,559],{"class":1173},[1167,14055,14056],{"class":1183}," secret",[1167,14058,3346],{"class":1183},[1167,14060,14061],{"class":1183}," db_password",[1167,14063,14064],{"class":1183}," .\u002Fsecrets\u002Fdb_password.txt\n",[1167,14066,14067,14069,14072,14074,14076,14078,14081,14084,14086,14089],{"class":1169,"line":1202},[1167,14068,559],{"class":1173},[1167,14070,14071],{"class":1183}," service",[1167,14073,3346],{"class":1183},[1167,14075,3349],{"class":2703},[1167,14077,4461],{"class":1183},[1167,14079,14080],{"class":2703}," --replicas",[1167,14082,14083],{"class":2703}," 3",[1167,14085,4695],{"class":2703},[1167,14087,14088],{"class":1183}," 8080:3000",[1167,14090,6546],{"class":2703},[1167,14092,14093,14096,14098,14100,14103],{"class":1169,"line":1208},[1167,14094,14095],{"class":2703},"  --network",[1167,14097,5555],{"class":1183},[1167,14099,6306],{"class":2703},[1167,14101,14102],{"class":1183}," type=volume,source=uploads,target=\u002Fsrv\u002Fshop\u002Fuploads",[1167,14104,6546],{"class":2703},[1167,14106,14107,14110,14112,14115,14118,14121,14123,14126,14129],{"class":1169,"line":1238},[1167,14108,14109],{"class":2703},"  --limit-cpu",[1167,14111,11992],{"class":2703},[1167,14113,14114],{"class":2703}," --limit-memory",[1167,14116,14117],{"class":1183}," 512M",[1167,14119,14120],{"class":2703}," --update-parallelism",[1167,14122,11992],{"class":2703},[1167,14124,14125],{"class":2703}," --update-delay",[1167,14127,14128],{"class":1183}," 10s",[1167,14130,6546],{"class":2703},[1167,14132,14133,14136,14139],{"class":1169,"line":1257},[1167,14134,14135],{"class":2703},"  --constraint",[1167,14137,14138],{"class":1183}," node.labels.zone==cn-east-a",[1167,14140,6546],{"class":2703},[1167,14142,14143],{"class":1169,"line":1274},[1167,14144,14145],{"class":1183},"  ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0ea7b2d4\n",[1167,14147,14148],{"class":1169,"line":1290},[1167,14149,9717],{"emptyLinePlaceholder":9716},[1167,14151,14152,14154,14156,14158,14161],{"class":1169,"line":1304},[1167,14153,559],{"class":1173},[1167,14155,14071],{"class":1183},[1167,14157,9077],{"class":1183},[1167,14159,14160],{"class":1183}," shop-api=",[1167,14162,14163],{"class":2703},"6\n",[1167,14165,14166,14168,14170,14172,14175,14178],{"class":1169,"line":1310},[1167,14167,559],{"class":1173},[1167,14169,14071],{"class":1183},[1167,14171,14014],{"class":1183},[1167,14173,14174],{"class":2703}," --image",[1167,14176,14177],{"class":1183}," ghcr.io\u002Fexample\u002Fshop-api:1.9.0",[1167,14179,11762],{"class":1183},[1167,14181,14182,14184,14186,14188,14190,14193],{"class":1169,"line":3259},[1167,14183,559],{"class":1173},[1167,14185,14071],{"class":1183},[1167,14187,4819],{"class":1183},[1167,14189,4461],{"class":1183},[1167,14191,14192],{"class":2703}," --no-trunc",[1167,14194,14195],{"class":3604},"           # 每个副本在哪台、为什么失败\n",[1167,14197,14198,14200,14202,14204,14206],{"class":1169,"line":3264},[1167,14199,559],{"class":1173},[1167,14201,14071],{"class":1183},[1167,14203,4444],{"class":1183},[1167,14205,2719],{"class":2703},[1167,14207,11762],{"class":1183},[1167,14209,14210,14212,14214,14216,14218],{"class":1169,"line":3269},[1167,14211,559],{"class":1173},[1167,14213,14071],{"class":1183},[1167,14215,3370],{"class":1183},[1167,14217,4461],{"class":1183},[1167,14219,14220],{"class":2703}," --pretty\n",[15,14222,1718,14223,14226,14227,793,14230,793,14233,14236,14237,1372,14240,14243,14244,793,14247,14250],{},[19,14224,14225],{},"service"," 而不是容器——你要的是「期望 3 个副本」，不是「起 3 个容器」；滚动更新由 ",[19,14228,14229],{},"--update-parallelism",[19,14231,14232],{},"--update-delay",[19,14234,14235],{},"--update-failure-action rollback"," 三个参数决定；",[19,14238,14239],{},"--constraint",[19,14241,14242],{},"node.labels.*","）做放置约束，",[19,14245,14246],{},"--limit-cpu",[19,14248,14249],{},"--limit-memory"," 让调度器算得准。",[15,14252,14253,14254,14256,14257,14259],{},"overlay 网络的 ",[19,14255,5703],{}," 是让单机 ",[19,14258,555],{}," 也能加入这个网络（不加则只有 service 能进），排障时非常需要。stack 则是「把 Compose 文件部署成 Swarm 服务」：",[1158,14261,14263],{"className":1160,"code":14262,"language":1162,"meta":1163,"style":1163},"docker stack deploy -c compose.yaml -c compose.prod.yaml shop\ndocker stack services shop\ndocker stack rm shop\n",[19,14264,14265,14286,14297],{"__ignoreMap":1163},[1167,14266,14267,14269,14272,14275,14277,14279,14281,14283],{"class":1169,"line":1170},[1167,14268,559],{"class":1173},[1167,14270,14271],{"class":1183}," stack",[1167,14273,14274],{"class":1183}," deploy",[1167,14276,5806],{"class":2703},[1167,14278,9444],{"class":1183},[1167,14280,5806],{"class":2703},[1167,14282,9449],{"class":1183},[1167,14284,14285],{"class":1183}," shop\n",[1167,14287,14288,14290,14292,14295],{"class":1169,"line":1177},[1167,14289,559],{"class":1173},[1167,14291,14271],{"class":1183},[1167,14293,14294],{"class":1183}," services",[1167,14296,14285],{"class":1183},[1167,14298,14299,14301,14303,14305],{"class":1169,"line":1202},[1167,14300,559],{"class":1173},[1167,14302,14271],{"class":1183},[1167,14304,1196],{"class":1183},[1167,14306,14285],{"class":1183},[15,14308,14309,14312,14313,14317,14318,14320,14321,793,14323,793,14326,14328,14329,14331],{},[19,14310,14311],{},"stack deploy"," 只支持 Compose 规范的一个子集：",[31,14314,14315,2099],{},[19,14316,8102],{},"（镜像必须先在 registry）、",[19,14319,8140],{}," 只能用目标模式、",[19,14322,8319],{},[19,14324,14325],{},"volumes_from",[19,14327,8309],{}," 不支持、",[19,14330,8228],{}," 被忽略（服务是无顺序的集合）。这几条限制解释了为什么生产上「Compose 文件不等于 Swarm 文件」，需要单独维护。",[40,14333,14335],{"id":14334},"kubernetes-与-docker-的关系dockershim-为什么被移除","Kubernetes 与 Docker 的关系：dockershim 为什么被移除",[15,14337,14338,14339,14342],{},"时间线：K8s 需要一个能「按 CRI 接口管理容器生命周期」的运行时。Docker Engine 不实现 CRI，于是社区写了 ",[31,14340,14341],{},"dockershim","（kubelet 里的翻译层，把 CRI 调用转成 Docker API）。因为维护成本高、行为与 Docker 强耦合，K8s 1.24 起把它从 kubelet 移除。",[15,14344,14345],{},"三个常被误解的事实：",[1318,14347,14348,14360,14370],{},[584,14349,14350,14353,14354,793,14356,14359],{},[31,14351,14352],{},"不是「K8s 不支持 Docker 了」","。Docker 构建的镜像是标准 OCI 镜像，K8s 照样跑；",[19,14355,1550],{},[19,14357,14358],{},"docker compose","（本地开发）继续可用。",[584,14361,14362,14363,14365,14366,14369],{},"K8s 用的是 ",[31,14364,571],{},"（正是第一章架构图里 Docker 下面的那一层，第二章）或 ",[31,14367,14368],{},"CRI-O","——Docker 自己也是通过 containerd 跑容器，移除的只是最上面那层 shim。",[584,14371,14372,14373,14375,14376,14378,14379,14382,14383,14386,14387,793,14390,14393,14394,14397,14398,14401],{},"生态定位：",[31,14374,571],{}," 是库\u002F守护进程（被 Docker、K8s、Singularity 复用）；",[31,14377,14368],{}," 是专为 K8s 实现的轻量 CRI 运行时；",[31,14380,14381],{},"Podman"," 是无 daemon、可 rootless、CLI 与 Docker 兼容的替代（",[19,14384,14385],{},"alias docker=podman"," 通常就能用，",[19,14388,14389],{},"podman compose",[19,14391,14392],{},"podman play kube"," 走 Compose\u002FK8s 文件）；",[31,14395,14396],{},"nerdctl"," 是 containerd 的 Docker 兼容 CLI，常用于 ",[19,14399,14400],{},"buildx"," 风格的本地构建。",[15,14403,14404],{},"概念对照表（迁移时最有用的东西）：",[45,14406,14407,14419],{},[48,14408,14409],{},[51,14410,14411,14414,14417],{},[54,14412,14413],{},"Compose",[54,14415,14416],{},"Kubernetes",[54,14418,6369],{},[67,14420,14421,14434,14451,14465,14478,14488,14502,14520,14534,14546,14559,14570],{},[51,14422,14423,14428,14431],{},[72,14424,14425,14427],{},[19,14426,7931],{}," 的一个服务",[72,14429,14430],{},"Deployment（+ ReplicaSet + Pod）",[72,14432,14433],{},"K8s 的最小单元是 Pod（可多容器共享 net\u002FIPC，对应「sidecar」）",[51,14435,14436,14442,14445],{},[72,14437,14438,728,14440],{},[19,14439,8140],{},[19,14441,2478],{},[72,14443,14444],{},"Service（ClusterIP\u002FNodePort\u002FLoadBalancer）+ Ingress\u002FGateway",[72,14446,14447,14448],{},"名字即 DNS，跨命名空间用 ",[19,14449,14450],{},"svc.ns.svc.cluster.local",[51,14452,14453,14457,14460],{},[72,14454,14455],{},[19,14456,7959],{},[72,14458,14459],{},"Namespace + NetworkPolicy",[72,14461,14462,14463,1075],{},"默认全通，靠 NetworkPolicy 收紧（对应第十二章的 ",[19,14464,10039],{},[51,14466,14467,14472,14475],{},[72,14468,14469,14471],{},[19,14470,7985],{}," 命名卷",[72,14473,14474],{},"PV \u002F PVC + StorageClass",[72,14476,14477],{},"「谁拥有存储」变成集群对象，跨节点可挂",[51,14479,14480,14482,14485],{},[72,14481,6214],{},[72,14483,14484],{},"hostPath（几乎不该用）",[72,14486,14487],{},"需要它通常说明架构有问题",[51,14489,14490,14496,14499],{},[72,14491,14492,888,14494],{},[19,14493,8228],{},[19,14495,8244],{},[72,14497,14498],{},"initContainer + readiness\u002Fliveness\u002Fstartup probe",[72,14500,14501],{},"readiness 决定「进不进 Service 端点」，比 unhealthy 不重启前进一大步",[51,14503,14504,14511,14517],{},[72,14505,14506,728,14509],{},[19,14507,14508],{},"scale",[19,14510,8331],{},[72,14512,14513,14516],{},[19,14514,14515],{},"spec.replicas"," + HPA",[72,14518,14519],{},"自动扩缩是控制器，不是命令",[51,14521,14522,14528,14531],{},[72,14523,14524,728,14526],{},[19,14525,8171],{},[19,14527,8194],{},[72,14529,14530],{},"ConfigMap + Secret（envFrom）",[72,14532,14533],{},"Secret 默认只是 base64，不是加密；要信封加密或外部密钥系统",[51,14535,14536,14540,14543],{},[72,14537,14538],{},[19,14539,8004],{},[72,14541,14542],{},"ConfigMap 挂载",[72,14544,14545],{},"对应「只读挂配置目录」（第十三章）",[51,14547,14548,14553,14556],{},[72,14549,14550],{},[19,14551,14552],{},"restart: unless-stopped",[72,14554,14555],{},"由 kubelet\u002F控制器保证，无需声明",[72,14557,14558],{},"Pod 挂了自动重建，这是编排层最大差异",[51,14560,14561,14565,14568],{},[72,14562,14563],{},[19,14564,8339],{},[72,14566,14567],{},"无直接对应（用不同清单\u002FHelm values）",[72,14569],{},[51,14571,14572,14577,14582],{},[72,14573,14574],{},[19,14575,14576],{},"docker compose up",[72,14578,14579],{},[19,14580,14581],{},"kubectl apply -f",[72,14583,14584],{},"声明式对齐这一思想一致",[40,14586,14587],{"id":14587},"什么时候真的需要编排",[15,14589,14590],{},"需要：多台机器且要自动分布与 failover、副本数会随负载变化、多团队共享集群、需要滚动发布与自动回滚、需要 RBAC 与审计、有状态服务要跨节点存储。",[15,14592,14593,14594,14597,14598,14601],{},"不需要：单机或两三台、服务数量个位数、变更频率低、团队没有 K8s 运维经验（这条最重要——",[31,14595,14596],{},"K8s 的运维成本会吃掉它的收益","）。中间态「Swarm 或 Compose + 一个反代 + 外部健康检查」常常是最优解。工程上的正确姿势是：",[31,14599,14600],{},"先把应用做成容器友好的","（第十三章无状态、配置注入、优雅停机、健康检查），这时上不上编排都是可逆决定；反过来，用一个「什么都做不了」的有状态容器去换 K8s，只会把问题从部署搬到运维。",[10,14603,14605],{"id":14604},"十八排障与速查报错原文原因与命令","十八、排障与速查：报错原文、原因与命令",[40,14607,14609],{"id":14608},"报错-原因-怎么办","报错 → 原因 → 怎么办",[45,14611,14612,14625],{},[48,14613,14614],{},[51,14615,14616,14619,14622],{},[54,14617,14618],{},"报错原文（片段）",[54,14620,14621],{},"真实原因",[54,14623,14624],{},"怎么办",[67,14626,14627,14649,14672,14695,14716,14733,14766,14792,14805,14826,14851,14869,14896,14919,14946,14964,14989],{},[51,14628,14629,14634,14637],{},[72,14630,14631],{},[19,14632,14633],{},"Cannot connect to the Docker daemon at unix:\u002F\u002F\u002Fvar\u002Frun\u002Fdocker.sock. Is the docker daemon running?",[72,14635,14636],{},"daemon 没起 \u002F socket 路径不对 \u002F 远程上下文指错",[72,14638,14639,716,14642,716,14645,14648],{},[19,14640,14641],{},"systemctl status docker",[19,14643,14644],{},"docker context ls",[19,14646,14647],{},"ls -l \u002Fvar\u002Frun\u002Fdocker.sock","；Desktop 用户先看图标是否在跑",[51,14650,14651,14655,14658],{},[72,14652,14653],{},[19,14654,945],{},[72,14656,14657],{},"当前用户不在 docker 组，或加了组没重新登录",[72,14659,14660,716,14663,13008,14666,14669,14670],{},[19,14661,14662],{},"getent group docker",[19,14664,14665],{},"usermod -aG docker $USER",[31,14667,14668],{},"重登","（第二章），或 ",[19,14671,913],{},[51,14673,14674,14678,14681],{},[72,14675,14676],{},[19,14677,13387],{},[72,14679,14680],{},"宿主磁盘满（多半是容器日志或镜像层），或 tmpfs\u002Fshm 小",[72,14682,14683,888,14686,14688,14689,14691,14692,14694],{},[19,14684,14685],{},"df -h",[19,14687,1419],{},"；先清 ",[19,14690,2971],{},"、日志（第十六章）、旧镜像；",[19,14693,8131],{}," 单独调",[51,14696,14697,14701,14704],{},[72,14698,14699],{},[19,14700,5880],{},[72,14702,14703],{},"宿主端口被别的容器\u002F进程占（第八章四步）",[72,14705,14706,14709,14710,14712,14713,14715],{},[19,14707,14708],{},"docker ps --format"," 找占用者；",[19,14711,5887],{}," 找宿主进程；僵尸 ",[19,14714,5773],{}," 靠重启 docker",[51,14717,14718,14722,14725],{},[72,14719,14720],{},[19,14721,463],{},[72,14723,14724],{},"镜像架构与宿主不符（amd64 镜像跑在 ARM）",[72,14726,14727,14729,14730,14732],{},[19,14728,3461],{},"；用 ",[19,14731,470],{}," 拉对应架构或构建多架构（第五章）",[51,14734,14735,14743,14752],{},[72,14736,14737,728,14740],{},[19,14738,14739],{},"Error: No such image: xxx:tag",[19,14741,14742],{},"image with reference ... was not found",[72,14744,14745,14746,14749,14750],{},"构建失败没产出 tag \u002F 名字或 tag 拼错 \u002F 用了 ",[19,14747,14748],{},"--output type=oci"," 没 ",[19,14751,2844],{},[72,14753,14754,14756,14757,12597,14759,14761,14762,2881,14764],{},[19,14755,1578],{}," 核对；",[19,14758,1550],{},[19,14760,2860],{}," 看是否真的成功；buildx 记得 ",[19,14763,2844],{},[19,14765,2841],{},[51,14767,14768,14776,14779],{},[72,14769,14770,728,14773],{},[19,14771,14772],{},"pull access denied for xxx",[19,14774,14775],{},"repository does not exist or may require 'docker login'",[72,14777,14778],{},"私有仓库未登录 \u002F 名字少了 registry 前缀 \u002F 镜像确实不存在",[72,14780,14781,14784,14785,114,14788,14791],{},[19,14782,14783],{},"docker login \u003Cregistry>","；补全 ",[19,14786,14787],{},"ghcr.io\u002F",[19,14789,14790],{},"localhost:5000\u002F"," 前缀（第十章）；确认命名空间大小写",[51,14793,14794,14799,14802],{},[72,14795,14796],{},[19,14797,14798],{},"toomanyrequests: You have reached your pull rate limit",[72,14800,14801],{},"Docker Hub 匿名限额（第十章）",[72,14803,14804],{},"CI 登录、把基础镜像同步到自建 registry、加缓存",[51,14806,14807,14813,14816],{},[72,14808,14809,14812],{},[19,14810,14811],{},"error during connect: ... open \u002F\u002F.\u002Fpipe\u002Fdocker_engine: The system cannot find the file specified","（Windows\u002FmacOS）",[72,14814,14815],{},"Docker Desktop 的 VM 没起来 \u002F 命名管道丢失 \u002F WSL2 未启用",[72,14817,14818,14819,716,14822,14825],{},"启动 Desktop 等到图标稳定；",[19,14820,14821],{},"docker context use desktop-linux",[19,14823,14824],{},"wsl --update","；确认 BIOS 虚拟化",[51,14827,14828,14833,14840],{},[72,14829,14830],{},[19,14831,14832],{},"Error response from daemon: container ... is not running",[72,14834,14835,14836,793,14838],{},"对已停止容器执行 ",[19,14837,3734],{},[19,14839,4544],{},[72,14841,14842,14843,14845,14846,564,14848,14850],{},"先 ",[19,14844,869],{}," 看状态 → ",[19,14847,4296],{},[19,14849,4164],{}," 手工跑（第七章三步法）",[51,14852,14853,14858,14861],{},[72,14854,14855],{},[19,14856,14857],{},"OCI runtime create failed: ... apply cgroup \u002Fsys\u002Ffs\u002Fcgroup\u002F...",[72,14859,14860],{},"cgroup 驱动冲突（systemd vs cgroupfs）、cgroup v1\u002Fv2 不匹配、或宿主 systemd 版本与 docker 不兼容",[72,14862,14863,4359,14865,14868],{},[19,14864,677],{},[19,14866,14867],{},"Cgroup Driver\u002FVersion","；统一驱动；宿主重启后残留 cgroup 未清理时重启 docker",[51,14870,14871,14876,14879],{},[72,14872,14873],{},[19,14874,14875],{},"docker: 'compose' is not a docker command",[72,14877,14878],{},"装的是 v1 独立二进制，或 CLI 插件目录缺失",[72,14880,14881,14884,14885,14888,14889,14892,14893],{},[19,14882,14883],{},"docker compose version"," 失败则装 ",[19,14886,14887],{},"docker-compose-plugin","；旧环境用 ",[19,14890,14891],{},"docker-compose","（带横线）；Linux 检查 ",[19,14894,14895],{},"~\u002F.docker\u002Fcli-plugins\u002F",[51,14897,14898,14903,14909],{},[72,14899,14900],{},[19,14901,14902],{},"network ... not found",[72,14904,14905,14906,14908],{},"网络被 ",[19,14907,8825],{}," 删了但容器还在引用，或用了默认网络名",[72,14910,14911,14914,14915,14918],{},[19,14912,14913],{},"docker network ls","；重建网络并 ",[19,14916,14917],{},"network connect","；Compose 项目名变了（第十一章）",[51,14920,14921,14926,14932],{},[72,14922,14923],{},[19,14924,14925],{},"failed to start daemon: error initializing network allocator: invalid IPv4 network",[72,14927,14928,14931],{},[19,14929,14930],{},"default-address-pools"," 与宿主网段冲突",[72,14933,14934,14936,14937,793,14940,14942,14943],{},[19,14935,7571],{}," 调整 ",[19,14938,14939],{},"bip",[19,14941,14930],{}," 避开 ",[19,14944,14945],{},"192.168.x",[51,14947,14948,14953,14956],{},[72,14949,14950],{},[19,14951,14952],{},"error while mounting volume: permission denied",[72,14954,14955],{},"bind mount 源目录 SELinux\u002FAppArmor 拦或属主不符（第九章）",[72,14957,14958,14959,793,14961,14963],{},"加 ",[19,14960,12600],{},[19,14962,12603],{},"（SELinux）；对齐 UID；rootless 检查 subuid",[51,14965,14966,14974,14977],{},[72,14967,14968,14969,939,14971,14973],{},"容器起来就 ",[19,14970,5132],{},[19,14972,4296],{}," 空白",[72,14975,14976],{},"启动即失败且错误写进了文件而非 stdout；或 CMD 不是前台进程",[72,14978,14979,564,14982,14984,14985,14988],{},[19,14980,14981],{},"logs --tail 50",[19,14983,4164],{}," 手工跑 → ",[19,14986,14987],{},"docker inspect .Path .Args"," 核对命令（第七、十三章）",[51,14990,14991,14995,15000],{},[72,14992,14968,14993],{},[19,14994,398],{},[72,14996,4349,14997,14999],{},[19,14998,4352],{},"、或 stop 超时（第十四章）",[72,15001,15002,4359,15004,15006,15007,15009],{},[19,15003,4358],{},[19,15005,4362],{},"；调 ",[19,15008,4072],{},"；看是否 swap 语义写错",[40,15011,15013],{"id":15012},"dockerfile-指令速查表","Dockerfile 指令速查表",[45,15015,15016,15031],{},[48,15017,15018],{},[51,15019,15020,15022,15025,15028],{},[54,15021,2196],{},[54,15023,15024],{},"一句话语义",[54,15026,15027],{},"进数据层？",[54,15029,15030],{},"最常犯的错",[67,15032,15033,15053,15071,15086,15104,15118,15139,15158,15172,15186,15200,15213,15231,15245,15264,15278],{},[51,15034,15035,15039,15042,15044],{},[72,15036,15037],{},[19,15038,1728],{},[72,15040,15041],{},"起点\u002F阶段",[72,15043,4201],{},[72,15045,3476,15046,15048,15049,15052],{},[19,15047,7125],{},"；忘写 ",[19,15050,15051],{},"AS"," 名字",[51,15054,15055,15059,15062,15065],{},[72,15056,15057],{},[19,15058,2214],{},[72,15060,15061],{},"构建期变量",[72,15063,15064],{},"否",[72,15066,15067,15068,15070],{},"当密钥用；",[19,15069,1728],{}," 后忘记重新声明",[51,15072,15073,15077,15080,15083],{},[72,15074,15075],{},[19,15076,1380],{},[72,15078,15079],{},"构建+运行期变量",[72,15081,15082],{},"元数据",[72,15084,15085],{},"当密钥用；放在第一层导致全失效",[51,15087,15088,15092,15095,15098],{},[72,15089,15090],{},[19,15091,1768],{},[72,15093,15094],{},"复制上下文文件",[72,15096,15097],{},"是",[72,15099,15100,15101,15103],{},"不放 ",[19,15102,1881],{},"；写太靠前击穿缓存",[51,15105,15106,15110,15113,15115],{},[72,15107,15108],{},[19,15109,1773],{},[72,15111,15112],{},"复制 + 自动解压 tar + 拉 URL",[72,15114,15097],{},[72,15116,15117],{},"用它下载（无法清缓存、不校验）",[51,15119,15120,15124,15127,15129],{},[72,15121,15122],{},[19,15123,1924],{},[72,15125,15126],{},"执行命令生成一层",[72,15128,15097],{},[72,15130,15131,15132,15135,15136,15138],{},"多条 ",[19,15133,15134],{},"apt-get"," 分开写；",[19,15137,6680],{}," 前一层的东西",[51,15140,15141,15145,15148,15150],{},[72,15142,15143],{},[19,15144,1386],{},[72,15146,15147],{},"设置后续相对路径",[72,15149,15082],{},[72,15151,3476,15152,15154,15155,15157],{},[19,15153,2393],{}," 代替；用相对\u002F",[19,15156,2403],{}," 路径",[51,15159,15160,15164,15167,15169],{},[72,15161,15162],{},[19,15163,1389],{},[72,15165,15166],{},"切换身份",[72,15168,15082],{},[72,15170,15171],{},"放太靠前导致后续装包失败；写用户名不写 UID",[51,15173,15174,15178,15181,15183],{},[72,15175,15176],{},[19,15177,1395],{},[72,15179,15180],{},"声明挂载点",[72,15182,15082],{},[72,15184,15185],{},"以为它会保留数据；构建期写该目录内容被丢",[51,15187,15188,15192,15195,15197],{},[72,15189,15190],{},[19,15191,1383],{},[72,15193,15194],{},"文档化端口",[72,15196,15082],{},[72,15198,15199],{},"以为它真的开了端口",[51,15201,15202,15206,15208,15210],{},[72,15203,15204],{},[19,15205,1392],{},[72,15207,15082],{},[72,15209,15082],{},[72,15211,15212],{},"放动态值（commit 号）击穿缓存",[51,15214,15215,15219,15222,15224],{},[72,15216,15217],{},[19,15218,2502],{},[72,15220,15221],{},"自证可用",[72,15223,15082],{},[72,15225,15226,15227,15230],{},"不设 ",[19,15228,15229],{},"start-period","；检查打到 127.0.0.1 之外的地址",[51,15232,15233,15237,15240,15242],{},[72,15234,15235],{},[19,15236,847],{},[72,15238,15239],{},"默认参数\u002F命令",[72,15241,15082],{},[72,15243,15244],{},"用 shell 形式导致 PID 1 收不到信号（第十六章）",[51,15246,15247,15251,15254,15256],{},[72,15248,15249],{},[19,15250,1377],{},[72,15252,15253],{},"主命令",[72,15255,15082],{},[72,15257,8099,15258,15260,15261,15263],{},[19,15259,847],{}," 语义混用；",[19,15262,6993],{}," 传参被整条替换",[51,15265,15266,15270,15273,15275],{},[72,15267,15268],{},[19,15269,2519],{},[72,15271,15272],{},"改默认 shell",[72,15274,15082],{},[72,15276,15277],{},"切 bash 后镜像里没有 bash",[51,15279,15280,15284,15287,15289],{},[72,15281,15282],{},[19,15283,2946],{},[72,15285,15286],{},"选 BuildKit 前端",[72,15288,4201],{},[72,15290,15291],{},"没放在文件最前面",[15,15293,15294,15295,939,15297,15299,15300,793,15302,793,15304,793,15306,793,15308,15310],{},"顺序铁律：",[31,15296,2541],{},[19,15298,1483],{}," 尽量靠后、元数据指令（",[19,15301,1380],{},[19,15303,1383],{},[19,15305,847],{},[19,15307,1377],{},[19,15309,1389],{},"）放最后。",[40,15312,15314],{"id":15313},"命令速查六组","命令速查：六组",[1158,15316,15318],{"className":1160,"code":15317,"language":1162,"meta":1163,"style":1163},"docker build -t repo\u002Fapp:1.0.0 --pull --no-cache-filter deps .\ndocker buildx build --platform linux\u002Famd64,linux\u002Farm64 -t repo\u002Fapp:1.0.0 --push .\ndocker images; docker image inspect img; docker history --no-trunc img\ndocker pull repo\u002Fapp:1.0.0@sha256:...; docker tag a b; docker push b\ndocker save -o a.tar img; docker load -i a.tar\ndocker rmi img; docker image prune -a --filter until=336h\n",[19,15319,15320,15342,15362,15392,15422,15446],{"__ignoreMap":1163},[1167,15321,15322,15324,15326,15328,15331,15334,15337,15340],{"class":1169,"line":1170},[1167,15323,559],{"class":1173},[1167,15325,2700],{"class":1183},[1167,15327,2704],{"class":2703},[1167,15329,15330],{"class":1183}," repo\u002Fapp:1.0.0",[1167,15332,15333],{"class":2703}," --pull",[1167,15335,15336],{"class":2703}," --no-cache-filter",[1167,15338,15339],{"class":1183}," deps",[1167,15341,2710],{"class":1183},[1167,15343,15344,15346,15348,15350,15352,15354,15356,15358,15360],{"class":1169,"line":1177},[1167,15345,559],{"class":1173},[1167,15347,3343],{"class":1183},[1167,15349,2700],{"class":1183},[1167,15351,3406],{"class":2703},[1167,15353,3409],{"class":1183},[1167,15355,2704],{"class":2703},[1167,15357,15330],{"class":1183},[1167,15359,3416],{"class":2703},[1167,15361,2710],{"class":1183},[1167,15363,15364,15366,15369,15371,15373,15375,15377,15380,15382,15384,15387,15389],{"class":1169,"line":1202},[1167,15365,559],{"class":1173},[1167,15367,15368],{"class":1183}," images",[1167,15370,3629],{"class":1216},[1167,15372,559],{"class":1173},[1167,15374,7223],{"class":1183},[1167,15376,3370],{"class":1183},[1167,15378,15379],{"class":1183}," img",[1167,15381,3629],{"class":1216},[1167,15383,559],{"class":1173},[1167,15385,15386],{"class":1183}," history",[1167,15388,14192],{"class":2703},[1167,15390,15391],{"class":1183}," img\n",[1167,15393,15394,15396,15398,15401,15403,15405,15407,15410,15413,15415,15417,15419],{"class":1169,"line":1208},[1167,15395,559],{"class":1173},[1167,15397,7067],{"class":1183},[1167,15399,15400],{"class":1183}," repo\u002Fapp:1.0.0@sha256:...",[1167,15402,3629],{"class":1216},[1167,15404,559],{"class":1173},[1167,15406,7409],{"class":1183},[1167,15408,15409],{"class":1183}," a",[1167,15411,15412],{"class":1183}," b",[1167,15414,3629],{"class":1216},[1167,15416,559],{"class":1173},[1167,15418,7420],{"class":1183},[1167,15420,15421],{"class":1183}," b\n",[1167,15423,15424,15426,15428,15430,15433,15435,15437,15439,15441,15443],{"class":1169,"line":1238},[1167,15425,559],{"class":1173},[1167,15427,7764],{"class":1183},[1167,15429,7767],{"class":2703},[1167,15431,15432],{"class":1183}," a.tar",[1167,15434,15379],{"class":1183},[1167,15436,3629],{"class":1216},[1167,15438,559],{"class":1173},[1167,15440,7796],{"class":1183},[1167,15442,7799],{"class":2703},[1167,15444,15445],{"class":1183}," a.tar\n",[1167,15447,15448,15450,15453,15455,15457,15459,15461,15463,15465,15467],{"class":1169,"line":1257},[1167,15449,559],{"class":1173},[1167,15451,15452],{"class":1183}," rmi",[1167,15454,15379],{"class":1183},[1167,15456,3629],{"class":1216},[1167,15458,559],{"class":1173},[1167,15460,7223],{"class":1183},[1167,15462,3695],{"class":1183},[1167,15464,12191],{"class":2703},[1167,15466,4515],{"class":2703},[1167,15468,15469],{"class":1183}," until=336h\n",[1158,15471,15473],{"className":1160,"code":15472,"language":1162,"meta":1163,"style":1163},"docker run -d --name app --restart unless-stopped -p 127.0.0.1:8080:3000 \\\n  -e TZ=Asia\u002FShanghai --env-file .env.prod -v app-data:\u002Fsrv\u002Fapp\u002Fdata \\\n  --network shop-net --init --read-only --tmpfs \u002Ftmp \\\n  --memory 512m --memory-swap 512m --cpus 1.5 --pids-limit 256 \\\n  --user 1001:1001 --cap-drop ALL --security-opt no-new-privileges img:1.0.0\ndocker ps -a; docker start\u002Fstop\u002Frestart\u002Fkill\u002Fpause\u002Funpause\u002Frm [-f] app\ndocker container prune; docker container ls --filter ancestor=img:1.0.0\ndocker logs -f --since 10m --timestamps app; docker stats --no-stream\ndocker top app; docker diff app; docker events --since 1h --filter event=die\ndocker exec -it app sh; docker cp app:\u002Fpath .\u002F; docker inspect --format '{{.State.Status}}' app\ndocker run --rm -it --entrypoint sh img -c 'node server.js'\n",[19,15474,15475,15499,15519,15535,15558,15579,15597,15618,15645,15676,15715],{"__ignoreMap":1163},[1167,15476,15477,15479,15481,15483,15485,15488,15490,15492,15494,15497],{"class":1169,"line":1170},[1167,15478,559],{"class":1173},[1167,15480,3380],{"class":1183},[1167,15482,5611],{"class":2703},[1167,15484,3349],{"class":2703},[1167,15486,15487],{"class":1183}," app",[1167,15489,7381],{"class":2703},[1167,15491,7384],{"class":1183},[1167,15493,4695],{"class":2703},[1167,15495,15496],{"class":1183}," 127.0.0.1:8080:3000",[1167,15498,6546],{"class":2703},[1167,15500,15501,15504,15507,15509,15512,15514,15517],{"class":1169,"line":1177},[1167,15502,15503],{"class":2703},"  -e",[1167,15505,15506],{"class":1183}," TZ=Asia\u002FShanghai",[1167,15508,11128],{"class":2703},[1167,15510,15511],{"class":1183}," .env.prod",[1167,15513,6285],{"class":2703},[1167,15515,15516],{"class":1183}," app-data:\u002Fsrv\u002Fapp\u002Fdata",[1167,15518,6546],{"class":2703},[1167,15520,15521,15523,15525,15527,15529,15531,15533],{"class":1169,"line":1202},[1167,15522,14095],{"class":2703},[1167,15524,5555],{"class":1183},[1167,15526,13283],{"class":2703},[1167,15528,12641],{"class":2703},[1167,15530,12644],{"class":2703},[1167,15532,12647],{"class":1183},[1167,15534,6546],{"class":2703},[1167,15536,15537,15540,15543,15546,15548,15550,15552,15554,15556],{"class":1169,"line":1208},[1167,15538,15539],{"class":2703},"  --memory",[1167,15541,15542],{"class":1183}," 512m",[1167,15544,15545],{"class":2703}," --memory-swap",[1167,15547,15542],{"class":1183},[1167,15549,11738],{"class":2703},[1167,15551,11741],{"class":2703},[1167,15553,11756],{"class":2703},[1167,15555,11759],{"class":2703},[1167,15557,6546],{"class":2703},[1167,15559,15560,15563,15565,15568,15571,15573,15576],{"class":1169,"line":1238},[1167,15561,15562],{"class":2703},"  --user",[1167,15564,12658],{"class":1183},[1167,15566,15567],{"class":2703}," --cap-drop",[1167,15569,15570],{"class":1183}," ALL",[1167,15572,12486],{"class":2703},[1167,15574,15575],{"class":1183}," no-new-privileges",[1167,15577,15578],{"class":1183}," img:1.0.0\n",[1167,15580,15581,15583,15585,15587,15589,15591,15594],{"class":1169,"line":1257},[1167,15582,559],{"class":1173},[1167,15584,4819],{"class":1183},[1167,15586,12191],{"class":2703},[1167,15588,3629],{"class":1216},[1167,15590,559],{"class":1173},[1167,15592,15593],{"class":1183}," start\u002Fstop\u002Frestart\u002Fkill\u002Fpause\u002Funpause\u002Frm",[1167,15595,15596],{"class":1216}," [-f] app\n",[1167,15598,15599,15601,15603,15605,15607,15609,15611,15613,15615],{"class":1169,"line":1274},[1167,15600,559],{"class":1173},[1167,15602,3692],{"class":1183},[1167,15604,3695],{"class":1183},[1167,15606,3629],{"class":1216},[1167,15608,559],{"class":1173},[1167,15610,3692],{"class":1183},[1167,15612,4689],{"class":1183},[1167,15614,4515],{"class":2703},[1167,15616,15617],{"class":1183}," ancestor=img:1.0.0\n",[1167,15619,15620,15622,15624,15626,15628,15631,15634,15636,15638,15640,15642],{"class":1169,"line":1290},[1167,15621,559],{"class":1173},[1167,15623,4444],{"class":1183},[1167,15625,2719],{"class":2703},[1167,15627,4455],{"class":2703},[1167,15629,15630],{"class":1183}," 10m",[1167,15632,15633],{"class":2703}," --timestamps",[1167,15635,15487],{"class":1183},[1167,15637,3629],{"class":1216},[1167,15639,559],{"class":1173},[1167,15641,4471],{"class":1183},[1167,15643,15644],{"class":2703}," --no-stream\n",[1167,15646,15647,15649,15651,15653,15655,15657,15659,15661,15663,15665,15667,15669,15672,15674],{"class":1169,"line":1304},[1167,15648,559],{"class":1173},[1167,15650,4484],{"class":1183},[1167,15652,15487],{"class":1183},[1167,15654,3629],{"class":1216},[1167,15656,559],{"class":1173},[1167,15658,4496],{"class":1183},[1167,15660,15487],{"class":1183},[1167,15662,3629],{"class":1216},[1167,15664,559],{"class":1173},[1167,15666,4508],{"class":1183},[1167,15668,4455],{"class":2703},[1167,15670,15671],{"class":1183}," 1h",[1167,15673,4515],{"class":2703},[1167,15675,11938],{"class":1183},[1167,15677,15678,15680,15682,15684,15686,15688,15690,15692,15695,15698,15701,15703,15705,15707,15709,15712],{"class":1169,"line":1310},[1167,15679,559],{"class":1173},[1167,15681,4629],{"class":1183},[1167,15683,4632],{"class":2703},[1167,15685,15487],{"class":1183},[1167,15687,4637],{"class":1183},[1167,15689,3629],{"class":1216},[1167,15691,559],{"class":1173},[1167,15693,15694],{"class":1183}," cp",[1167,15696,15697],{"class":1183}," app:\u002Fpath",[1167,15699,15700],{"class":1183}," .\u002F",[1167,15702,3629],{"class":1216},[1167,15704,559],{"class":1173},[1167,15706,3370],{"class":1183},[1167,15708,4777],{"class":2703},[1167,15710,15711],{"class":1183}," '{{.State.Status}}'",[1167,15713,15714],{"class":1183}," app\n",[1167,15716,15717,15719,15721,15723,15725,15727,15729,15731,15733],{"class":1169,"line":3259},[1167,15718,559],{"class":1173},[1167,15720,3380],{"class":1183},[1167,15722,3386],{"class":2703},[1167,15724,4632],{"class":2703},[1167,15726,5157],{"class":2703},[1167,15728,4637],{"class":1183},[1167,15730,15379],{"class":1183},[1167,15732,5806],{"class":2703},[1167,15734,15735],{"class":1183}," 'node server.js'\n",[1158,15737,15739],{"className":1160,"code":15738,"language":1162,"meta":1163,"style":1163},"docker network create --driver bridge --subnet 172.28.0.0\u002F16 shop-net\ndocker network connect [--alias api] shop-net app; docker network disconnect shop-net app\ndocker network ls; docker network inspect shop-net; docker network rm shop-net\ndocker network prune\n",[19,15740,15741,15759,15790,15818],{"__ignoreMap":1163},[1167,15742,15743,15745,15747,15749,15751,15753,15755,15757],{"class":1169,"line":1170},[1167,15744,559],{"class":1173},[1167,15746,5521],{"class":1183},[1167,15748,3346],{"class":1183},[1167,15750,3355],{"class":2703},[1167,15752,5528],{"class":1183},[1167,15754,5531],{"class":2703},[1167,15756,5534],{"class":1183},[1167,15758,5543],{"class":1183},[1167,15760,15761,15763,15765,15767,15770,15773,15775,15777,15779,15781,15783,15786,15788],{"class":1169,"line":1177},[1167,15762,559],{"class":1173},[1167,15764,5521],{"class":1183},[1167,15766,5552],{"class":1183},[1167,15768,15769],{"class":1216}," [--alias ",[1167,15771,15772],{"class":1183},"api]",[1167,15774,5555],{"class":1183},[1167,15776,15487],{"class":1183},[1167,15778,3629],{"class":1216},[1167,15780,559],{"class":1173},[1167,15782,5521],{"class":1183},[1167,15784,15785],{"class":1183}," disconnect",[1167,15787,5555],{"class":1183},[1167,15789,15714],{"class":1183},[1167,15791,15792,15794,15796,15798,15800,15802,15804,15806,15808,15810,15812,15814,15816],{"class":1169,"line":1202},[1167,15793,559],{"class":1173},[1167,15795,5521],{"class":1183},[1167,15797,4689],{"class":1183},[1167,15799,3629],{"class":1216},[1167,15801,559],{"class":1173},[1167,15803,5521],{"class":1183},[1167,15805,3370],{"class":1183},[1167,15807,5555],{"class":1183},[1167,15809,3629],{"class":1216},[1167,15811,559],{"class":1173},[1167,15813,5521],{"class":1183},[1167,15815,1196],{"class":1183},[1167,15817,5543],{"class":1183},[1167,15819,15820,15822,15824],{"class":1169,"line":1208},[1167,15821,559],{"class":1173},[1167,15823,5521],{"class":1183},[1167,15825,15826],{"class":1183}," prune\n",[1158,15828,15830],{"className":1160,"code":15829,"language":1162,"meta":1163,"style":1163},"docker volume create --name db-data --opt type=nfs --opt o=addr=...,rw --opt device=:\u002Fsrv\u002Fnfs\ndocker volume ls; docker volume inspect db-data; docker volume rm db-data\ndocker run --rm --volumes-from shop-db -v \"$PWD:\u002Fbackup\" alpine tar czf \u002Fbackup\u002Fdb.tar.gz \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\ndocker run -d --mount type=volume,source=db-data,target=\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata postgres:16.4\ndocker run -d -v .\u002Fconf:\u002Fetc\u002Fapp:ro nginx:1.27-alpine          # bind mount\ndocker run -d --mount type=tmpfs,tmpfs-size=64m,target=\u002Frun app img:1.0.0\n",[19,15831,15832,15860,15889,15919,15934,15952],{"__ignoreMap":1163},[1167,15833,15834,15836,15838,15840,15842,15844,15847,15850,15852,15855,15857],{"class":1169,"line":1170},[1167,15835,559],{"class":1173},[1167,15837,6591],{"class":1183},[1167,15839,3346],{"class":1183},[1167,15841,3349],{"class":2703},[1167,15843,6604],{"class":1183},[1167,15845,15846],{"class":2703}," --opt",[1167,15848,15849],{"class":1183}," type=nfs",[1167,15851,15846],{"class":2703},[1167,15853,15854],{"class":1183}," o=addr=...,rw",[1167,15856,15846],{"class":2703},[1167,15858,15859],{"class":1183}," device=:\u002Fsrv\u002Fnfs\n",[1167,15861,15862,15864,15866,15868,15870,15872,15874,15876,15878,15880,15882,15884,15886],{"class":1169,"line":1177},[1167,15863,559],{"class":1173},[1167,15865,6591],{"class":1183},[1167,15867,4689],{"class":1183},[1167,15869,3629],{"class":1216},[1167,15871,559],{"class":1173},[1167,15873,6591],{"class":1183},[1167,15875,3370],{"class":1183},[1167,15877,6604],{"class":1183},[1167,15879,3629],{"class":1216},[1167,15881,559],{"class":1173},[1167,15883,6591],{"class":1183},[1167,15885,1196],{"class":1183},[1167,15887,15888],{"class":1183}," db-data\n",[1167,15890,15891,15893,15895,15897,15899,15901,15903,15905,15907,15909,15911,15913,15915,15917],{"class":1169,"line":1202},[1167,15892,559],{"class":1173},[1167,15894,3380],{"class":1183},[1167,15896,3386],{"class":2703},[1167,15898,6527],{"class":2703},[1167,15900,5664],{"class":1183},[1167,15902,6285],{"class":2703},[1167,15904,6534],{"class":1183},[1167,15906,6537],{"class":1216},[1167,15908,6540],{"class":1183},[1167,15910,6543],{"class":1183},[1167,15912,6714],{"class":1183},[1167,15914,6554],{"class":1183},[1167,15916,6719],{"class":1183},[1167,15918,13722],{"class":1183},[1167,15920,15921,15923,15925,15927,15929,15932],{"class":1169,"line":1208},[1167,15922,559],{"class":1173},[1167,15924,3380],{"class":1183},[1167,15926,5611],{"class":2703},[1167,15928,6306],{"class":2703},[1167,15930,15931],{"class":1183}," type=volume,source=db-data,target=\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata",[1167,15933,6291],{"class":1183},[1167,15935,15936,15938,15940,15942,15944,15947,15949],{"class":1169,"line":1238},[1167,15937,559],{"class":1173},[1167,15939,3380],{"class":1183},[1167,15941,5611],{"class":2703},[1167,15943,6285],{"class":2703},[1167,15945,15946],{"class":1183}," .\u002Fconf:\u002Fetc\u002Fapp:ro",[1167,15948,3601],{"class":1183},[1167,15950,15951],{"class":3604},"          # bind mount\n",[1167,15953,15954,15956,15958,15960,15962,15965,15967],{"class":1169,"line":1257},[1167,15955,559],{"class":1173},[1167,15957,3380],{"class":1183},[1167,15959,5611],{"class":2703},[1167,15961,6306],{"class":2703},[1167,15963,15964],{"class":1183}," type=tmpfs,tmpfs-size=64m,target=\u002Frun",[1167,15966,15487],{"class":1183},[1167,15968,15578],{"class":1183},[1158,15970,15972],{"className":1160,"code":15971,"language":1162,"meta":1163,"style":1163},"docker compose config                                      # 排错第一步\ndocker compose -f compose.yaml -f compose.prod.yaml up -d --build --force-recreate\ndocker compose ps; docker compose logs -f --tail 100 api\ndocker compose exec api sh; docker compose run --rm migrate node scripts\u002Fmigrate.js\ndocker compose up -d --scale api=3 --profile debug\ndocker compose stop; docker compose down            # 数据保留\ndocker compose down -v --remove-orphans             # -v 删卷，先想清楚\n",[19,15973,15974,15985,16008,16032,16062,16084,16103],{"__ignoreMap":1163},[1167,15975,15976,15978,15980,15982],{"class":1169,"line":1170},[1167,15977,559],{"class":1173},[1167,15979,7896],{"class":1183},[1167,15981,8954],{"class":1183},[1167,15983,15984],{"class":3604},"                                      # 排错第一步\n",[1167,15986,15987,15989,15991,15993,15995,15997,15999,16001,16003,16005],{"class":1169,"line":1177},[1167,15988,559],{"class":1173},[1167,15990,7896],{"class":1183},[1167,15992,2719],{"class":2703},[1167,15994,9444],{"class":1183},[1167,15996,2719],{"class":2703},[1167,15998,9449],{"class":1183},[1167,16000,7911],{"class":1183},[1167,16002,5611],{"class":2703},[1167,16004,8983],{"class":2703},[1167,16006,16007],{"class":2703}," --force-recreate\n",[1167,16009,16010,16012,16014,16016,16018,16020,16022,16024,16026,16028,16030],{"class":1169,"line":1202},[1167,16011,559],{"class":1173},[1167,16013,7896],{"class":1183},[1167,16015,4819],{"class":1183},[1167,16017,3629],{"class":1216},[1167,16019,559],{"class":1173},[1167,16021,7896],{"class":1183},[1167,16023,4444],{"class":1183},[1167,16025,2719],{"class":2703},[1167,16027,4449],{"class":2703},[1167,16029,4452],{"class":2703},[1167,16031,9032],{"class":1183},[1167,16033,16034,16036,16038,16040,16042,16044,16046,16048,16050,16052,16054,16057,16059],{"class":1169,"line":1208},[1167,16035,559],{"class":1173},[1167,16037,7896],{"class":1183},[1167,16039,4629],{"class":1183},[1167,16041,5626],{"class":1183},[1167,16043,4637],{"class":1183},[1167,16045,3629],{"class":1216},[1167,16047,559],{"class":1173},[1167,16049,7896],{"class":1183},[1167,16051,3380],{"class":1183},[1167,16053,3386],{"class":2703},[1167,16055,16056],{"class":1183}," migrate",[1167,16058,9062],{"class":1183},[1167,16060,16061],{"class":1183}," scripts\u002Fmigrate.js\n",[1167,16063,16064,16066,16068,16070,16072,16075,16077,16079,16081],{"class":1169,"line":1238},[1167,16065,559],{"class":1173},[1167,16067,7896],{"class":1183},[1167,16069,7911],{"class":1183},[1167,16071,5611],{"class":2703},[1167,16073,16074],{"class":2703}," --scale",[1167,16076,9080],{"class":1183},[1167,16078,10951],{"class":2703},[1167,16080,9921],{"class":2703},[1167,16082,16083],{"class":1183}," debug\n",[1167,16085,16086,16088,16090,16092,16094,16096,16098,16100],{"class":1169,"line":1257},[1167,16087,559],{"class":1173},[1167,16089,7896],{"class":1183},[1167,16091,3646],{"class":1183},[1167,16093,3629],{"class":1216},[1167,16095,559],{"class":1173},[1167,16097,7896],{"class":1183},[1167,16099,9109],{"class":1183},[1167,16101,16102],{"class":3604},"            # 数据保留\n",[1167,16104,16105,16107,16109,16111,16113,16116],{"class":1169,"line":1274},[1167,16106,559],{"class":1173},[1167,16108,7896],{"class":1183},[1167,16110,9109],{"class":1183},[1167,16112,6285],{"class":2703},[1167,16114,16115],{"class":2703}," --remove-orphans",[1167,16117,16118],{"class":3604},"             # -v 删卷，先想清楚\n",[1158,16120,16122],{"className":1160,"code":16121,"language":1162,"meta":1163,"style":1163},"docker system df                # 动手清理前先看这个\ndocker builder prune; docker container prune; docker image prune\ndocker system prune                        # 相对安全：只清已停止\u002F悬空\u002F未用网络\ndocker system prune -a --volumes           # 高危：删所有未引用镜像与卷\ndocker login ghcr.io -U user --password-stdin; docker logout ghcr.io\ndocker manifest inspect img:1.0.0; docker buildx inspect --bootstrap\n",[19,16123,16124,16135,16159,16170,16185,16214],{"__ignoreMap":1163},[1167,16125,16126,16128,16130,16132],{"class":1169,"line":1170},[1167,16127,559],{"class":1173},[1167,16129,12152],{"class":1183},[1167,16131,12155],{"class":1183},[1167,16133,16134],{"class":3604},"                # 动手清理前先看这个\n",[1167,16136,16137,16139,16141,16143,16145,16147,16149,16151,16153,16155,16157],{"class":1169,"line":1177},[1167,16138,559],{"class":1173},[1167,16140,12201],{"class":1183},[1167,16142,3695],{"class":1183},[1167,16144,3629],{"class":1216},[1167,16146,559],{"class":1173},[1167,16148,3692],{"class":1183},[1167,16150,3695],{"class":1183},[1167,16152,3629],{"class":1216},[1167,16154,559],{"class":1173},[1167,16156,7223],{"class":1183},[1167,16158,15826],{"class":1183},[1167,16160,16161,16163,16165,16167],{"class":1169,"line":1202},[1167,16162,559],{"class":1173},[1167,16164,12152],{"class":1183},[1167,16166,3695],{"class":1183},[1167,16168,16169],{"class":3604},"                        # 相对安全：只清已停止\u002F悬空\u002F未用网络\n",[1167,16171,16172,16174,16176,16178,16180,16182],{"class":1169,"line":1208},[1167,16173,559],{"class":1173},[1167,16175,12152],{"class":1183},[1167,16177,3695],{"class":1183},[1167,16179,12191],{"class":2703},[1167,16181,12230],{"class":2703},[1167,16183,16184],{"class":3604},"           # 高危：删所有未引用镜像与卷\n",[1167,16186,16187,16189,16192,16195,16198,16201,16204,16206,16208,16211],{"class":1169,"line":1238},[1167,16188,559],{"class":1173},[1167,16190,16191],{"class":1183}," login",[1167,16193,16194],{"class":1183}," ghcr.io",[1167,16196,16197],{"class":2703}," -U",[1167,16199,16200],{"class":1183}," user",[1167,16202,16203],{"class":2703}," --password-stdin",[1167,16205,3629],{"class":1216},[1167,16207,559],{"class":1173},[1167,16209,16210],{"class":1183}," logout",[1167,16212,16213],{"class":1183}," ghcr.io\n",[1167,16215,16216,16218,16220,16222,16225,16227,16229,16231,16233],{"class":1169,"line":1257},[1167,16217,559],{"class":1173},[1167,16219,3425],{"class":1183},[1167,16221,3370],{"class":1183},[1167,16223,16224],{"class":1183}," img:1.0.0",[1167,16226,3629],{"class":1216},[1167,16228,559],{"class":1173},[1167,16230,3343],{"class":1183},[1167,16232,3370],{"class":1183},[1167,16234,3373],{"class":2703},[15,16236,16237],{},"示例 18 是全教程的收口演练：给出一批真实报错原文，每张卡片对应「先跑哪条命令、看到什么、结论是什么」，点按钮按顺序走完一次线上排障。",[1405,16239],{"height":16240,"src":16241,"title":16242},660,"\u002Ftutorials\u002Fdocker\u002Fdemos\u002F18-troubleshoot.html","示例 18：排障演练——从报错原文到结论",[15,16244,16245,16246,16248,16249,16251,16252,16254],{},"回望这十八章：第一到三章解释了「为什么容器能到处跑」，四到五章是造镜像的手艺，六到九章是让一个容器活着并把数据留住，十章解决镜像从哪来，十一到十三章把单容器扩展成一组服务，十四到十五章谈限制与加固，十六章把「能跑」变成「敢上生产」，十七到十八章给出更大规模的答案与出事时的路线。接下来的提升没有捷径：把 ",[19,16247,5166],{}," 这个例子自己敲一遍，然后用 ",[19,16250,1419],{}," 和 ",[19,16253,1593],{}," 去观察每一个决定留下的痕迹。",[16256,16257,16258],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":1163,"searchDepth":1177,"depth":1177,"links":16260},[16261,16267,16275,16281,16291,16301,16307,16317,16325,16334,16343,16351,16360,16367,16375,16383,16392,16398],{"id":12,"depth":1177,"text":13,"children":16262},[16263,16264,16265,16266],{"id":42,"depth":1202,"text":43},{"id":155,"depth":1202,"text":156},{"id":267,"depth":1202,"text":268},{"id":413,"depth":1202,"text":414},{"id":544,"depth":1177,"text":545,"children":16268},[16269,16270,16271,16272,16273,16274],{"id":548,"depth":1202,"text":549},{"id":640,"depth":1202,"text":641},{"id":799,"depth":1202,"text":800},{"id":898,"depth":1202,"text":899},{"id":976,"depth":1202,"text":977},{"id":1091,"depth":1202,"text":1092},{"id":1141,"depth":1177,"text":1142,"children":16276},[16277,16278,16279,16280],{"id":1152,"depth":1202,"text":1153},{"id":1354,"depth":1202,"text":1355},{"id":1423,"depth":1202,"text":1424},{"id":1501,"depth":1202,"text":1502},{"id":1678,"depth":1177,"text":1679,"children":16282},[16283,16284,16285,16286,16287,16288,16289,16290],{"id":1689,"depth":1202,"text":1690},{"id":1754,"depth":1202,"text":1755},{"id":1888,"depth":1202,"text":1889},{"id":1987,"depth":1202,"text":1988},{"id":2186,"depth":1202,"text":2187},{"id":2337,"depth":1202,"text":2338},{"id":2534,"depth":1202,"text":2535},{"id":2604,"depth":1202,"text":2605},{"id":2683,"depth":1177,"text":2684,"children":16292},[16293,16294,16296,16297,16298,16299,16300],{"id":2687,"depth":1202,"text":2688},{"id":2891,"depth":1202,"text":16295},"BuildKit 与 # syntax=",{"id":2984,"depth":1202,"text":2984},{"id":3141,"depth":1202,"text":3142},{"id":3200,"depth":1202,"text":3201},{"id":3330,"depth":1202,"text":3331},{"id":3468,"depth":1202,"text":3469},{"id":3568,"depth":1177,"text":3569,"children":16302},[16303,16304,16305,16306],{"id":3581,"depth":1202,"text":3582},{"id":3764,"depth":1202,"text":3765},{"id":4170,"depth":1202,"text":4171},{"id":4432,"depth":1202,"text":4432},{"id":4601,"depth":1177,"text":4602,"children":16308},[16309,16311,16312,16313,16314,16315,16316],{"id":4612,"depth":1202,"text":16310},"docker exec 与 -- 的坑",{"id":4750,"depth":1202,"text":4751},{"id":4866,"depth":1202,"text":4867},{"id":4898,"depth":1202,"text":4899},{"id":5125,"depth":1202,"text":5126},{"id":5221,"depth":1202,"text":5222},{"id":5263,"depth":1202,"text":5264},{"id":5328,"depth":1177,"text":5329,"children":16318},[16319,16320,16321,16322,16323,16324],{"id":5335,"depth":1202,"text":5335},{"id":5453,"depth":1202,"text":5454},{"id":5508,"depth":1202,"text":5509},{"id":5707,"depth":1202,"text":5708},{"id":5894,"depth":1202,"text":5895},{"id":6023,"depth":1202,"text":6023},{"id":6128,"depth":1177,"text":6129,"children":16326},[16327,16328,16330,16331,16332,16333],{"id":6132,"depth":1202,"text":6133},{"id":6256,"depth":1202,"text":16329},"-v 与 --mount 语法对照",{"id":6510,"depth":1202,"text":6511},{"id":6578,"depth":1202,"text":6579},{"id":6790,"depth":1202,"text":6791},{"id":6969,"depth":1202,"text":6970},{"id":7051,"depth":1177,"text":7052,"children":16335},[16336,16337,16338,16339,16340,16341,16342],{"id":7055,"depth":1202,"text":7055},{"id":7147,"depth":1202,"text":7148},{"id":7280,"depth":1202,"text":7281},{"id":7359,"depth":1202,"text":7360},{"id":7565,"depth":1202,"text":7565},{"id":7751,"depth":1202,"text":7752},{"id":7832,"depth":1202,"text":7833},{"id":7866,"depth":1177,"text":7867,"children":16344},[16345,16346,16347,16348,16349,16350],{"id":7870,"depth":1202,"text":7871},{"id":7917,"depth":1202,"text":7918},{"id":8071,"depth":1202,"text":8072},{"id":8348,"depth":1202,"text":8348},{"id":8836,"depth":1202,"text":8837},{"id":8939,"depth":1202,"text":8940},{"id":9230,"depth":1177,"text":9231,"children":16352},[16353,16354,16355,16356,16357,16358,16359],{"id":9234,"depth":1202,"text":9235},{"id":9427,"depth":1202,"text":9428},{"id":9620,"depth":1202,"text":9621},{"id":9833,"depth":1202,"text":9834},{"id":9957,"depth":1202,"text":9957},{"id":10070,"depth":1202,"text":10070},{"id":10254,"depth":1202,"text":10254},{"id":11086,"depth":1177,"text":11087,"children":16361},[16362,16363,16364,16365,16366],{"id":11090,"depth":1202,"text":11090},{"id":11259,"depth":1202,"text":11260},{"id":11365,"depth":1202,"text":11366},{"id":11466,"depth":1202,"text":11467},{"id":11536,"depth":1202,"text":11537},{"id":11598,"depth":1177,"text":11599,"children":16368},[16369,16371,16372,16373,16374],{"id":11602,"depth":1202,"text":16370},"内存：--memory 与 --memory-swap 的语义陷阱",{"id":11721,"depth":1202,"text":11722},{"id":11856,"depth":1202,"text":11857},{"id":11971,"depth":1202,"text":11972},{"id":12139,"depth":1202,"text":12140},{"id":12343,"depth":1177,"text":12344,"children":16376},[16377,16378,16379,16380,16381,16382],{"id":12347,"depth":1202,"text":12348},{"id":12369,"depth":1202,"text":12370},{"id":12464,"depth":1202,"text":12464},{"id":12625,"depth":1202,"text":12625},{"id":12680,"depth":1202,"text":12680},{"id":12862,"depth":1202,"text":12863},{"id":12978,"depth":1177,"text":12979,"children":16384},[16385,16387,16388,16389,16390,16391],{"id":12982,"depth":1202,"text":16386},"--restart 四种策略怎么选",{"id":13102,"depth":1202,"text":13103},{"id":13200,"depth":1202,"text":13201},{"id":13369,"depth":1202,"text":13370},{"id":13493,"depth":1202,"text":13494},{"id":13650,"depth":1202,"text":13650},{"id":13841,"depth":1177,"text":13842,"children":16393},[16394,16395,16396,16397],{"id":13845,"depth":1202,"text":13846},{"id":13956,"depth":1202,"text":13957},{"id":14334,"depth":1202,"text":14335},{"id":14587,"depth":1202,"text":14587},{"id":14604,"depth":1177,"text":14605,"children":16399},[16400,16401,16402],{"id":14608,"depth":1202,"text":14609},{"id":15012,"depth":1202,"text":15013},{"id":15313,"depth":1202,"text":15314},"\u002Fimg\u002Fdocker-cover.svg","2026-10-04","从容器原理到生产部署：镜像分层与 Dockerfile、网络与数据卷、Compose、资源限制、安全与编排，18 章循序渐进，配模拟终端的可交互示例。","md",{},"\u002Ftutorials\u002Fdocker",{"title":5,"description":16405},{"loc":16408},"tutorials\u002Fdocker","gbyOgVodsvJ8PBLqGkkM9haZ1qJS82E13nLwjIMgOKo",[16414,16416,16420,16424,16428,16432,16436,16439,16443,16447,16451,16455,16459,16463,16467,16471,16475,16479,16483,16487,16491,16495,16499,16503,16507,16511,16515,16519,16522,16526,16530,16534,16538,16542,16546,16550,16554,16558,16562,16566,16570,16574,16578,16582,16586,16590,16594,16598,16602,16605,16609,16613,16617,16621,16625,16629,16632,16636,16640,16644,16648,16652,16656,16659,16663,16667,16671,16675,16679,16683,16687,16690,16694,16698,16702,16706,16710,16714,16717,16721,16725,16729,16733,16737,16741,16745,16748,16752,16756,16760,16764,16768,16771,16775,16779,16783,16787,16791,16794,16798,16802,16806,16810,16814,16818,16821,16825,16829,16833,16837,16841,16845,16848,16852,16856,16860,16864,16867,16871,16875],{"id":16408,"title":5,"titles":16415,"content":16405,"level":1170},[],{"id":16417,"title":13,"titles":16418,"content":16419,"level":1177},"\u002Ftutorials\u002Fdocker#一容器到底解决了什么镜像容器与仓库三件套",[5],"先还原一个每天都在上演的场景。你在自己机器上跑通了服务，python app.py 一切正常；交给测试同学，报错 ModuleNotFoundError: No module named 'pandas'；测试环境装了 pandas，你本地是 1.1 而那边是 2.0，接口返回的日期格式对不上；终于版本对齐了，生产机是 ARM 架构，而依赖里有一个需要现场编译 C 扩展的包。 「在我机器上能跑」这句话之所以让人头疼，不是因为机器有坏有好，而是因为运行环境是一组隐式依赖的集合：操作系统版本、C 库（glibc 还是 musl）、解释器与依赖库的具体版本、时区、环境变量、字体、可用的系统调用、CPU 架构。传统部署方式里，这堆东西散落在各台机器上，靠文档和运维的记忆维持一致。容器要解决的正是这个：把「应用 + 它需要的一切」打成一个不可变的包，让隐式依赖变成显式内容。",{"id":16421,"title":43,"titles":16422,"content":16423,"level":1202},"\u002Ftutorials\u002Fdocker#镜像容器仓库三个名词先把边界划清",[5,13],"概念 它是什么 类比 关键性质 镜像 image 只读的文件系统快照 + 启动参数 安装光盘 \u002F class 不可变、分层、可按内容寻址（digest） 容器 container 镜像加上一个可写层，跑起来的进程 进程 \u002F new 出来的对象 可创建、可停止、可删除，数据默认随它消失 仓库 repository 存放某个应用的一组镜像（按 tag 区分） Maven 仓库 nginx、ghcr.io\u002Fexample\u002Fshop-api 都是一个仓库 注册中心 registry 提供仓库服务的服务器 私服 Nexus docker.io、ghcr.io、自建 registry:2 最容易混的是最后两个：仓库是一组 tag，注册中心是那个服务。而镜像与容器的关系，第三章讲分层时会具体化：镜像是只读的，容器启动时在这一叠只读层上面盖了一层自己的读写层。你往容器里写的任何文件都只在那一层里，docker rm 之后一并消失——这是新手最常踩的第一个坑（第九章）。",{"id":16425,"title":156,"titles":16426,"content":16427,"level":1202},"\u002Ftutorials\u002Fdocker#容器不是轻量虚拟机共享内核是全部差异的来源",[5,13],"很多人把容器理解成「小一点的虚拟机」，这个类比会让你后面所有判断都跑偏。 维度 虚拟机 容器 内核 每个 VM 自己一个完整内核 所有容器共享宿主内核 启动 几十秒（走完整 BIOS\u002Fsystemd） 几十到几百毫秒（只是起一个进程） 内存开销 预先分配整个 guest 内存 就是进程本身，用多少占多少 镜像大小 几 GB 几十 MB 到几百 MB 单机密度 几台到十几台 几十到几百个 隔离强度 硬件虚拟化，边界很硬 内核特性 + 能力裁剪，边界是「软」的 跨内核 Linux VM 可以跑在 Windows 宿主上 Linux 容器必须跑在 Linux 内核上 最后一行值得单独强调：容器不是把硬件虚拟化了一遍，它只是把一个进程的可见世界做了限制。所以 Windows 上跑 Linux 容器必须在 Linux 内核上面垫一层虚拟机——第二章讲 Docker Desktop 时这个事实会造成一堆「奇怪现象」（bind mount 慢、host 网络不生效）。",{"id":16429,"title":268,"titles":16430,"content":16431,"level":1202},"\u002Ftutorials\u002Fdocker#namespace-管看得见什么cgroup-管用得了多少",[5,13],"容器为什么能「像独立机器」？靠的是内核里两套机制，分工非常清楚。 namespace 负责可见性的隔离，让同一个进程看到的世界上少了一半东西： namespace 隔离了什么 你在容器里的直观感受 mnt 挂载点 根目录是镜像的文件系统，宿主的 \u002Fhome 看不见 pid 进程号 ps 里自己的应用是 PID 1，看不见宿主的 sshd net 网络栈 有自己的网卡、IP、路由表、iptables uts 主机名 hostname 返回容器名 ipc 信号量 \u002F 共享内存 只能和同组容器通信 user UID\u002FGID 映射 rootless 模式下容器 root 其实是宿主普通用户 cgroup（control group）负责资源的计量与限制：CPU 时间、内存上限、块设备 IO、进程数、网络带宽（通过 cgroup v2）。docker stats 看到的数字、--memory 的限制、第十四章那个 Exited (137)，全部来自 cgroup。 一句话记住分工：namespace 决定容器「不知道宿主存在」，cgroup 决定容器「不能把宿主吃穷」。也正因为隔离是靠内核特性而不是硬件，容器才又快又省，但也才需要第十五章那整套安全加固——默认的容器不是一个足够硬的安全边界。",{"id":16433,"title":414,"titles":16434,"content":16435,"level":1202},"\u002Ftutorials\u002Fdocker#一次构建到处运行的边界在哪里",[5,13],"这句宣传语是真的，但它有明确的适用范围。下面这些东西不在镜像里，因此换机器就可能变行为： 不受镜像控制 后果示例 正确做法 宿主内核版本与模块 老内核跑新镜像报 unknown mount option \u002F overlay 不支持 统一宿主基线，CI 里用目标内核测 CPU 架构 exec format error（第十八章） 多架构镜像（第五章）或固定 --platform 设备与驱动 GPU 容器缺少 nvidia 运行时 设备透传 + 对应 runtime 资源上限 同镜像在小机器上 OOM 被杀 --memory \u002F Compose 显式声明（第十四章） 文件系统属主 UID bind mount 进去写不进去 对齐 UID 或用卷（第九章） 外部地址与密钥 连不上数据库、密钥泄漏 环境变量与 secrets 注入（第十三章） 并发与连接池大小 单机调好的参数在 8 核机上打爆 DB 从 --cpus 推导，不写死 所以准确的说法是：镜像保证「应用及其用户空间依赖」一致，不保证「运行时环境」一致。剩下的差异要么由编排层注入，要么由你在配置里显式声明。理解了这条边界，后面十七章的所有「为什么要这样写」都有了落点。 下一章开始动手：装好它，跑通第一个容器，并看清那条命令背后到底发生了什么。",{"id":16437,"title":545,"titles":16438,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#二安装与第一次运行daemon客户端与虚拟机的本质",[5],{"id":16440,"title":549,"titles":16441,"content":16442,"level":1202},"\u002Ftutorials\u002Fdocker#daemon-与客户端docker-命令其实只是个-http-客户端",[5,545],"很多人以为 docker run 是本机执行了容器，其实不是。真实链路是：docker CLI 发 HTTP 请求 → \u002Fvar\u002Frun\u002Fdocker.sock → dockerd（常驻服务，负责接收请求、拉镜像、管网络与卷）→ containerd（真正的容器运行时管理层，镜像解包、任务生命周期归它）→ runc（一次性小工具，做完 namespace\u002Fcgroup 那套系统调用把容器进程生出来就退出，不留常驻进程）。 关键推论有三个： 客户端与 daemon 可以完全分离。设 DOCKER_HOST=tcp:\u002F\u002Fbuild-server:2375，你的 docker 命令就在操控远端机器。CI 里的 docker:24-dind 镜像就是这么用的。 daemon 挂了，容器不一定挂。已运行的容器由 runc 生成的进程继续跑；重启 dockerd 会重新接管它们（这也是为什么 docker restart dockerd 比想象中有用，但也可能造成短暂失联）。 能访问那个 socket 就等于宿主 root。\u002Fvar\u002Frun\u002Fdocker.sock 的权限、挂载它的风险，第十五章会专门算这笔账。 安装方式上，Linux 建议用发行版的官方仓库装 docker-ce（snap 版本会限制挂载路径，坑很多），装完 sudo systemctl enable --now docker，再用 systemctl status docker --no-pager 确认是 active (running)。",{"id":16444,"title":641,"titles":16445,"content":16446,"level":1202},"\u002Ftutorials\u002Fdocker#docker-version-与-docker-info先学会读字段",[5,545],"装完先跑这两条，它们是你后面所有排障的信息源。docker version 输出分 Client 和 Server 两段：ApiVersion 决定客户端能说什么话，Version 是引擎版本，GitCommit 是构建标识（提 issue 时要给），Os\u002FArch 是这台机器的目标平台——跨架构问题第一个看这里。如果只有 Client 段然后报错 Cannot connect to the Docker daemon，说明 daemon 没跑或你没权限，这是第十八章表格的第一行。 docker info 要重点读的字段： 字段 看它干什么 Server Version 引擎版本，决定可用特性 Storage Driver 几乎总是 overlay2；vfs 说明没加速层，又慢又占盘 Cgroup Driver \u002F Cgroup Version systemd 还是 cgroupfs、v1 还是 v2——与 k8s 混用时驱动必须一致，v1\u002Fv2 的内存与 swap 语义有差异（第十四章） Security Options seccomp、apparmor、rootless 都在这里体现 Docker Root Dir 镜像与卷的真实落盘位置，磁盘爆了对着它 du（第九章） Registry Mirrors、NCPU、Total Memory 加速器是否生效；宿主真实上限（--memory\u002F--cpus 是相对它而言）",{"id":16448,"title":800,"titles":16449,"content":16450,"level":1202},"\u002Ftutorials\u002Fdocker#docker-run-hello-world拆开看它做了六件事",[5,545],"跑一条 docker run hello-world，屏幕上那段 \"Hello from Docker!\" 背后是一条完整链路： 客户端把「跑 hello-world」的请求发给 daemon； daemon 本地查 docker.io\u002Flibrary\u002Fhello-world:latest 的镜像记录，没有，于是向 registry-1.docker.io 发认证与拉取； 按 manifest 找到匹配 linux\u002Famd64 的那一组层，逐层下载并校验 sha256； 把层解压到 \u002Fvar\u002Flib\u002Fdocker\u002Foverlay2\u002F，用联合挂载叠成一个只读根文件系统； 在上面盖一层容器可写层，配上 namespace\u002Fcgroup，让 runc 拉起镜像 CMD 指定的 \u002Fhello； 进程打印一段文本后 exit 0，容器进入 exited 状态——docker ps 看不见它，docker ps -a 能看见（第六章）。 这条链路解释了三个常见困惑：第一次跑慢是拉镜像（不是启动慢）；跑完就没了是应用正常退出（容器活着的时间 = 主进程活着的时间）；docker run 其实是 pull + create + start 三个动作的合称。",{"id":16452,"title":899,"titles":16453,"content":16454,"level":1202},"\u002Ftutorials\u002Fdocker#sudo-usermod-ag-docker-user为什么要重新登录",[5,545],"把用户加进 docker 组，是为了不用每条命令都 sudo：sudo usermod -aG docker $USER，然后 newgrp docker 或直接退出终端重新登录，用 id 确认 groups 里能看到 docker。 必须重新登录（或 newgrp）的原因是：组的成员关系是进程凭证的一部分，只在登录那一刻被读进进程。你改的是 \u002Fetc\u002Fgroup，而当前这个 shell 进程的凭证是登录时抄的一份快照，不重新登录它不会更新。新手在这里卡住的典型症状是明明执行过 usermod，docker ps 依旧报 permission denied while trying to connect to the Docker daemon socket。 -aG 的 a 不能省：usermod -G docker $USER（没有 a）会把用户从其他所有附加组里移除，这是能把机器权限改坏的经典事故。 还要清楚这件事的实质：加入 docker 组不是「降权」，而是把宿主 root 能力授给了你。能起容器就能挂载任意宿主目录，包括 \u002F。生产机器上谁该进这个组，要当成 sudoers 来审。",{"id":16456,"title":977,"titles":16457,"content":16458,"level":1202},"\u002Ftutorials\u002Fdocker#rootless-模式把-daemon-也变成普通用户",[5,545],"rootless 模式让 dockerd 自己跑在普通用户下（靠 newuidmap\u002Fnewgidmap 与 user namespace）。它换来的是「即使容器逃逸，攻击者拿到的也是普通用户」，代价是一串限制： 项目 rootful rootless 绑定 80\u002F443 特权端口 可以 不行（>1024，除非改 sysctl） --network=host \u002F macvlan 可以 受限或不可用 bind mount 宿主文件 直接对应 受 subuid 映射，属主看着是 100000+（第九章、第十五章） 数据目录 \u002Fvar\u002Flib\u002Fdocker ~\u002F.local\u002Fshare\u002Fdocker 开机自启 systemd 服务 需要用户级 lingering（loginctl enable-linger） 装法是官方脚本 rootlesskit + dockerd-rootless-setuptool.sh install，然后 systemctl --user 管理。对个人开发机和「只跑自己代码」的构建机很合适；给外部内容做隔离时它确实提高了一档门槛。",{"id":16460,"title":1092,"titles":16461,"content":16462,"level":1202},"\u002Ftutorials\u002Fdocker#docker-desktop-与那台看不见的虚拟机",[5,545],"macOS 和 Windows 上你装的是 Docker Desktop，它不是「原生支持容器」，而是在宿主上偷偷起了一台 Linux 虚拟机（macOS 用 Apple 虚拟化框架 \u002F HyperKit，Windows 用 WSL2 或 Hyper-V），daemon 和所有容器都跑在那台 VM 里。你敲的 docker 命令通过它暴露的 socket 转发过去。 由此产生的三个「为什么」必须提前知道，否则后面会一直被折磨： 为什么 bind mount 这么慢？ 容器在 Linux VM 里，而你的代码在 macOS\u002FWindows 文件系统上，每次读写都要跨一次虚拟机文件共享（virtiofs \u002F gRPC-FUSE \u002F WSL2 计划）。stat 密集型操作（npm install、大量小文件）慢一个数量级。解法：把代码放在命名卷里、或用 WSL2 内的文件系统（Windows 侧）、或直接在 VM 里跑构建。 为什么 --network=host 不生效？ 那个 host 是 VM 的 host，不是你 Mac 的。同理 127.0.0.1 在容器里也不指向你的浏览器（第八章）。 为什么内存把 Mac 吃满？ VM 会尽量占满分配给它的内存并不归还，需要在 Desktop 设置里给 Docker VM 分配上限（第八章的资源视角）。 Linux 上没有这层虚拟机，性能与语义都是「原生」的——这也是生产环境几乎清一色 Linux 容器的原因。 下一章来看镜像的真实结构：它是一叠只读层，理解这一点之后，Dockerfile 里所有「为什么要按这个顺序写」都会自动说得通。",{"id":16464,"title":1142,"titles":16465,"content":16466,"level":1177},"\u002Ftutorials\u002Fdocker#三镜像分层只读层联合挂载与容器可写层",[5],"镜像不是一个大文件，而是一叠只读层。这一章的目标是让你能从命令输出里亲眼看到这些层——看懂了层，第五章的缓存、第十四章的瘦身、第十五章的密钥泄漏才是「显然」的结论。",{"id":16468,"title":1153,"titles":16469,"content":16470,"level":1202},"\u002Ftutorials\u002Fdocker#一叠只读层-一个读写层",[5,1142],"启动容器时，Docker 用联合挂载（Linux 上默认 overlay2）把这叠只读层叠成一个根文件系统，再在最上面加一层属于这个容器的可写层： ┌────────────────────────────────┐\n│ 容器可写层（每个容器一份）     │ ← 所有写入都发生在这里，docker rm 即消失\n├────────────────────────────────┤\n│ CMD [\"node\", \"server.js\"]   0B │ 元数据层：不占空间，但参与缓存判定\n│ COPY . .                 1.8MB │\n│ RUN npm ci                212MB│\n│ WORKDIR \u002Fsrv\u002Fshop           0B │\n│ FROM node:22-bookworm-slim 78MB│\n└────────────────────────────────┘\n  下面全是只读层：可被多个镜像、多个容器共享 三个直接后果： 共享。跑 20 个基于同一镜像的容器，磁盘上只有一份镜像层，20 个可写层而已。这就是容器密度高、镜像「下载快」的物理原因（很多层早就在本地了，只拉缺的）。 写时复制（copy-up）。你要改下层的 app.conf，文件系统先把整份文件复制到可写层，你改的是副本；上层「遮住」下层。删文件同理，是在可写层放一个 whiteout 标记，下层那个字节还在——所以在 Dockerfile 里 RUN rm bigfile 不会让镜像变小，必须和产生它的指令合并在同一层（第十四章瘦身核心）。 容器可写层是易逝的。docker rm 就没了。数据库要活着必须走卷（第九章）。",{"id":16472,"title":1355,"titles":16473,"content":16474,"level":1202},"\u002Ftutorials\u002Fdocker#docker-history把层和-dockerfile-指令对上",[5,1142],"docker history ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --no-trunc 的输出里每条指令对应一层，SIZE 是该层新增的字节数。你会看到大量 SIZE 为 0 的层——它们是元数据层（CMD、ENTRYPOINT、ENV、EXPOSE、WORKDIR、USER、LABEL、VOLUME），只改配置不写文件，不占空间但仍然算一层、仍然参与缓存判定。这个区分在第四章讲指令顺序时是关键。 示例 03 把这几条真实输出摆在一起，可以逐字段点开看：层是从下往上叠加的，history 是从上往下打印的，方向感要建起来。 另外两个观察入口是 docker image inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --format '{{json .RootFS.Layers}}'（层的内容哈希数组，从上到下即从最底层到最上层）和 docker system df（给四行账：Images、Containers、Local Volumes、Build Cache，以及各自的「可回收」比例——第十四章讨论清理策略时，它就是你每次决策前该看的那张表）。",{"id":16476,"title":1424,"titles":16477,"content":16478,"level":1202},"\u002Ftutorials\u002Fdocker#改一行代码为什么要重建内容哈希决定失效点",[5,1142],"构建时每一层的 ID 由「父层 ID + 本条指令 + 本指令涉及的输入内容（对 COPY\u002FADD 是文件内容哈希）」算出来。因此： FROM node:22-bookworm-slim           # 层 1\nWORKDIR \u002Fsrv\u002Fshop                    # 层 2（元数据）\nCOPY package.json package-lock.json .\u002F  # 层 3：只依赖这两个文件\nRUN npm ci                           # 层 4：npm 只依赖上一份 package-lock\nCOPY . .                             # 层 5：整个源码目录 —— 改任何一行源码，从这层起重跑\nENV NODE_ENV=production              # 层 6（元数据）\nEXPOSE 3000                          # 层 7（元数据）\nCMD [\"node\", \"server.js\"]            # 层 8（元数据） 改一行源码 → 层 1–4 命中缓存，层 5 起重建。但如果你把 COPY . . 写在 COPY package.json 前面，或者干脆第一行就 COPY . .，那么改一行代码就要重新 npm ci（因为 npm ci 那层的父层哈希变了）——一个几秒的改动变成几分钟的构建。这一条顺序调整，是 Dockerfile 性能优化里性价比最高的一件事，没有之一。",{"id":16480,"title":1502,"titles":16481,"content":16482,"level":1202},"\u002Ftutorials\u002Fdocker#镜像就是一个-tar-的集合",[5,1142],"镜像在磁盘上是散的目录，但在网络上和 docker save 的结果里，它是一组 tar。执行 docker save -o shop-api-1.8.3.tar ghcr.io\u002Fexample\u002Fshop-api:1.8.3 && tar tf shop-api-1.8.3.tar | head -8，你会看到 blobs\u002Fsha256\u002F... 一堆层文件加一个 manifest.json、index.json、oci-layout。也就是说：「镜像」本质上是一份带元数据的文件系统 tar 包集合，OCI 规范只是把这格式标准化了。这带来两个实用能力： docker save \u002F docker load：离线分发。内网机器没外网、气隔环境、应急回滚包，都靠它（第十章细讲用法与风险）。 层可以被任何兼容 OCI 的工具消费：Podman、containerd、Kubernetes 直接用 docker build 出来的镜像（第十七章讲 dockershim 移除时这条是重点）。 对应命令组： 命令 作用 注意 docker images 本地镜像列表 SIZE 是解压后共享大小，不等于它占的磁盘 docker history 层与指令对应表 --no-trunc 才看得到完整指令，也是查密钥泄漏的第一现场 docker image inspect 完整元数据 JSON RootFS.Layers、Config.Env、RepoDigests、Architecture docker image ls -q 只要 ID 配合 docker rmi $(...) 批量删 docker system df 磁盘账本 清理前先跑它，别反过来 docker save \u002F docker load 导出 \u002F 导入 tar 不跨架构；tar 体积按未压缩算 这一章的收获应该是：你从此看 docker build 的输出不再是「它在忙」，而是「它重跑到第几层、为什么这层没命中」。下一章把 Dockerfile 的每一条指令讲透，那正是控制分层的唯一手段。",{"id":16484,"title":1679,"titles":16485,"content":16486,"level":1177},"\u002Ftutorials\u002Fdocker#四dockerfile-逐条讲透指令语义四种组合与元数据层",[5],"Dockerfile 是镜像的源代码，也是本教程密度最高的一章。逐条讲不是因为记不住就没法用，而是因为每条指令都有「看起来对、跑起来炸」的陷阱，而陷阱基本都藏在默认行为里。",{"id":16488,"title":1690,"titles":16489,"content":16490,"level":1202},"\u002Ftutorials\u002Fdocker#from起点也是体积与漏洞的主要来源",[5,1679],"FROM node:22-bookworm-slim AS builder     # 带别名，供多阶段构建引用\nFROM golang:1.23-alpine\nFROM scratch                             # 空镜像：只有你显式复制进去的东西\nFROM ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0e... # 标签 + digest 双锁 要点： 一个 Dockerfile 可以有多个 FROM，这就是多阶段构建的骨架（第五章）；最终镜像只包含最后一个 FROM 之后的内容。 FROM 决定了一切上游：glibc 还是 musl、有没有 apt\u002Fapk、默认用户是不是 root、有多少 CVE 等着被打。基础镜像选型在第十五章展开。 版本要写具体。FROM node:latest 意味着你的构建今天绿、下个月不知道因为什么红。生产用「大版本 tag + digest」。",{"id":16492,"title":1755,"titles":16493,"content":16494,"level":1202},"\u002Ftutorials\u002Fdocker#copy-vs-add默认选-copy没有例外",[5,1679],"COPY ADD 复制本地文件到镜像 ✓ ✓ 源是 URL（ADD http:\u002F\u002F…） ✗ ✓（但不是为它设计的功能） 源是 tar 自动解压 ✗ ✓（隐式行为，容易踩） 支持 --chown \u002F --chmod ✓ ✓ 官方文档自己就写着「通常用 COPY」。ADD 那两个额外能力都有更好的替代品：下载用 RUN curl + RUN rm（能校验、能清理、能合进一层），解压用 RUN tar -xzf。一个细节值得记：ADD \u003Ctar-url> 不解压（网络源不触发自动解压），这种不一致正是它该被避开的原因。 COPY 的两个易错点：COPY --chown=app:app --chmod=0644 . \u002Fsrv\u002Fshop\u002F 避免整棵目录被 root 占有；目标以 \u002F 结尾是「拷进目录」，不以 \u002F 结尾且源是单个文件则是「重命名」。另外 COPY 只能看见构建上下文，COPY ..\u002Fx 一律失败；.dockerignore 会同时影响 COPY . . 的内容与缓存哈希（第五章）。",{"id":16496,"title":1889,"titles":16497,"content":16498,"level":1202},"\u002Ftutorials\u002Fdocker#runshell-形式与-exec-形式以及-pid-1-的伏笔",[5,1679],"RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \\\n    && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*          # 三层合一：一条 RUN 一个层，必须链起来\nSHELL [\"\u002Fbin\u002Fbash\", \"-o\", \"pipefail\", \"-c\"] # 改默认 shell（见本章 SHELL）\nRUN set -o pipefail && make 2>&1 | tee build.log 每多一条 RUN 就多一层，而层里的东西删不掉。所以「装包 → 清理」必须用 && 写在同一条 RUN 里；分开写等于用下一层去遮蔽，磁盘上原文件还在。 apt-get update 单独成层会长期缓存，之后装到的永远是那次快照的旧包；和 install 同层才能一起失效。 每条 RUN 都是全新 shell 进程，互不继承状态：RUN cd \u002Ftmp 下一层回到工作目录，RUN export FOO=1 下一层消失（要跨指令请用 ENV）。 形式有两种，差别很重要：RUN npm ci（shell 形式，实际执行 \u002Fbin\u002Fsh -c \"npm ci\"）与 RUN [\"npm\", \"ci\"]（exec 形式，没有 shell）。shell 形式能用管道、&&、通配符、变量展开，代价是你真正想跑的程序不是 PID 1——这直接决定信号能不能收到（第十六章的优雅停机整章在讲这个坑）。exec 形式里 node 就是 PID 1。",{"id":16500,"title":1988,"titles":16501,"content":16502,"level":1202},"\u002Ftutorials\u002Fdocker#cmd-vs-entrypoint四种组合必须一次讲清",[5,1679],"这两个概念之所以绕，是因为它们一个管「主命令的参数」，一个管「主命令本身」，而且只有最后一层生效。 Dockerfile 里写了 容器启动时执行 docker run img python -m http.server 时执行 CMD [\"node\",\"app.js\"]，无 ENTRYPOINT node app.js python -m http.server（整条被替换） ENTRYPOINT [\"node\"]，无 CMD node node python -m http.server（参数追加） ENTRYPOINT [\"node\"] + CMD [\"app.js\"] node app.js node python -m http.server（只替换参数——最常用的组合） ENTRYPOINT [\"sh\",\"-c\",\"node $FLAG app.js\"] + CMD shell 解释 shell 解释，追加的参数变成 $0 $1，CMD 被忽略 实践结论： 一个纯服务型镜像：ENTRYPOINT 定命令、CMD 定默认参数。这样使用者可以用 docker run img --help 换参数，而不是被迫重写整个命令。 一个工具型镜像（ubuntu、alpine）：只写 CMD [\"bash\"]，因为用户就是要随便跑点什么，ENTRYPOINT 会碍事。 docker run 后面的参数永远覆盖 CMD，--entrypoint 永远覆盖 ENTRYPOINT：docker run --rm --entrypoint sh ghcr.io\u002Fexample\u002Fshop-api:1.8.3 -c \"node -v\" 是第七章调试的起手式。 只有最后一层生效：Dockerfile 里写两条 CMD，第一条静默失效；继承自基础镜像的 CMD\u002FENTRYPOINT 也会被你覆盖。想确认最终值：docker image inspect img --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'。 ENTRYPOINT\u002FCMD 是元数据层，不占空间，把它们写在最后能最大化缓存命中。",{"id":16504,"title":2187,"titles":16505,"content":16506,"level":1202},"\u002Ftutorials\u002Fdocker#env-arg构建期与运行期以及它们都会留在历史里",[5,1679],"指令 何时可见 是否留在最终镜像 典型用途 ARG 仅构建期（--build-arg 传入） 否（值不留，但指令文本留） 版本号、代理、构建目标 ENV 构建期 + 运行期 是（Config.Env） NODE_ENV、PATH、TZ ARG NODE_VERSION=22.11.0        # 有默认值；ARG 只在声明之后的指令生效\nFROM node:${NODE_VERSION}-bookworm-slim   # FROM 之前的 ARG 是「全局作用域」，不进任何层\nENV NODE_ENV=production 两条铁律： ARG 不产生数据层，但 docker history 里能看到那行指令文本，所以 --build-arg PASSWORD=... 依然是泄漏。 ENV\u002FARG\u002FRUN 都禁止放密钥：RUN curl -H \"token: $SECRET\" 里字面量写死的 token，会原样出现在层历史与构建日志里（第十五章会演示用 docker history --no-trunc 把它捞出来）。密钥走 --mount=type=secret 或运行时注入。 ENV 的另一个用途是运行期可覆盖的默认值：ENV TZ=Asia\u002FShanghai 进镜像，但 docker run -e TZ=UTC 仍能盖掉它——这是第十三章「配置优先级」的第一层。",{"id":16508,"title":2338,"titles":16509,"content":16510,"level":1202},"\u002Ftutorials\u002Fdocker#workdiruservolumeexposelabelhealthcheckshell",[5,1679],"WORKDIR \u002Fsrv\u002Fshop      # 比 RUN cd 好：后续 RUN\u002FCOPY\u002FADD 的相对路径基准，且是元数据层\nUSER 1001:1001         # 之后的 RUN 与容器运行都以该身份（rootless 与第十五章相关）\nVOLUME \u002Fvar\u002Flib\u002Fmysql  # 声明「这里该有独立存储」，构建时不生效、运行时无名卷兜底\nEXPOSE 3000            # 纯文档：不发布端口、不写 iptables，只是给人和 tool 看的\nLABEL org.opencontainers.image.source=\"https:\u002F\u002Fgithub.com\u002Fexample\u002Fshop\"  # 治理用\nHEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD node healthcheck.js\nSHELL [\"\u002Fbin\u002Fbash\", \"-eo\", \"pipefail\", \"-c\"]   # 改默认 shell，让管道失败可见 逐条说清「为什么这么设计」和「不这么做会怎样」： WORKDIR 而不是 RUN cd：RUN cd \u002Fx && make 那条指令内确实生效，但下一层又回到原地；而 WORKDIR 会自动创建目录、跨指令生效、且只是元数据（不新增大小）。用绝对路径，别用 ~。 USER 放在能放的位置：如果 USER app 之后的 RUN 需要往系统目录写，构建就会失败。正确姿势是 root 完成安装、chown\u002F--chown 好、最后 USER。 VOLUME 有副作用：它保证挂载点里不会被 RUN 写入的内容进入镜像（构建时对该目录的写会被丢弃），也让 docker run 忘了挂卷时悄悄建一个匿名卷（第九章讲怎么认出这种「幽灵数据」）。数据库镜像里 VOLUME \u002Fvar\u002Flib\u002Fmysql 就是这个意思：那目录里的数据不该属于镜像。也正因为 VOLUME 会丢写，如果你想在镜像里预置模板数据，得换个路径。 EXPOSE 只是文档：docker run -p 才是真的开端口映射；EXPOSE 的作用是让 docker ps 显示、让 --publish-all（-P）有东西可发布、让 Compose 的 expose 语义有依据。不写它不会连不上，写错它也不会连不上——很多人被这点反复误导。 LABEL 用于治理：镜像多了以后，「这是谁建的、源码在哪、什么构建时间」全靠 OCI 标准标签（org.opencontainers.image.*）。别把动态值（如 git rev）放进第一层，否则每次构建所有层全失效。 HEALTHCHECK 是「自证活着」的声明：没有它，Docker 只知道进程在不在，不知道服务能不能用。它是第十六章就绪判断和 depends_on: service_healthy 的基础。注意：unhealthy 不会自动重启容器，得配合重启策略或外部 watcher。 SHELL 改默认 shell：\u002Fbin\u002Fsh 各发行版行为不同（dash vs bash），管道失败与否、set -e 支持都不同。想写严谨的构建脚本就显式切 bash 并开 pipefail。",{"id":16512,"title":2535,"titles":16513,"content":16514,"level":1202},"\u002Ftutorials\u002Fdocker#指令顺序缓存的分水岭",[5,1679],"把上面的知识合起来就是一条排序原则——变化频率从低到高。下面这个左右对比能背下来： FROM node:22-bookworm-slim                # 很少变\nWORKDIR \u002Fsrv\u002Fshop                          # 很少变\nCOPY package.json package-lock.json .\u002F     # 依赖变更时才变\nRUN npm ci --omit=dev                       # 跟着依赖锁变\nCOPY . .                                    # 每次都变 ← 分水岭\nENV NODE_ENV=production                     # 元数据\nEXPOSE 3000                                 # 元数据\nCMD [\"node\", \"server.js\"]                   # 元数据，永远放最后 反过来写（先 COPY . . 再 RUN npm ci）的话，改一行注释就要重装依赖。示例 04 把这份 Dockerfile 做成了可点交互：点任意一条指令，右侧的层堆叠会即时高亮「新增层 \u002F 元数据层 \u002F 复用缓存」，并演示改动源码时的失效范围。",{"id":16516,"title":2605,"titles":16517,"content":16518,"level":1202},"\u002Ftutorials\u002Fdocker#一个可以直接抄的最小-dockerfile",[5,1679],"FROM node:22-bookworm-slim\nENV NODE_ENV=production\nWORKDIR \u002Fsrv\u002Fshop\nCOPY package.json package-lock.json .\u002F\nRUN npm ci --omit=dev --npm-config.ignore-scripts=false\nCOPY src .\u002Fsrc\nEXPOSE 3000\nUSER 1001:1001\nHEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD node -e \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"\nCMD [\"node\", \"src\u002Fserver.js\"] 里面每一行的理由：ENV 提前让依赖只装生产包；两次 COPY 切出缓存分水岭；USER 用数字 UID（不依赖镜像里恰好存在那个用户名）；HEALTHCHECK 走 exec 形式并显式 || exit；CMD 用 exec 形式以便收信号。下一章讲构建流程时，我们围绕它做缓存实验。",{"id":16520,"title":2684,"titles":16521,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#五构建流程与缓存buildkitdockerignore-与多阶段构建",[5],{"id":16523,"title":2688,"titles":16524,"content":16525,"level":1202},"\u002Ftutorials\u002Fdocker#docker-build-的最小形态与必给参数",[5,2684],"docker build -t ghcr.io\u002Fexample\u002Fshop-api:1.8.3 .\ndocker build -f Dockerfile.prod --target runtime -t shop-api:1.8.3 .\u002Fbackend 参数 作用 什么时候必须用 -t name:tag 命名并打标签 每次；没 tag 的镜像是悬空镜像，难找难删 -f \u002F --target 指定 Dockerfile \u002F 多阶段只做到某阶段 一仓库多构建；单独调试 builder 阶段 --build-arg K=V 传 ARG 版本号、代理、构建开关 --pull 强制重新拉基础镜像 追基础镜像的安全补丁（否则用本地旧副本） --no-cache 全量重建 复现 CI 的干净构建，日常别用（慢） --platform 指定目标平台 在 x86 上给 ARM 设备构建 --push \u002F --load 构建完推送 \u002F 载入本地 多架构时 --load 不支持多平台镜像 --progress=plain 展开完整日志 构建失败时看清是哪条指令哪一行 结尾那个 . 是构建上下文：Docker 会把它整个目录打包传给 daemon。目录里塞了 2GB 的 node_modules 或 .git，你每次构建就白传 2GB——这是 .dockerignore 存在的第一理由。",{"id":16527,"title":16295,"titles":16528,"content":16529,"level":1202},"\u002Ftutorials\u002Fdocker#buildkit-与-syntax",[5,2684],"从 Docker 23 起 BuildKit 是默认构建器。它相对老构建器的差异不是「快一点」，而是三条实质能力（--mount、heredoc、并行构建）。启用新语法靠文件第一行的解析器指令： #syntax=docker\u002Fdockerfile:1\nRUN --mount=type=cache,target=\u002Froot\u002F.npm npm ci --omit=dev\nCOPY --link . .                         # 不因父层权限反复失效，也不重置时间戳\nRUN \u003C\u003CEOF                             # heredoc：多行脚本不用拼 && 与转义\n  addgroup -g 1001 app && adduser -S -D -u 1001 -G app app\nEOF #syntax= 必须放在 ARG\u002FFROM 之前才生效（规范写法哈希后带空格也合法，解析器允许）。写 docker\u002Fdockerfile:1 表示跟随 1.x，写 docker\u002Fdockerfile:1.7 是固定版本、CI 可复现。--mount=type=cache 的缓存不进镜像，只在构建环境保留，能把「每次重装依赖」从 90 秒压到 5 秒；代价是它不受层缓存控制，换基础镜像后可能读到旧包（必要时 docker builder prune）。 示例 05 用同一个 Dockerfile 做三种改动——改注释、改依赖锁、改源码——对比 docker build 的逐层输出，失效起点完全不同。",{"id":16531,"title":2984,"titles":16532,"content":16533,"level":1202},"\u002Ftutorials\u002Fdocker#层缓存命中的判定规则",[5,2684],"一句话规则：命中 = 父层 ID 相同 + 指令文本相同 + （对 COPY\u002FADD）被复制文件的内容哈希相同。三条任何一条变了，这一层以及它之后的所有层全部重建。 变更内容 从哪一层开始重建 只改 CMD\u002FENV\u002FEXPOSE 的值 从那条元数据指令起（前面全命中，重建很快） 改 README.md（没被 COPY 到） 通常不重建；但若它在上下文里且被 COPY . . 覆盖，就重建 改 package-lock.json COPY package-lock 那层起重建 改 src\u002Fserver.js COPY . . 那层起重建 基础镜像更新（--pull） 全部重建 文件权限\u002Fmode 变化 也算内容变化（COPY 把 mode 纳入哈希） 两个反直觉细节： COPY . . 的哈希是整个上下文（扣除 .dockerignore）算的。于是 .gitignore 改了、dist\u002F 多了临时文件、生成了 *.log 都会击穿缓存。把 COPY . . 收窄成 COPY src .\u002Fsrc，既减小上下文又稳定缓存。 RUN 只看指令文本，不看外部世界。RUN apt-get install -y curl=7.88.1-1+deb12u1 带版本写法能按需失效；而 RUN git clone https:\u002F\u002Fgithub.com\u002Fexample\u002Fshop 只要文本不变就永远命中旧缓存，拿到的是半年前的代码——「代码明明更新了镜像里却是旧的」的成因。解法：用 COPY 放进去，或加 --build-arg COMMIT= 让文本变化。",{"id":16535,"title":3142,"titles":16536,"content":16537,"level":1202},"\u002Ftutorials\u002Fdocker#dockerignore不是可选项",[5,2684],".git\n**\u002Fnode_modules\ndist\ncoverage\n*.log\n.env\n.env.*\n!.env.example\ncompose*.yaml\ntests 不写它的三个后果：构建变慢（上下文几 GB）、缓存被击穿（任何杂文件都改内容哈希）、泄密（.env、id_rsa 被 COPY . . 打进镜像，docker history 与 docker save 之后都能翻出来）。语法接近 .gitignore，有一处常见「误报错」：.env 被 ignore 后 COPY .env \u002Fapp\u002F 直接失败——这不是 bug，而是提示：镜像里不该有需要 .env 的东西（第十三章）。",{"id":16539,"title":3201,"titles":16540,"content":16541,"level":1202},"\u002Ftutorials\u002Fdocker#多阶段构建编译阶段与运行阶段彻底分开",[5,2684],"问题的形状：构建 Node\u002FGo\u002FJava 应用需要编译器、包管理器、源码、测试依赖、头文件，运行只需要运行时和产物。把它们留在一个镜像里，等于把攻击面和体积一起打包上线。 #syntax=docker\u002Fdockerfile:1\nFROM node:22-bookworm-slim AS deps\nWORKDIR \u002Fsrv\u002Fshop\nCOPY package.json package-lock.json .\u002F\nRUN npm ci --omit=dev\nFROM node:22-bookworm-slim AS build\nCOPY --from=deps \u002Fsrv\u002Fshop\u002Fnode_modules .\u002Fnode_modules\nCOPY . .\nRUN npm run build && npm prune --omit=dev\nFROM node:22-bookworm-slim AS runtime        # 只有最后一个 FROM 之后的内容进最终镜像\nENV NODE_ENV=production\nWORKDIR \u002Fsrv\u002Fshop\nCOPY --from=build \u002Fsrv\u002Fshop .\u002F\nUSER 1001:1001\nEXPOSE 3000\nCMD [\"node\", \"server.js\"] 要点四条：COPY --from=\u003C阶段名|序号|外部镜像> 从别的阶段取产物，未被最终阶段引用的中间阶段会被跳过；产物里不要夹带构建工具（npm prune --omit=dev；Go 用 CGO_ENABLED=0 静态编译后拷进 FROM scratch）；阶段命名可读（AS deps）才方便 --target deps 单独构建调试；各阶段共享同一基础镜像的层，多阶段不额外吃磁盘。常见量级：1.2GB → 180MB。",{"id":16543,"title":3331,"titles":16544,"content":16545,"level":1202},"\u002Ftutorials\u002Fdocker#buildx-与多架构",[5,2684],"docker buildx create --name multi --driver docker-container --use\ndocker buildx inspect --bootstrap\ndocker run --privileged --rm tonistiigi\u002Fbinfmt --install all\ndocker buildx build --platform linux\u002Famd64,linux\u002Farm64 -t ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --push .\ndocker manifest inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3 跨架构构建靠 QEMU 用户态模拟（binfmt 注册解释器），慢且某些编译会段错误，稳妥做法是用原生 runner（CI 上的 ARM 机器或云端构建）。一次产出多架构的结果是一个 manifest list（镜像索引），--load 只能载入单个平台，多架构必须 --push（或 --output type=oci,dest=out.tar）。exec format error 的根因就在这里：拿 amd64 镜像去 ARM 上跑；查平台用 docker image inspect --format '{{.Os}}\u002F{{.Architecture}}'，验证可以用 docker pull --platform linux\u002Farm64 busybox。",{"id":16547,"title":3469,"titles":16548,"content":16549,"level":1202},"\u002Ftutorials\u002Fdocker#构建参数与-secrets-mount",[5,2684],"需要凭据才能构建（私有 npm registry、GitHub Packages）时，不要用 --build-arg TOKEN=xxx，它会留在层历史里；用 BuildKit 的 secret mount： printf '%s' \"${NPM_TOKEN}\" | docker buildx build --secret id=npm_token,src=- -t shop-api:1.8.3 . RUN --mount=type=secret,id=npm_token,target=\u002Frun\u002Fsecrets\u002Fnpm_token \\\n    NPM_TOKEN=$(cat \u002Frun\u002Fsecrets\u002Fnpm_token) npm ci --omit=dev --mount=type=secret 的文件只在这条指令执行期间存在，不进任何层、不出现在 docker history。另外 --progress=plain 的日志会被 CI 存档，别把 secret echo 出来。 构建侧的收获总结成三句：Dockerfile 的顺序决定速度，.dockerignore 决定速度与安全性，多阶段决定体积与攻击面。下面进入运行侧。",{"id":16551,"title":3569,"titles":16552,"content":16553,"level":1177},"\u002Ftutorials\u002Fdocker#六容器生命周期与-run-参数一张表把命令行讲清",[5],"镜像是死的，容器是活的。这一章把 docker run 的几十个参数按用途分组，再解决「docker ps -a 里那些状态字符串到底在说什么」。",{"id":16555,"title":3582,"titles":16556,"content":16557,"level":1202},"\u002Ftutorials\u002Fdocker#状态机created-running-paused-exited-removed",[5,3569],"docker create --name shop-web nginx:1.27-alpine    # 只建不跑：created\ndocker start shop-web                              # running\ndocker pause shop-web; docker unpause shop-web     # 冻结 \u002F 解冻（cgroup freezer）\ndocker stop -t 10 shop-web                         # 先 SIGTERM，超时后 SIGKILL → exited\ndocker kill -s SIGTERM shop-web                    # 直接发信号，默认 SIGKILL\ndocker rm shop-web                                 # 删除容器（连同可写层）\ndocker container prune                             # 清掉所有已停止容器 三条要点： stop 是请求，kill 是强制。stop 发 SIGTERM 并等 -t 秒（默认 10），到点没退就 SIGKILL。这段等待期是应用收尾（放连接、刷缓冲、提交 offset）的唯一窗口——第十六章整章讲这条链路为什么经常失效。 pause 不是「暂停服务」。它冻结进程，TCP 连接会一直挂着直到超时；而且暂停中的容器不能 exec 也不能 stop（要先 unpause）。生产里几乎不用。 --rm 让容器一次性：主进程退出即自动 docker rm，可写层与匿名卷一起销毁。好处是不留垃圾，坏处是排障现场消失了——调试阶段永远别加 --rm，有数据的容器永远不要加。",{"id":16559,"title":3765,"titles":16560,"content":16561,"level":1202},"\u002Ftutorials\u002Fdocker#run-参数分组速查",[5,3569],"组 参数 说明与坑 运行控制 -d 后台运行并打印容器 ID -it -i 保持 stdin、-t 分配伪终端，通常两个一起用；cron\u002FCI 里不要加 -it，否则 the input device is not a TTY --rm 退出即删，别用于有数据的容器 标识 --name \u002F --hostname 前者是容器名（也是网络里的 DNS 名），重名报 Conflict. The container name ... is already in use；后者只改容器内 hostname 的值，两者是两回事 --label env=prod 打标签，配合 --filter label=env=prod 批量管理 端口 -p 8080:80 宿主 0.0.0.0:8080 → 容器 80；写 -p 8080:80\u002Fudp 才是 UDP，不写默认 tcp -p 127.0.0.1:8080:80 只绑回环，数据库与管理面板的默认写法 -p 8080 \u002F -P 前者宿主端口随机、后者把所有 EXPOSE 过的端口都随机发布（docker ps 看结果） 环境 -e KEY=VAL 可重复；-e KEY 表示从当前 shell 透传 --env-file .env.production 每行 K=V，# 开头是注释；别把 .env 提交 挂载 -v name:\u002Fpath 命名卷（省略 name 则是匿名卷）；-v .\u002Fconf:\u002Fetc\u002Fapp:ro 是 bind mount + 只读，相对路径基于当前目录 --mount type=volume,source=db,target=\u002Fvar\u002Flib\u002Fmysql 等价但可写 readonly、volume-opt，拼错会直接报错（第九章对照表） 网络 --network shop-net \u002F --network host 自定义网络（有 DNS，第八章）；host 共享宿主栈（Mac\u002FWindows 上不生效，且让 --dns、--network-alias 失效） --add-host db:10.0.0.5 往 \u002Fetc\u002Fhosts 追加一条 重启 --restart unless-stopped 四种策略见第十六章 资源 --memory 512m \u002F -m 内存上限，超了就 Exited (137)；--memory-swap 768m 是总内存 + swap，语义特殊，第十四章专门讲 --cpus 1.5 \u002F --cpuset-cpus 0,1 \u002F --pids-limit 256 CPU 配额 \u002F 绑核 \u002F 防 fork 炸弹 安全 --user 1001:1001 以该身份运行（对齐 bind mount 属主时用） --read-only --tmpfs \u002Ftmp 根文件系统只读 + 临时可写目录 --cap-drop ALL --cap-add NET_BIND_SERVICE 能力裁剪；--security-opt no-new-privileges 禁止提权（第十五章） --init 用 tini 当 PID 1，转发信号、回收僵尸进程 --entrypoint sh 覆盖 ENTRYPOINT 来调试（第七章）",{"id":16563,"title":4171,"titles":16564,"content":16565,"level":1202},"\u002Ftutorials\u002Fdocker#docker-ps-a-的-status-怎么读",[5,3569],"STATUS 片段 含义 下一步查什么 Up 3 hours 正常运行 3 小时 — Up 3 hours (healthy) 健康检查通过 第十六章 HEALTHCHECK Up 3 hours (health: starting) 还在 --start-period 内 连续通过 retries 次才转 healthy Up 3 hours (unhealthy) 检查连续失败 容器还在跑但服务不可用；Docker 不会自动重启它 Restarting (1) 5 seconds ago 崩溃后被重启策略反复拉起 docker logs --tail 50；1 是退出码 Exited (0) 2 hours ago 正常退出 服务型容器退 0 多半是「前台命令跑完了」，检查 CMD Exited (1) 5 minutes ago 应用自身报错 logs → --entrypoint sh（第七章三步法） Exited (126) 命令不可执行（权限\u002F不是可执行文件） 检查 ENTRYPOINT 指向的文件与 +x Exited (127) 命令找不到（路径错\u002F缺动态库） --entrypoint sh 进去 ls、ldd Exited (137) = 128+9 被 SIGKILL：OOM、docker kill、或 stop 超时 inspect 看 OOMKilled（第十四章） Exited (143) = 128+15 被 SIGTERM 终止（没自己处理） 优雅停机没做好（第十六章） Created 只 create 过，从未成功启动 docker start 看它报什么 Dead 清理时出错，异常状态 docker rm -f，并查内核日志 退出码只需记一条：128 以上是「被信号 X 杀死」，X = 码 − 128。137−128=9（KILL）、143−128=15（TERM）。看到 Exited (137) 先怀疑资源限制，别急着改业务代码。",{"id":16567,"title":4432,"titles":16568,"content":16569,"level":1202},"\u002Ftutorials\u002Fdocker#观察一个正在跑的容器",[5,3569],"docker logs -f --tail 100 --since 30m shop-api     # -f 跟随，-t\u002F--timestamps 加时间戳\ndocker stats --no-stream                            # 一次快照：CPU%、MEM USAGE\u002FLIMIT、NET、BLK IO、PIDS\ndocker top shop-api                                 # 宿主视角看容器进程（真实 PID 与 USER）\ndocker diff shop-web                                # 可写层里增(A)\u002F改(C)\u002F删(D) 的文件\ndocker events --since 30m --filter event=die        # 生命周期事件 各自的用处： logs 读的是容器的 stdout\u002Fstderr，不是容器里的文件。应用若把日志写进容器内文件，docker logs 永远空白——这就是 12-factor 要求「日志打到标准输出」的原因（第十三章）。 stats 的 MEM USAGE \u002F LIMIT，LIMIT 是 cgroup 上限而不是宿主内存，百分比按上限算——判断「是不是被限制死了」的最快入口。--no-stream 只采一次，适合脚本。 top 解决「容器里跑着 java 但镜像没装 ps」的尴尬（第七章）。 diff 是个便宜的侦探：让容器跑一轮业务再看 diff，就知道该挂哪个卷、--read-only 会挡住哪些路径。 示例 06 把这套观察串成一次真实的排查流：从 docker ps -a 里那行 Restarting (1) 开始，逐步定位到 Exited (137)。 生命周期搞清楚了，下一章学「进去看」——以及当镜像里什么都没有时怎么调试。",{"id":16571,"title":4602,"titles":16572,"content":16573,"level":1177},"\u002Ftutorials\u002Fdocker#七进入容器与调试execinspect-与镜像里什么都没有",[5],"调试容器的心法只有一句：容器就是一个进程，进不去就用外部的眼睛看。这一章把两种眼睛都给你。",{"id":16575,"title":16310,"titles":16576,"content":16577,"level":1202},"\u002Ftutorials\u002Fdocker#docker-exec-与-的坑",[5,4602],"docker exec -it shop-api sh          # 最常用：alpine\u002Fbusybox 只有 sh；Debian 系镜像才有 bash\ndocker exec -u root -it shop-api sh  # 镜像里 USER 非 root 时临时提权\ndocker exec shop-api cat \u002Fetc\u002Fos-release        # 非交互，适合脚本\ndocker exec shop-api ls -- -p                   # 文件名以 - 开头时必须用 -- 分隔 为什么很多镜像没有 bash？因为基础镜像是 alpine\u002Fbusybox（体积小、攻击面小），甚至 distroless\u002Fscratch 连 sh 都没有。这不是作者恶意，而是第十五章的取舍结果。遇到没 shell 的镜像，看本章后半部分。 -- 的规则只有一条：「选项解析到此为止」。docker exec -it c -- node -v 里 -- 之后全部交给容器；docker run --rm img -- --version 里 docker 会把第二个 -- 之后的内容当成「镜像自己的参数」，行为和你预期不同。当你要传的参数本身带 -（-p、--version），不加分隔符时 docker 会先把它当成自己的选项，报出的错误通常很难看。",{"id":16579,"title":4751,"titles":16580,"content":16581,"level":1202},"\u002Ftutorials\u002Fdocker#inspect用-format-只要你要的字段",[5,4602],"docker inspect 一次吐几 KB JSON，硬读会累死，--format 才是正解： docker inspect shop-api --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'\ndocker inspect shop-api --format '{{json .Config.Env}}'      # 同样套路：{{json .Mounts}}、.State.*\ndocker inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} health={{.State.Health.Status}}' shop-api shop-db\ndocker ps --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}'   # ps 同样支持，脚本神器 --format 是 Go 模板：{{.X}} 取值、{{json .X}} 打印结构、{{range}} 遍历、{{if}} 条件。报 nil can't be fields 一般是路径写错（比如 NetworkSettings.Networks 是 map，得 range 进去），先原样 inspect 一次看清结构再改模板。",{"id":16583,"title":4867,"titles":16584,"content":16585,"level":1202},"\u002Ftutorials\u002Fdocker#docker-cp双向搬文件",[5,4602],"docker cp shop-api:\u002Fsrv\u002Fshop\u002Fconfig\u002Fapp.yaml .\u002Fapp.yaml 是容器 → 宿主，docker cp .\u002Fnginx.conf shop-web:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fsite.conf 是宿主 → 容器。 用途是把配置塞进去做一次性试验、把日志与产物捞出来。两个限制：运行中改的文件容器重建就没了（所以它是调试手段，不是部署手段）；cp 目录时的合并语义与 rsync 不同，路径末尾的 \u002F 要写清楚。",{"id":16587,"title":4899,"titles":16588,"content":16589,"level":1202},"\u002Ftutorials\u002Fdocker#镜像里没有编辑器没有-ps没有-curl-怎么办",[5,4602],"按代价从小到大三个方案： 方案 做法 适合 代价 从外部看 docker top、docker logs、docker cp、宿主 \u002Fproc\u002F\u003Cpid>\u002F、nsenter 只想看状态与文件 需要宿主权限 临时装工具 docker exec -it shop-api sh -c 'apk add --no-cache curl tcpdump' 一次性排查 破坏「镜像不可变」，只允许在开发\u002F临时环境 调试镜像 单独 Dockerfile.debug（含 strace\u002Flsof\u002Fjq），或把 nicolaka\u002Fnetshoot 连进同一网络 反复排查网络与进程 多一份镜像要维护 从宿主直接看容器内部，是很值钱的一招（Linux 上）： PID=$(docker inspect --format '{{.State.Pid}}' shop-api)\nsudo ls -l \u002Fproc\u002F$PID\u002Froot\u002Fsrv\u002Fshop; sudo cat \u002Fproc\u002F$PID\u002Fcgroup   # 容器的根文件系统 \u002F 它归属哪个 cgroup\nsudo nsenter -t $PID -n ss -lntp                                 # 进入它的 net namespace 看监听端口\nsudo nsenter -t $PID -m -p -- cat \u002Fproc\u002F1\u002Fenviron | tr '\\0' '\\n' nsenter 借用容器的 namespace，看到的端口、路由、文件都是容器视角，而工具装在宿主上——镜像里什么都没有也能查。",{"id":16591,"title":5126,"titles":16592,"content":16593,"level":1202},"\u002Ftutorials\u002Fdocker#起来就退出-entrypoint-sh-是唯一可靠的入口",[5,4602],"容器 Exited (1) 且没有日志时，你连 exec 的机会都没有（它已经不跑了）。正确做法是把主命令换成 shell，再手工跑原来那条： docker run --rm -it --entrypoint sh ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n\u002Fsrv\u002Fshop $ node server.js        # 错误现在直接打到你的终端上\n\u002Fsrv\u002Fshop $ env | sort; id; ls -l . 三步定位法（第十八章的排障表也按它走）：docker logs --tail 50 \u003Cname> → 没输出就 --entrypoint sh 手工启动 → 怀疑环境差异就在 shell 里核对 env、id、路径与权限。",{"id":16595,"title":5222,"titles":16596,"content":16597,"level":1202},"\u002Ftutorials\u002Fdocker#docker-events谁在什么时候动了什么",[5,4602],"docker events --since 30m --until 10m --filter type=container，或者按事件与标签收窄：docker events --filter 'event=die' --filter 'label=app=shop-api'。 die 事件带 exitCode，--since\u002F--until 支持相对时间。它能回答「谁杀了我的容器」：如果 die 前面有 kill 事件，那是人为或脚本发的信号；只有一个 die 且 OOMKilled=true，那是内核干的（第十四章）。",{"id":16599,"title":5264,"titles":16600,"content":16601,"level":1202},"\u002Ftutorials\u002Fdocker#为什么容器里通常没有-systemd",[5,4602],"因为容器不是虚拟机。systemd 的职责是管理一整台机器（init、服务、cgroup、日志、登录会话），而容器的定位是「一个进程 + 它需要的东西」： PID 1 应该留给你的应用；给了 systemd，信号、退出码、日志转发全乱（第十六章的信号问题会加倍）。 systemd 要写 cgroup 树、访问 \u002Fsys\u002Ffs\u002Fcgroup、可能需要 CAP_SYS_ADMIN——在共享内核的容器里做这些，等于和宿主的管理者（dockerd、kubelet）抢 cgroup。这就是第二章 docker info 里 Cgroup Driver 必须统一的深层原因。 需要多个服务时的答案是多容器 + Compose（第十一章），不是在一个容器里跑五个 daemon。 确有特例（跑完整发行版做系统级测试），社区做法是带 systemd 的特权镜像，形如 --cgroupns=host --tmpfs \u002Frun --tmpfs \u002Frun\u002Flock -v \u002Fsys\u002Ffs\u002Fcgroup:\u002Fsys\u002Ffs\u002Fcgroup:ro。生产里把它当代码异味看待。 示例 07 把 exec、inspect --format、top、cp、--entrypoint 串成一次完整排查演练，全部输出是真实样本。 调试能力有了，下一章处理运行侧最容易出事的一块：网络。",{"id":16603,"title":5329,"titles":16604,"content":5332,"level":1177},"\u002Ftutorials\u002Fdocker#八端口与网络bridge自定义网络与那个会变-ip-的坑",[5],{"id":16606,"title":5335,"titles":16607,"content":16608,"level":1202},"\u002Ftutorials\u002Fdocker#五种网络驱动与取舍",[5,5329],"驱动 机制 典型用途 代价与限制 bridge（默认） Linux bridge + veth + NAT 本地开发、单机多容器 默认网络无 DNS、有 NAT 开销 host 容器直接用宿主网络栈 追求极致性能、需要看宿主网卡 直接占用宿主端口、与其他容器 hostname 混在一起、Mac\u002FWindows 无效 none 只有 lo 回环 纯计算任务、彻底断网 完全没网络 overlay VXLAN 隧道跨宿主 Swarm 多机服务 只有 Swarm 才用得上（第十七章） macvlan \u002F ipvlan 容器在局域网里拿到独立 MAC\u002FIP 需要以「独立网络设备」身份存在 与宿主通信受阻、交换机端口安全\u002F802.1X 会拒、AP 隔离网络无效 选择顺序基本是：自定义 bridge（单机首选） → host（明确要性能或要宿主栈） → macvlan（明确要求二层可达） → overlay（跨机且用 Swarm）。「默认 bridge」不在可选之列，理由见下一节。",{"id":16610,"title":5454,"titles":16611,"content":16612,"level":1202},"\u002Ftutorials\u002Fdocker#默认网桥-docker0-的真实行为",[5,5329],"装完 Docker，宿主上会出现 docker0 网桥，默认网段 172.17.0.0\u002F16。不加 --network 的容器都接在上面。它有两个坑： 没有基于名字的 DNS。默认网络里容器只能靠 IP 互相访问，而 IP 每次重启都可能变：docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' shop-api 今天拿到 172.17.0.2，重启后可能变成 172.17.0.3——任何把 IP 写进配置的用法都是定时炸弹。 同网段一律互通（icc=true），且所有容器混在一张广播域里，噪音与端口冲突随容器数量增长。 结论：生产与多容器开发一律用自定义网络，默认 bridge 只适合「临时跑一个孤立容器」。",{"id":16614,"title":5509,"titles":16615,"content":16616,"level":1202},"\u002Ftutorials\u002Fdocker#自定义网络与-dns-解析",[5,5329],"docker network create --driver bridge --subnet 172.28.0.0\u002F16 --gateway 172.28.0.1 shop-net\ndocker network connect shop-net shop-api     # 断开是 network disconnect；network inspect 看细节\ndocker network ls; docker network rm shop-net 自定义网络里，容器名就是主机名，可以直接 curl http:\u002F\u002Fshop-api:3000。原理是每个网络内嵌一个 DNS 服务器（容器里 cat \u002Fetc\u002Fresolv.conf 会看到 nameserver 127.0.0.11），它把容器名与网络别名解析成当前 IP——IP 变了名字不变，配置不用改。这就是「不要靠 IP，要靠名字」的技术依据。 docker run -d --name shop-api --network shop-net --network-alias api ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker exec shop-web curl -s http:\u002F\u002Fapi:3000\u002Fhealthz        # 别名同样可用\ndocker exec shop-api getent hosts shop-db                    # 看 DNS 实际解析到什么 --link 是老写法（默认网络时代的补丁），现在忘掉它：它靠注入环境变量和改 \u002Fetc\u002Fhosts 工作，行为隐晦、已废弃，Compose 里写了也不起作用。 network create 值得知道的开关：--internal（完全不出外网，只让容器间互通）、--ipv6、--ip-range、--opt com.docker.network.driver.mtu=1400（overlay\u002FVXLAN 必须减 MTU，否则出现「小请求正常、大请求卡死」的经典诡异现象）、--attachable（Swarm，第十七章）。",{"id":16618,"title":5708,"titles":16619,"content":16620,"level":1202},"\u002Ftutorials\u002Fdocker#端口映射四种写法与port-is-already-allocated",[5,5329],"docker run -d -p 8080:80 shop-web             # 宿主 0.0.0.0:8080 → 容器 80\ndocker run -d -p 127.0.0.1:8080:80 shop-web   # 只监听回环（推荐的默认姿势）\ndocker run -d -p 8080 shop-web                # 宿主端口随机；`-p 80\u002Fudp` 则是显式指定协议 背后是 docker-proxy 进程 + iptables DNAT。因此端口不通的排查顺序是固定的四步： docker ps --format '{{.Names}} {{.Ports}}'                # ① 映射到底存在吗\ndocker exec shop-web sh -c 'ss -lntp || netstat -lntp'    # ② 容器里真的在听吗？听的是 0.0.0.0 吗\nsudo ss -lntp | grep 8080                                 # ③ 宿主上这个端口被谁占了\nsudo iptables -t nat -S DOCKER | grep 8080                # ④ NAT 规则真的写进去了吗 第②步是新手最大的盲区：应用只 bind 127.0.0.1 时，端口映射永远不会通——从 veth 进来的包不是本地回环来源。所以「容器里的服务必须 bind 0.0.0.0」是硬性要求，而很多框架默认绑 localhost（Uvicorn、部分 Spring 配置、老版 webpack devServer），必须显式改。 Error response from daemon: driver failed programming external connectivity ... port is already allocated 的三类原因：别的容器占了（docker ps 查）、宿主进程占了（ss -lntp 查）、daemon 上次异常退出留下僵尸 docker-proxy（重启 docker 解决）。示例 08 是一次完整演练：从「连不上」一路查到「应用只 bind 了 127.0.0.1」。",{"id":16622,"title":5895,"titles":16623,"content":16624,"level":1202},"\u002Ftutorials\u002Fdocker#容器互访宿主访问容器",[5,5329],"容器 → 容器：同一个自定义网络，用容器名\u002F别名 + 容器内端口（http:\u002F\u002Fshop-db:5432）。不要绕道宿主映射端口，也不要用 IP。 宿主 → 容器：curl localhost:8080（走映射）；Linux 上还能直接 curl 172.28.0.5:5432（容器 IP，只用于调试）。 容器 → 宿主上的服务：用 host.docker.internal。Docker Desktop 自带该域名；Linux 上要手动加： docker run -d --name shop-api --add-host=host.docker.internal:host-gateway ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker exec shop-api curl -s http:\u002F\u002Fhost.docker.internal:5432\u002F host-gateway 是特殊值，让 Docker 自己填宿主在容器网络里的网关 IP，比写死 172.17.0.1 可靠。Compose 里对应 extra_hosts: [\"host.docker.internal:host-gateway\"]（第十二章）。注意 macOS\u002FWindows 上容器里的 127.0.0.1 不是你本机（第二章那台虚拟机），别拿 localhost 指宿主。 --network=host 为什么在 Mac 上不生效：那个 host 是 Docker VM 的 host，不是你 Mac 的；表现为容器内部能通、外部访问不了、-p 被忽略。Linux 上则真的共享栈：ss -lntp 能直接看到容器进程，同时端口冲突会打到宿主服务——宿主跑着 nginx 又 --network host 起一个 nginx，就是 address already in use。",{"id":16626,"title":6023,"titles":16627,"content":16628,"level":1202},"\u002Ftutorials\u002Fdocker#暴露到公网的安全边界",[5,5329],"写法 可达范围 评价 -p 8080:80 所有网卡（含公网 IP） 只在真正对外服务时用 -p 127.0.0.1:8080:80 仅本机 数据库、管理面板、内部 API 的默认写法 -p 10.0.0.5:8080:80 指定网卡（如内网口） 内外网分流时用 不写 -p，只在网络内互通 同网络容器 最稳妥的默认 还有一条只在 Docker 出现的经典事故：iptables 让 ufw\u002Ffirewalld 的规则形同虚设。Docker 在 PREROUTING\u002FFORWARD 里插了自己的链，优先级高于 ufw，于是你 ufw deny 5432 之后公网照样连得上数据库。解法是把访问控制放进 DOCKER-USER 链（Docker 不会重写它），或者从一开始就不给容器绑 0.0.0.0。 网络通了，数据该往哪儿放？下一章讲四种存储方式，以及「容器删了数据没了」这个事故是怎么发生的。",{"id":16630,"title":6129,"titles":16631,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#九数据持久化匿名卷命名卷bind-mount-与-tmpfs",[5],{"id":16633,"title":6133,"titles":16634,"content":16635,"level":1202},"\u002Ftutorials\u002Fdocker#四种存储先分清谁拥有这块空间",[5,6129],"容器可写层随 docker rm 消失，所以要持久化的数据必须放到容器文件系统之外。Docker 给了四条路： 类型 谁创建与管理 宿主上的位置 典型用途 容器删了数据还在吗 匿名卷 Docker 自动建（a1b2c3... 这种名字） \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F\u003C随机名>\u002F_data 临时容器、忘了挂卷时的兜底 在（成为孤儿卷，prune 才清） 命名卷 你显式命名，或 Compose 声明 \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002Fshop_db-data\u002F_data 数据库、需要备份与迁移的数据 在，且可预期 bind mount 你指向宿主任意路径 就是那个路径本身 开发时挂载源码、挂配置文件 在（本来就在宿主） tmpfs 内存\u002Fswap，不落盘 内存 密钥、会话、临时上传缓冲 不在（重启即清零） 选择依据只有一句：要不要被 Docker 生命周期管理。要 → 卷（跨机器一致、可备份、不受宿主目录权限影响）；要直接读写宿主上的真实文件（编辑即生效）→ bind mount；不能落盘 → tmpfs。",{"id":16637,"title":16329,"titles":16638,"content":16639,"level":1202},"\u002Ftutorials\u002Fdocker#v-与-mount-语法对照",[5,6129],"两种写法能表达的集合不同： docker run -d --name shop-db -v db-data:\u002Fvar\u002Flib\u002Fmysql postgres:16.4\ndocker run -d --name shop-db --mount type=volume,source=db-data,target=\u002Fvar\u002Flib\u002Fmysql postgres:16.4\ndocker run -d --name shop-web -v \u002Fsrv\u002Fshop\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf:ro nginx:1.27-alpine\ndocker run -d --name shop-web --mount type=bind,source=\u002Fsrv\u002Fshop\u002Fnginx.conf,target=\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf,readonly nginx:1.27-alpine -v 写法 --mount 等价 说明 -v \u002Fabs\u002Fpath --mount type=volume,target=\u002Fabs\u002Fpath 只给目标 → 匿名卷（最容易长出孤儿卷） -v name:\u002Fpath --mount type=volume,source=name,target=\u002Fpath 命名卷；name 不以 .\u002F\u002F 开头即判定为卷名 -v .\u002Fhost:\u002Fcont --mount type=bind,source=$(pwd)\u002Fhost,target=\u002Fcont bind；-v 相对路径基于当前工作目录 -v a:b:ro,z --mount type=bind,...,readonly,z 选项：ro、rw、z\u002FZ（SELinux）、volume-opt 实践建议：脚本与 Compose 用 --mount——参数拼错（比如卷名写了相对路径）时 --mount 会明确报错，而 -v 会静默地给你建一个空目录，最后表现为「配置没生效」。手工敲命令时 -v 省事。tmpfs 则只能用长语法：--mount type=tmpfs,tmpfs-size=64m,target=\u002Frun\u002Fredis（-v 表达不了大小限制）。 一个必须提前知道的坑：-v .\u002Fconf:\u002Fetc\u002Fapp 若宿主的 .\u002Fconf 不存在，Docker 会创建它并 chown 给 root；容器内原本 \u002Fetc\u002Fapp 里的文件会被空目录遮住，于是出现「镜像里有配置，挂载后配置没了」。",{"id":16641,"title":6511,"titles":16642,"content":16643,"level":1202},"\u002Ftutorials\u002Fdocker#volumes-from把别人的卷借过来",[5,6129],"docker run --rm --volumes-from shop-db -v \"$PWD:\u002Fbackup\" alpine \\\n  tar czf \u002Fbackup\u002Fdb-data-20261004.tar.gz -C \u002Fvar\u002Flib\u002Fmysql . --volumes-from 复用另一个容器声明过的所有挂载（包括匿名卷），是「备份\u002F恢复容器数据」的标准套路。注意它继承的是那个容器创建时的挂载组合，改了原容器的挂载配置要重建才生效。",{"id":16645,"title":6579,"titles":16646,"content":16647,"level":1202},"\u002Ftutorials\u002Fdocker#卷在宿主上的实际位置和怎么看清它",[5,6129],"docker volume ls; docker volume inspect db-data     # inspect 里的 Mountpoint 是宿主真实路径\nsudo du -sh \u002Fvar\u002Flib\u002Fdocker\u002Fvolumes\u002F* | sort -h | tail\ndocker volume rm db-data; docker volume prune       # prune 会删掉所有没被任何容器引用的卷 inspect 的 Mountpoint 就是宿主真实路径（rootless 下是 ~\u002F.local\u002Fshare\u002Fdocker\u002Fvolumes\u002F...）。docker volume prune 会删掉所有没被任何容器引用的卷——包括你上周测试留下的数据库，所以先 ls 再 rm，别直接 prune（第十四章把这条列入危险命令）。 两种备份\u002F迁移方式： docker run --rm --volumes-from shop-db -v \"$PWD:\u002Fbackup\" alpine tar czf \u002Fbackup\u002Fdb.tar.gz \u002Fvar\u002Flib\u002Fmysql\ndocker run -d --name shop-db-new -v db-data-new:\u002Fvar\u002Flib\u002Fmysql postgres:16.4\ndocker run --rm -v \"$PWD:\u002Fbackup\" -v db-data-new:\u002Fvar\u002Flib\u002Fmysql alpine tar xzf \u002Fbackup\u002Fdb.tar.gz -C \u002F 第二种把卷挂进一个临时容器再解包，好处是不依赖原容器存在，跨机器迁移时只需要那个 tar。生产上更常见的是走数据库自身的备份（pg_dump），卷快照只作灾备——文件级备份对运行中的数据库常常是不一致的。",{"id":16649,"title":6791,"titles":16650,"content":16651,"level":1202},"\u002Ftutorials\u002Fdocker#bind-mount-的权限坑uid-对不上是最常见故障",[5,6129],"现象：挂载成功、目录可见，但写进去报 EACCES: permission denied。原因是容器内的 UID 和宿主目录属主对不上——权限检查用的是数字 UID，不看用户名。 docker exec shop-api id                # uid=1001(app) gid=1001(app)\nls -ld \u002Fsrv\u002Fshop\u002Fuploads               # drwxr-xr-x 2 root root ... \u002Fsrv\u002Fshop\u002Fuploads\ndocker exec shop-api touch \u002Fsrv\u002Fshop\u002Fuploads\u002Ft   # permission denied → sudo chown -R 1001:1001 对齐数字 UID 四种解法与取舍： 解法 做法 备注 改宿主目录属主 chown 1001:1001（数字 UID） 最直接；要知道镜像里应用跑在哪个 UID（docker inspect 或 Dockerfile 的 USER） 用 --user 对齐 --user \"$(id -u):$(id -g)\" 开发机神器：容器进程就是你的宿主用户，写出的文件属主正确 改组权限 chgrp + chmod g+w + --group-add 需要共享写时更协作友好 改用命名卷 让 Docker 自己管理目录 卷首次挂到空目录时会把镜像里该目录的内容复制进卷（bind mount 不会！）——这条差异能救「配置丢了」的故障 rootless 模式再多一层：subuid 映射。容器里的 UID 会被平移，docker exec 看到 uid=0 实际是宿主的 100000，宿主目录属主要按 \u002Fetc\u002Fsubuid 的偏移算（grep $(whoami) \u002Fetc\u002Fsubuid）。这也是 rootless + bind mount 反复出权限问题的原因，第十五章还会遇到它。",{"id":16653,"title":6970,"titles":16654,"content":16655,"level":1202},"\u002Ftutorials\u002Fdocker#为什么数据库必须用卷镜像里为什么不该有数据",[5,6129],"三个理由： 性能：往镜像层所在的 overlayfs 写随机 IO 明显慢于专用文件系统目录（卷或 bind）。数据库这种写密集负载，overlay2 的 copy-up 会让每次首次写都变成「整文件复制」。 生命周期：容器是可丢弃的，数据库不是。挂卷之后 docker rm 掉容器，重新 run 同名卷，数据回来了。 可移植与备份：卷能被 volume inspect、被快照、被 --volumes-from 备份；容器可写层这三样都做不好。 而「镜像里的 \u002Fvar\u002Flib\u002Fmysql 为什么不该写进镜像」：镜像层是只读的，运行时对它的一切写入都落在容器层；把初始化数据打进镜像会导致镜像体积暴涨、每个容器各存一份、VOLUME 声明处还会丢弃写入（第四章）。更关键的是镜像会泄密——初始化 SQL 里往往带着默认密码、示例账号，任何能 pull 的人都能 docker save 后翻出来。正确做法是首次启动时用 POSTGRES_* 环境变量或 initdb 钩子建库（第十三章）。 示例 09 用一次完整流程演示这四种存储、diff 发现写入位置、以及那个权限坑的现场与修法。 数据落盘的问题解决了，下一章解决「镜像从哪来、到哪去」：仓库、tag 规范与私有 registry。",{"id":16657,"title":7052,"titles":16658,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十镜像仓库与-tag-规范拉推锁定与私有-registry",[5],{"id":16660,"title":7055,"titles":16661,"content":16662,"level":1202},"\u002Ftutorials\u002Fdocker#一个镜像的完整名字长什么样",[5,7052],"docker pull nginx:1.27-alpine\ndocker pull registry-1.docker.io\u002Flibrary\u002Fnginx:1.27-alpine      # 上面那行的完整展开\ndocker pull ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker pull localhost:5000\u002Fshop-api:1.8.3 名字被解析成三段：\u003Cregistry>\u002F\u003Cnamespace>\u002F\u003Crepo>:\u003Ctag>。省略 registry 时默认 docker.io，省略 namespace 时默认 library（官方镜像命名空间）。所以 nginx 的真实身份是 docker.io\u002Flibrary\u002Fnginx:latest——不写 tag 就是 latest，这一点在下一节会付出代价。 docker pull 时屏幕上打印的 1.27-alpine: Pulling from library\u002Fnginx 后面跟着一串 Layer ID 的下载进度；同一层已在本地会显示 Already exists（第三章的分层共享）。",{"id":16664,"title":7148,"titles":16665,"content":16666,"level":1202},"\u002Ftutorials\u002Fdocker#tag-不是版本是可变的指针",[5,7052],"事实 后果 tag 可以反复指向不同的镜像（覆盖） 今天的 shop-api:1.8.3 和昨天的可能不是同一个东西 latest 永远指向「最后一次 push 的」 无法回滚、CI 不可复现、A\u002FB 两台机器可能跑不同版本 digest（sha256:...）由内容算出，永不变 唯一可靠的锁定手段 生产规范：用不可变 tag + digest 双锁。 docker pull nginx@sha256:2bc60bb1c9d0c4b7aa220796840eab3d18d613059be13b63bea490dc8a32fc36\ndocker image inspect nginx:1.27-alpine --format '{{index .RepoDigests 0}}'\ndocker run -d --name shop-web nginx:1.27-alpine@sha256:2bc60bb1c9d0... Compose 里同样支持 image: nginx:1.27-alpine@sha256:2bc6...。落地建议：tag 给人读（语义版本 1.8.3、1.8.3-build.20261004），digest 给机器锁（部署清单里记 digest，回滚 = 用旧 digest 再起一次）。 latest 唯一合理的使用场景是本地开发随手试。另一种常见误用是 1.8（大版本 tag）：它会被上游重新指向 1.8.4，你的生产库一夜之间换了小版本。要固定就固定到具体 patch 版本 + digest。",{"id":16668,"title":7281,"titles":16669,"content":16670,"level":1202},"\u002Ftutorials\u002Fdocker#多架构与-manifest",[5,7052],"docker manifest inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker manifest inspect -v nginx:1.27-alpine | grep -A3 '\"platform\"'\ndocker pull --platform linux\u002Farm64 busybox 单个名字背后可以是一个 manifest list（每种架构一个子镜像），docker pull 会按宿主平台自动挑。看不到 manifests 数组说明这是单架构镜像——把它跑在别的架构上就是 exec format error（第七章）。国内机器上 docker manifest inspect 常因 registry 认证\u002F代理失败，加 --insecure 或换 docker buildx imagetools inspect。",{"id":16672,"title":7360,"titles":16673,"content":16674,"level":1202},"\u002Ftutorials\u002Fdocker#私有-registry起一个认证以及-insecure-registries-的取舍",[5,7052],"docker run -d --name registry --restart unless-stopped -p 127.0.0.1:5000:5000 \\\n  -v registry-data:\u002Fvar\u002Flib\u002Fregistry registry:2\ndocker tag shop-api:1.8.3 localhost:5000\u002Fshop-api:1.8.3\ndocker push localhost:5000\u002Fshop-api:1.8.3\ncurl -s http:\u002F\u002Flocalhost:5000\u002Fv2\u002F_catalog 推到 myreg.internal:5000 这种域名时，Docker 要求 HTTPS；没证书就得在 \u002Fetc\u002Fdocker\u002Fdaemon.json 里声明： { \"insecure-registries\": [\"myreg.internal:5000\"] } 改完 systemctl restart docker。insecure-registries 的含义是明文 HTTP 传输：镜像内容与认证 token 都在网上裸奔，只适合封闭内网 + 有物理隔离保证的环境；一旦跨公网就必须上 TLS（用 Caddy\u002Fnginx 反代 + Let's Encrypt，或 registry 自带 tls 配置）。 认证相关： 操作 命令 登录 docker login ghcr.io -U \u003Cuser> --password-stdin（别在命令行里明文写密码，会进 shell history） 登出 docker logout ghcr.io 凭证存放 ~\u002F.docker\u002Fconfig.json 里的 auths；接 pass \u002F secretservice 时用 credsStore CI 里 用 secret 变量喂 --password-stdin，或云厂商的短期 token",{"id":16676,"title":7565,"titles":16677,"content":16678,"level":1202},"\u002Ftutorials\u002Fdocker#镜像加速与代理",[5,7052],"国内机器直连 Docker Hub 经常超时或限速，daemon.json 里配 mirror： {\n  \"registry-mirrors\": [\"https:\u002F\u002Fdocker.m.daocloud.io\"],\n  \"dns\": [\"114.114.114.114\", \"8.8.8.8\"],\n  \"log-driver\": \"json-file\",\n  \"log-opts\": { \"max-size\": \"20m\", \"max-file\": \"3\" }\n} 要点：mirror 只是缓存代理，只加速公开镜像的 pull，push 与私有镜像不走它；配完 docker info 里能看到 Registry Mirrors 才算生效。企业环境走 HTTP 代理用 ~\u002F.docker\u002Fconfig.json 的 proxies（只影响客户端）或 systemd drop-in（影响 daemon，拉镜像真正吃代理的是 daemon）： sudo mkdir -p \u002Fetc\u002Fsystemd\u002Fsystem\u002Fdocker.service.d\nprintf '[Service]\\nEnvironment=\"HTTPS_PROXY=http:\u002F\u002F10.0.0.9:3128\"\\nEnvironment=\"NO_PROXY=localhost,127.0.0.1\"\\n' \\\n  | sudo tee \u002Fetc\u002Fsystemd\u002Fsystem\u002Fdocker.service.d\u002Fproxy.conf\nsudo systemctl daemon-reload && sudo systemctl restart docker",{"id":16680,"title":7752,"titles":16681,"content":16682,"level":1202},"\u002Ftutorials\u002Fdocker#离线分发save-load",[5,7052],"docker save -o shop-images.tar ghcr.io\u002Fexample\u002Fshop-api:1.8.3 postgres:16.4 redis:7.2-alpine\nscp u@build:\u002Ftmp\u002Fshop-images.tar prod:\u002Ftmp\u002F\ndocker load -i \u002Ftmp\u002Fshop-images.tar 内网\u002F气隔环境的常规手段。三个提醒：save 出的 tar 是未压缩的（体积按解压后算），用 docker save img | gzip > img.tar.gz 减一半；tar 绑定架构，别指望 amd64 的 tar 能在 ARM 上跑；load 之后镜像没有 registry 前缀的 RepoDigest，pull_policy 相关行为要注意（第十二章）。",{"id":16684,"title":7833,"titles":16685,"content":16686,"level":1202},"\u002Ftutorials\u002Fdocker#docker-hub-的限流",[5,7052],"未登录 6 小时内每 IP 约 100 次 pull，免费账号 200 次；超了报： toomanyrequests: You have reached your pull rate limit. You may enable and increase limits by upgrading to Team or Enterprise. CI 里最容易撞上。缓解手段按有效性排：把公共基础镜像同步到自己的 registry（最稳，skopeo copy 或 CI 里一次性 mirror）、CI 用云厂商提供的免费 mirror、给构建加缓存层（避免每次重新解析 latest）、docker login（把限额从匿名 IP 池换成账号）。 下一章把「一条一条 docker run」升级成「一个文件描述整套服务」：Compose。",{"id":16688,"title":7867,"titles":16689,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十一docker-compose-入门composeyaml-的骨架",[5],{"id":16691,"title":7871,"titles":16692,"content":16693,"level":1202},"\u002Ftutorials\u002Fdocker#为什么需要-compose",[5,7867],"手工 docker run 起四个服务，你要记住每条命令的 20 个参数、顺序（谁先起）、网络名、卷名，还要在改代码后逐个重建。Compose 把这些写进一个文件：声明式地描述一组服务，然后 up 一下就把状态对齐。它是「单机多容器」的事实标准，也是学 Swarm\u002FK8s 的概念入口。 docker compose version     # v2 是 docker 子命令，注意不是 docker-compose\ndocker compose up -d",{"id":16695,"title":7918,"titles":16696,"content":16697,"level":1202},"\u002Ftutorials\u002Fdocker#顶层结构五个键",[5,7867],"services:            # 必填；每个键是一个容器（或一组副本）\n  web: { image: nginx:1.27-alpine }\nnetworks:            # 自定义网络；不写则自动有一个 \u003Cproject>_default\n  shop-net: { driver: bridge }\nvolumes:             # 命名卷；不写则匿名卷或带项目前缀的自动卷\n  db-data:\nconfigs:             # 文本配置以文件形式注入容器\n  nginx-conf: { file: .\u002Fnginx\u002Fconf.d\u002Fsite.conf }\nsecrets:             # 密钥（Compose 里默认是 file driver）\n  db_password: { file: .\u002Fsecrets\u002Fdb_password.txt } services 之外四个都可选，但生产风格的文件会显式声明 networks 与 volumes，因为隐式资源的名字带项目前缀、难以在外部复用（第十二章的 external）。",{"id":16699,"title":8072,"titles":16700,"content":16701,"level":1202},"\u002Ftutorials\u002Fdocker#service-的常用字段",[5,7867],"字段 含义 关键细节 image 直接用现成镜像 与 build 互斥或共存（共存时 up 用构建产物） build 现场构建 context、dockerfile、args、target、shm_size、pull ports 宿主→容器 列表，\"127.0.0.1:8080:80\" 完全等价于 -p expose 仅文档 只告诉别人「这个端口对内开放」，不产生任何映射 environment 环境变量 map 或 list 两种写法，值必须是字符串（\"true\" 不是 true） env_file 从文件注入 列表按顺序，后者覆盖前者；与 .env 是两回事（第十三章） volumes 挂载 短语法 \"db-data:\u002Fvar\u002Flib\u002Fmysql\" 或长语法 type\u002Ftarget depends_on 启动顺序 只保证顺序，不保证就绪（下一节） healthcheck 健康检查 等价于 HEALTHCHECK，可在 Compose 里覆盖镜像的 restart 重启策略 unless-stopped 是单机默认答案（第十六章） command \u002F entrypoint 覆盖镜像 分别覆盖 CMD \u002F ENTRYPOINT（第四章四组合） network_mode \u002F networks 网络 优先用 networks，network_mode: host 会忽略 ports container_name 固定名字 慎用：与 --scale 冲突，多实例必炸 profiles 按需启动 第十二章",{"id":16703,"title":8348,"titles":16704,"content":16705,"level":1202},"\u002Ftutorials\u002Fdocker#一段完整可用的例子",[5,7867],"services:\n  api:\n    build: { context: ., dockerfile: Dockerfile, args: { NODE_ENV: production } }\n    image: ghcr.io\u002Fexample\u002Fshop-api:1.8.3\n    ports: [\"127.0.0.1:8080:3000\"]\n    environment: { DATABASE_URL: postgres:\u002F\u002Fshop:secret@db:5432\u002Fshop, REDIS_URL: redis:\u002F\u002Fcache:6379\u002F0 }\n    volumes: [uploads:\u002Fsrv\u002Fshop\u002Fuploads]\n    depends_on: { db: { condition: service_healthy }, cache: { condition: service_started } }\n    healthcheck:\n      test: [\"CMD\", \"node\", \"-e\", \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"]\n      interval: 15s\n      retries: 3\n      start_period: 20s\n    restart: unless-stopped\n  web:\n    image: nginx:1.27-alpine\n    ports: [\"80:80\"]\n    volumes: [\".\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro\"]\n    depends_on: [api]\n  db:\n    image: postgres:16.4\n    environment: { POSTGRES_USER: shop, POSTGRES_DB: shop }\n    volumes: [db-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata]\n    healthcheck: { test: [\"CMD-SHELL\", \"pg_isready -U shop\"], interval: 10s, retries: 5 }\n  cache:\n    image: redis:7.2-alpine\n    command: [\"redis-server\", \"--appendonly\", \"yes\"]\n    volumes: [cache-data:\u002Fdata]\nvolumes: { db-data: {}, cache-data: {}, uploads: {} } db 那段里 \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata 这类目标路径必须抄官方文档，不能凭印象。写错成 \u002Fvar\u002Flib\u002Fmysql 时 Compose 不会报错，只会在镜像里建一个空目录，数据静默落在容器可写层，up 看着正常、down 之后全丢——这是教程里最难自查的一类错误。用 docker diff shop-db 或 docker exec shop-db ls \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata 一眼就能看出数据没进卷。",{"id":16707,"title":8837,"titles":16708,"content":16709,"level":1202},"\u002Ftutorials\u002Fdocker#depends_on-只保证顺序不保证就绪",[5,7867],"这是 Compose 最坑人的一个字段。depends_on: [db] 的含义只是「先创建并启动 db 容器」，而容器启动 ≠ 数据库能接受连接。Postgres 初始化要几秒，你的 API 在此期间 ECONNREFUSED，然后崩溃退出。 正确写法是 depends_on + condition，而 condition 依赖健康检查： depends_on:\n      db: { condition: service_healthy }        # 等 db 的 healthcheck 通过才起 api\n      cache: { condition: service_started }     # 只要起来了（等价于旧写法） 可选值有三种：service_started（默认）、service_healthy（要求被依赖方有 healthcheck，否则 Compose 报错）、service_completed_successfully（等对方退出码 0，适合 migration\u002Finit 容器）。 condition 解决的是「应用启动时的瞬时失败」，但运行期依赖方挂了依然要你自己的重连逻辑。别指望 depends_on 提供高可用。",{"id":16711,"title":8940,"titles":16712,"content":16713,"level":1202},"\u002Ftutorials\u002Fdocker#日常命令与那个危险的-v",[5,7867],"docker compose config            # 校验并打印合并渲染后的最终配置（排错第一步）\ndocker compose up -d             # 对齐期望状态：缺的建、变的换\ndocker compose up -d --build     # 顺带重新构建 build 的服务\ndocker compose up -d --force-recreate api   # 配置没变但想换镜像\u002F环境变量时强制重建\ndocker compose ps                # 只看本项目的容器（含 health 状态）\ndocker compose logs -f --tail 100 api\ndocker compose exec api sh       # 进容器（exec 不是 run，不会新建）\ndocker compose run --rm api node scripts\u002Fmigrate.js   # 起一次性容器，可覆盖 command\ndocker compose scale api=3\ndocker compose stop \u002F start \u002F restart\ndocker compose down              # 停并删容器和网络（卷和数据不动）\ndocker compose down -v          # 连命名卷一起删 —— 数据库清空 down -v 会删除本项目声明的所有命名卷，不可恢复。习惯是永远写 down，要清数据时明确地用 docker volume rm \u003C精确名字>。 up 的变更判定很保守：只改镜像 tag、只改 environment 的值都会重建容器；只改 ports 的宿主侧也一样。但它不会因为 image 内容变了（同 tag 重新 push）自动重建——这时要 --pull always + --force-recreate。 config 是理解 Compose 的第一工具：插值、多文件叠加、锚点展开的最终结果都在它输出里，出问题先看它。 项目名决定资源前缀（shop_api_1、shop_db-data）；来源优先级是 -p\u002F--project-name > COMPOSE_PROJECT_NAME > 顶层 name: > 当前目录名。同一个目录不同路径跑起来会变成两个项目，这是「明明 up 了但 ps 看不到」的常见原因。 示例 11 是 up -d 的完整输出流：从网络创建、卷创建、拉镜像、按依赖顺序起四个容器，到 ps 里 health: starting 转 healthy 的全过程。 一个文件能描述服务，但还不够「生产」。下一章处理多环境、变量、锚点与那些必须显式声明的开关。",{"id":16715,"title":9231,"titles":16716,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十二compose-进阶变量插值多文件叠加与生产风格配置",[5],{"id":16718,"title":9235,"titles":16719,"content":16720,"level":1202},"\u002Ftutorials\u002Fdocker#变量与-env插值语法",[5,9231],"services:\n  api:\n    image: ghcr.io\u002Fexample\u002Fshop-api:${APP_VERSION:-1.8.3}\n    ports:\n      - \"${HTTP_PORT:-8080}:3000\"\n    environment:\n      DATABASE_URL: postgres:\u002F\u002F${DB_USER:-shop}:${DB_PASSWORD:?set DB_PASSWORD in .env}@db:5432\u002Fshop\n      REPLICAS: ${SHARD_COUNT} 语法 行为 ${VAR} 未定义时替换为空字符串 ${VAR:-默认} 未定义或为空 → 用默认（值不进容器，只在 Compose 层展开） ${VAR-default} 仅未定义时用默认（空串保留） ${VAR:?错误信息} 未定义或为空 → 直接报错退出（强制必填，很好的守卫） ${VAR?错误信息} 仅未定义时报错 ${VAR:?} 与 $$ $$ 是转义，输出一个 $（防止应用密钥里带 $ 被二次展开） .env（与 compose.yaml 同目录）里的值供给上面的插值，作用范围是整个 Compose 文件；env_file: 注入的是容器内环境变量，不参与文件插值。两者都叫「环境变量」但层次完全不同，这是第十三章的第一条主线。 插值发生在 Compose 层，因此 docker compose config 输出里你看到的一定是展开后的值——检查变量到底吃到没有，看 config。",{"id":16722,"title":9428,"titles":16723,"content":16724,"level":1202},"\u002Ftutorials\u002Fdocker#多文件叠加base-override",[5,9231],"docker compose -f compose.yaml -f compose.prod.yaml up -d\ndocker compose -f compose.yaml -f compose.override.yaml config   # 看合并结果 同名文件自动叠加规则（Compose v2）：compose.yaml + compose.override.yaml（开发用，自动加载）；显式 -f 时按给定顺序合并，后者覆盖前者。合并语义很关键： 类型 合并规则 标量（image、restart） 后面的直接覆盖 environment \u002F labels 按键合并，同名覆盖 ports \u002F volumes \u002F command 等列表 一般整体替换（不是追加！）——想追加要写全 build.args 按键合并 healthcheck 子字段逐个覆盖；disable: true 可关掉 deploy 深度合并 「列表是替换不是追加」这点最容易被误解：override 文件里写一条 ports 并不会给 base 加一个端口，而是替换掉整个 ports 列表。 典型分层：compose.yaml（公共）+ compose.override.yaml（本地开发：bind mount 源码、开调试端口、command 换成 nodemon）+ compose.prod.yaml（生产：镜像 digest、restart、资源限制、只挂卷）。把 compose.override.yaml 加进 .gitignore 或不提交，避免开发者机器上的配置跑到生产。",{"id":16726,"title":9621,"titles":16727,"content":16728,"level":1202},"\u002Ftutorials\u002Fdocker#扩展字段与锚点复用又不产生隐式行为",[5,9231],"x-common: &common\n  restart: unless-stopped\n  logging: { driver: json-file, options: { max-size: \"20m\", max-file: \"3\" } }\n  security_opt: [\"no-new-privileges:true\"]\n  networks: [shop-net]\n\nservices:\n  api:\n    \u003C\u003C: *common\n    environment: { TZ: Asia\u002FShanghai, NODE_ENV: production, PORT: \"3000\" }\n  cache:\n    \u003C\u003C: *common\n    image: redis:7.2-alpine x- 前缀的顶层键是扩展字段，Compose 忽略它，但允许在里面放可复用片段；&anchor 定义锚点、*alias 引用、\u003C\u003C: 做映射合并。收益是少抄 20 行，代价是改一处会同时影响多个服务（生产上这常常正是你想要的），以及覆盖顺序的心智负担（\u003C\u003C: 中已显式写出的键优先）。锚点是 YAML 特性，别指望它做复杂逻辑——真要参数化就换 envsubst 模板或 Helm。",{"id":16730,"title":9834,"titles":16731,"content":16732,"level":1202},"\u002Ftutorials\u002Fdocker#profiles按需启动",[5,9231],"services:\n  adminer: { image: adminer:4, profiles: [\"debug\"] }\n  migrate: { build: ., command: [\"node\", \"scripts\u002Fmigrate.js\"], profiles: [\"ops\"] } docker compose --profile debug up -d\ndocker compose --profile ops run migrate 没有 profile 的服务总是启动；有 profile 的只有匹配 --profile 才启动。用途是把「调试面板、一次性任务、测试桩」从常规 up 里摘出去，而不是靠注释代码。",{"id":16734,"title":9957,"titles":16735,"content":16736,"level":1202},"\u002Ftutorials\u002Fdocker#网络与外部资源",[5,9231],"networks:\n  shop-net:\n    name: shop-net           # 显式命名，不带项目前缀，便于项目外复用\n  edge:\n    external: true           # 引用已存在的网络，up\u002Fdown 都不许删它\n  db-only:\n    internal: true           # 出不去外网，只能容器间互访 默认网络（\u003Cproject>_default）由 Compose 自动创建与销毁，服务默认全接进去、互相可按名字访问。显式自定义网络的好处是：能用 name: 固定名字给项目外用、能 internal、能用多张网做隔离（前端接 edge、数据库只接 db-only，于是 web 根本 ping 不到 db——网络层强制了架构分层）。 external: true 把资源和容器生命周期解耦，down 不会删它。生产里数据库卷推荐显式声明（带 name: 或 external），防止换项目名（换目录）后「数据凭空消失」（其实还在旧卷里，这类事故很常见）。",{"id":16738,"title":10070,"titles":16739,"content":16740,"level":1202},"\u002Ftutorials\u002Fdocker#那些必须显式写的开关",[5,9231],"字段 作用 为什么要显式写 pull_policy missing（默认）\u002F always \u002F build \u002F never 同 tag 覆盖镜像时，always 才能确保拉到新的；离线环境用 never init: true 容器里跑 tini 当 PID 1 否则僵尸进程不收尸、SIGTERM 转发有问题（第十四、十六章） stop_grace_period: 30s SIGTERM 后等多久发 SIGKILL 默认 10s；长任务\u002F大流量服务要加长 shm_size: 256m \u002Fdev\u002Fshm 大小 默认 64MB，Chrome\u002FPuppeteer、Postgres 并行扫描会撑爆并报 No space left on device ulimits nofile: 65536、nproc 宿主默认 1024 文件描述符，高并发必调 extra_hosts host.docker.internal:host-gateway Linux 上访问宿主服务的前提（第八章） readonly: true + tmpfs 根文件系统只读 第十五章安全基线 deploy.resources.limits memory: 512M、cpus: \"1.5\" Compose v2 在单机 up 时也会应用 limits（K8s 迁移友好，但 replicas 等仍需 Swarm） logging max-size \u002F driver 默认 json-file 不轮转，磁盘必爆（第十六章）",{"id":16742,"title":10254,"titles":16743,"content":16744,"level":1202},"\u002Ftutorials\u002Fdocker#一个生产风格的完整例子",[5,9231],"name: shop\nx-service: &service\n  restart: unless-stopped\n  init: true\n  stop_grace_period: 30s\n  security_opt: [\"no-new-privileges:true\"]\n  cap_drop: [\"ALL\"]\n  read_only: true\n  tmpfs: [\"\u002Ftmp\"]\n  logging: { driver: json-file, options: { max-size: \"20m\", max-file: \"3\" } }\n  ulimits: { nofile: 65536 }\n  networks: [shop-net]\nservices:\n  web:\n    \u003C\u003C: *service\n    image: nginx:1.27-alpine@sha256:2bc60bb1c9d0\n    ports: [\"80:80\", \"127.0.0.1:443:443\"]\n    volumes: [\".\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro\", \".\u002Fnginx\u002Fcerts:\u002Fetc\u002Fnginx\u002Fcerts:ro\"]\n    depends_on: { api: { condition: service_healthy } }\n  api:\n    \u003C\u003C: *service\n    image: ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0ea7b2d4\n    expose: [\"3000\"]\n    environment: { NODE_ENV: production, TZ: Asia\u002FShanghai }\n    configs: [{ source: app-conf, target: \u002Fetc\u002Fshop\u002Fconfig.yaml }]\n    secrets: [database_url]\n    healthcheck:\n      test: [\"CMD\", \"node\", \"-e\", \"require('http').get('http:\u002F\u002F127.0.0.1:3000\u002Fhealthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))\"]\n      interval: 15s\n      timeout: 3s\n      retries: 3\n      start_period: 20s\n    deploy: { resources: { limits: { memory: 512M, cpus: \"1.5\" } } }\n    depends_on: { db: { condition: service_healthy }, cache: { condition: service_started } }\n  db:\n    \u003C\u003C: *service\n    image: postgres:16.4@sha256:6d7df8c5b0f8\n    environment: { POSTGRES_PASSWORD_FILE: \u002Frun\u002Fsecrets\u002Fdb_password }\n    volumes: [db-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata]\n    secrets: [db_password]\n    networks: [shop-net, db-only]\n    healthcheck: { test: [\"CMD-SHELL\", \"pg_isready -U shop\"], interval: 10s, retries: 5 }\n  cache:\n    \u003C\u003C: *service\n    image: redis:7.2-alpine@sha256:1a2b3c4d5e6f\n    command: [\"redis-server\", \"--appendonly\", \"yes\", \"--maxmemory\", \"256mb\"]\n    volumes: [cache-data:\u002Fdata]\n    healthcheck: { test: [\"CMD\", \"redis-cli\", \"ping\"], interval: 10s, retries: 3 }\nnetworks:\n  shop-net: {}\n  db-only: { internal: true }\nvolumes: { db-data: {}, cache-data: {} }\nconfigs: { app-conf: { file: .\u002Fconfig\u002Fapp.prod.yaml } }\nsecrets:\n  db_password: { file: .\u002Fsecrets\u002Fdb_password.txt }\n  database_url: { file: .\u002Fsecrets\u002Fdatabase_url.txt } 这份配置里每一条都能在前面章节找到理由：digest 锁版本（第十章）、expose 而非 ports 让 api 不对外（第八章）、internal 网络隔离数据库、read_only + tmpfs（第十五章）、init（本章）、healthcheck + service_healthy（第十一章）、日志轮转（第十六章）、密钥走 secrets 文件（下一章主题）。 下一章就专门讲这条线：配置与密钥怎么进容器。",{"id":16746,"title":11087,"titles":16747,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十三配置与密钥优先级secrets-与别把配置打进镜像",[5],{"id":16749,"title":11090,"titles":16750,"content":16751,"level":1202},"\u002Ftutorials\u002Fdocker#环境变量注入的四条路径与优先级",[5,11087],"同一个变量在四个地方都能写，最终生效的是优先级最高的那个： printf 'LOG_LEVEL=debug\\nPORT=3000\\n' > .env.runtime\ndocker run --rm -e LOG_LEVEL=warn --env-file .env.runtime myimg:1.0 printenv LOG_LEVEL 优先级（低 → 高） 来源 特点 1 Dockerfile ENV 进镜像，所有环境共享，只能被覆盖不能删 2 --env-file \u002F Compose env_file 文件里的值；Compose 中多个文件后者覆盖前者 3 -e KEY=VAL \u002F Compose environment 命令行级，覆盖 env_file 4 Compose environment 里的 ${VAR} 插值 + 宿主真实环境 .env 只做文本展开，不影响上面的覆盖次序 一句话记忆：environment 压过 env_file，env_file 压过镜像 ENV。而 Compose 的 .env（插值用）和 env_file（注入容器用）根本不在同一层——.env 里的 APP_VERSION 只会出现在 yaml 里，不会自动进容器。这是新手最常见的「我在 .env 写了怎么容器里读不到」。 验证永远是同一条命令：docker exec shop-api printenv，或 docker inspect --format '{{json .Config.Env}}'。",{"id":16753,"title":11260,"titles":16754,"content":16755,"level":1202},"\u002Ftutorials\u002Fdocker#_12-factor-在容器里的落地",[5,11087],"原则 容器里的做法 反例 配置存环境 环境变量 \u002F secrets 文件 config.prod.yaml 打进镜像 日志到标准输出 不写日志文件，交给驱动与采集 容器内 \u002Fvar\u002Flog\u002Fapp\u002F*.log（docker logs 全空） 一个进程一个容器 web\u002Fapi\u002Fworker 分开 一个容器跑 nginx + php-fpm + cron 快速启动与优雅停机 就绪探针 + SIGTERM 处理 启动要跑 30 秒初始化脚本 无状态 状态放卷\u002F对象存储\u002FDB session 存进程内存（扩容即掉线） 构建一次多环境跑 镜像不变，只换环境变量 生产用「生产版 Dockerfile」重新构建 核心是最后一条：镜像不可变，环境差异全靠注入。这也回答了「为什么不要把配置 COPY 进镜像」——一旦 COPY，每个环境都要构建一个镜像，缓存、回滚、审计全部翻倍；而且配置文件里通常藏着连接串和密钥，镜像一旦被 pull 就等于泄密（第十五章演示 docker history 捞密钥）。",{"id":16757,"title":11366,"titles":16758,"content":16759,"level":1202},"\u002Ftutorials\u002Fdocker#secretsswarm-原生-vs-compose-的-file-driver",[5,11087],"secrets:\n  db_password:\n    file: .\u002Fsecrets\u002Fdb_password.txt     # file driver：本质是把这个文件挂到 \u002Frun\u002Fsecrets\u002Fdb_password\n  jwt_key:\n    source: jwt_key                      # 外部已存在的 secret（Swarm 或外部管理） 必须知道的区别：非 Swarm 环境下 Compose 的 secret 就是 bind mount 一个文件，落盘、可读、宿主上明文存在；只有 Swarm 才把它放进内存（tmpfs）并加密分发。docker secret create 在非 Swarm 节点会报 This node is not a swarm manager。所以「用了 secrets 就安全」是不成立的——真正的收益是约定路径（\u002Frun\u002Fsecrets\u002F\u003Cname>）与应用用 *_FILE 变量读（如 POSTGRES_PASSWORD_FILE），让密钥不进环境变量列表（docker inspect、printenv、\u002Fproc\u002F1\u002Fenviron 都能读到环境变量，而 secret 文件需要能访问挂载点才行）。 临时与内存型的配置用 tmpfs（第九章）：--mount type=tmpfs,target=\u002Frun\u002Fsecrets,tmpfs-size=1m。",{"id":16761,"title":11467,"titles":16762,"content":16763,"level":1202},"\u002Ftutorials\u002Fdocker#挂载配置目录-只读以及-gitignore",[5,11087],"docker run -d --name shop-web -v \u002Fsrv\u002Fshop\u002Fnginx\u002Fconf.d:\u002Fetc\u002Fnginx\u002Fconf.d:ro nginx:1.27-alpine ro 不是可选的礼貌：配置文件被容器改坏（某些镜像会在启动脚本里 envsubst 写回 \u002Fetc\u002F...）时，只读挂载能让失败在第一次写就暴露，而不是悄悄污染宿主。宿主机上把配置放在 \u002Fsrv\u002F\u003Cproject>\u002Fconfig，用 chmod 0640 root:shop 收紧，容器以 --group-add 或 --user 匹配读组。 .gitignore 里和容器相关的必备项： .env\n.env.*\n!.env.example\nsecrets\u002F\n*.pem\n*.key\ndocker-compose.override.yaml\ncompose.override.yaml\n\u002Fdata\u002F .env.example（只有键名和假值）应该提交，它是给同事的「配置契约」。",{"id":16765,"title":11537,"titles":16766,"content":16767,"level":1202},"\u002Ftutorials\u002Fdocker#模板化配置一行-envsubst",[5,11087],"配置里需要变量替换时（nginx upstream、数据库连接串），把模板留在镜像里、启动时渲染，是 12-factor 与「不重新构建」的折中： envsubst '${DB_HOST} ${DB_PORT}' \u003C \u002Fetc\u002Fshop\u002Fapp.yaml.tpl > \u002Fetc\u002Fshop\u002Fapp.yaml 官方 nginx:1.27-alpine 已经内置这个机制（\u002Fdocker-entrypoint.envsubst.sh 目录约定）。两个坑：envsubst 不带参数会替换所有 $VAR（把应用里的 $2、shell 变量一起吃掉），所以一定限定变量名单；渲染目标是 bind mount 来的路径时会因 :ro 失败——那种情况请挂模板目录、输出到容器内可写路径或 tmpfs（第九章的 diff 可以帮你找出所有需要可写的路径）。",{"id":16769,"title":11599,"titles":16770,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十四资源限制与性能内存cpu-与-137-的定位路径",[5],{"id":16772,"title":16370,"titles":16773,"content":16774,"level":1202},"\u002Ftutorials\u002Fdocker#内存-memory-与-memory-swap-的语义陷阱",[5,11599],"写法 含义 -m 512m（不写 swap） 内存 512m + 等量 swap（实际默认 swap 上限 = 2× 内存，即总 1024m） -m 512m --memory-swap 512m 禁用 swap（总可用就是 512m） -m 512m --memory-swap 768m 512m 内存 + 256m swap -m 512m --memory-swap -1 swap 不限（危险，磁盘 IO 可能拖垮宿主） --memory-reservation 256m 软下限，内存紧张时才尝试回收 --memory-swap 是总量（内存 + swap），不是 swap 量——这个反直觉定义让「写 0」有特殊意义：--memory-swap 0 无效，想禁 swap 要写成与 -m 相等。生产建议显式禁 swap（--memory-swap 等于 --memory），因为被换出的容器进程会出现「没被 OOM 但响应巨慢」的诡异状态，比直接 OOM 更难诊断。 cgroup v1 与 v2 的差异要知道：v2 下没有独立的 memory.memsw.limit_in_bytes，Docker 用 memory.swap.max 表示「swap 部分」，同样的参数在不同机器上落点不同（docker info 看 Cgroup Version）。",{"id":16776,"title":11722,"titles":16777,"content":16778,"level":1202},"\u002Ftutorials\u002Fdocker#cpu-与进程数",[5,11599],"docker run -d --cpus 1.5 --cpuset-cpus 2,3 --cpu-shares 512 --pids-limit 256 shop-api 参数 机制 要点 --cpus 1.5 cgroup quota（cpu.max） 硬上限；1.5 = 每秒最多用 1.5 核时间。写 0.5 表示半核 --cpu-shares（默认 1024） 权重 只在 CPU 争抢时生效；空闲时容器能用满宿主。别把它当上限 --cpuset-cpus 绑核 减少调度迁移与 NUMA 跨节点，低延迟场景用 --pids-limit 进程\u002F线程总数 防 fork 炸弹打爆宿主（默认 unlimited 是个隐患） --cpus 与 --cpu-shares 的区别是「天花板」和「分蛋糕比例」——把后者当限制用的人，会发现 docker stats 里 CPU 偶尔 800%，那不是 bug。",{"id":16780,"title":11857,"titles":16781,"content":16782,"level":1202},"\u002Ftutorials\u002Fdocker#oom-与-exit-137-的定位路径",[5,11599],"看到 Exited (137) 时的四步（顺序不能乱）： docker inspect shop-api --format 'oom={{.State.OOMKilled}} exit={{.State.ExitCode}}'\ndocker stats --no-stream shop-api                   # 看 LIMIT 是不是本来就设小了\ndmesg -T | grep -i -E 'oom|killed process' | tail   # 或 journalctl -k\ndocker events --since 2h --filter event=die OOMKilled=true → 内存配额不够或应用泄漏，调 -m 或修应用；OOMKilled=false 而码是 137 → 有人（或脚本、docker kill、stop 超时）发了 SIGKILL，用 docker events 找时间点。注意内核 OOM 也可能杀宿主进程：没设 -m 的容器可以一路吃到宿主内存，最后 OOM killer 挑了个 sshd——这就是「慎用 --oom-kill-disable」的理由：它让容器在内存不足时不可被杀，于是内核去杀邻居。",{"id":16784,"title":11972,"titles":16785,"content":16786,"level":1202},"\u002Ftutorials\u002Fdocker#容器里看到的免费内存核数是宿主的",[5,11599],"docker run -it --rm --cpus 1 --memory 256m node:22-bookworm-slim sh\n\u002Fsrv\u002Fshop $ free -h          # 显示宿主总内存\n\u002Fsrv\u002Fshop $ nproc            # 显示宿主核数\n\u002Fsrv\u002Fshop $ cat \u002Fsys\u002Ffs\u002Fcgroup\u002Fmemory.max      # 268435456  ← 真实上限在这\n\u002Fsrv\u002Fshop $ cat \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.max          # 100000 25000 ← quota\u002Fperiod = 4 核？其实是 1 核 原因是 \u002Fproc 与 free 读的是全局视图，namespace 没隔离它们（lxcfs 就是专门伪造这些文件的方案，K8s 环境偶尔引入）。所以：依赖 os.totalmem()、Runtime.availableProcessors() 的库在容器里都会算错。 JVM：新版默认按 cgroup 限制算堆（-XX:+UseContainerSupport），显式配 -XX:MaxRAMPercentage=70.0 比写死 -Xmx 更可移植；老 8u 之前的版本会按宿主内存开堆，然后被 OOM Kill。 Node：--max-old-space-size 要小于容器内存（留 20–25% 给 Buffer\u002F线程栈），否则 V8 觉得还有空间、内核先动手；uv.defaultConcurrency、os.cpus().length 决定 worker\u002F线程池大小，按 --cpus 自己算而不是读 cpus()。 Go：GOMAXPROCS 默认读宿主核数，CPU 限额 0.5 核时会出现大量线程被 throttle，用 automaxprocs 或显式设。",{"id":16788,"title":12140,"titles":16789,"content":16790,"level":1202},"\u002Ftutorials\u002Fdocker#清理与镜像瘦身prune-的杀伤范围",[5,11599],"docker system df                 # 先看账本，永远先跑这个\ndocker container prune           # 只删已停止容器\ndocker image prune               # 只删悬空镜像（无 tag 的）\ndocker image prune -a            # 删所有「没有被任何容器使用」的镜像 —— 会拉回重下\ndocker builder prune             # 只清构建缓存（BuildKit），层缓存没了但镜像还在\ndocker system prune              # 上述前三类的组合\ndocker system prune -a --volumes  # 加上所有未使用卷 —— 数据库没了 最后一行是 Docker 世界里的 rm -rf：--volumes 会删掉所有当前没被容器引用的命名卷，包括你昨天测试留下的生产库副本（第九章匿名卷那段）。安全替代： docker image prune -a --filter \"until=336h\"     # 只清 7 天前的，留回滚余地\ndocker ps -a --filter status=exited --format '{{.Names}}'   # 先列清单再决定\ndocker volume ls --filter name=shop              # 精确定位，然后 volume rm 具体名字 镜像体积影响的不只是磁盘：容器首次启动要等层下载+解压完成（第三章），1GB 与 80MB 的镜像在扩缩容、故障重调度时的恢复时间差一个量级。多阶段构建（第五章）+ slim\u002Falpine 基础镜像 + 把 RUN 链起来减少层数（第四章的 rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*），三招通常能砍掉 70%。 示例 14 演示一次完整的定位：容器反复 Exited (137)，从 inspect 到 stats 到内核日志，最后落到 JVM 堆参数与 -m 的错配。",{"id":16792,"title":12344,"titles":16793,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十五安全实践非-root能力裁剪只读根文件系统与扫描",[5],{"id":16795,"title":12348,"titles":16796,"content":16797,"level":1202},"\u002Ftutorials\u002Fdocker#为什么默认是-rootrootless-能不能真解决",[5,12344],"镜像默认以 root 跑是历史决定：装包、写系统目录、绑 80 端口都方便。后果是应用一旦被 RCE，攻击者拿到的是容器内的 root——它可以调用镜像允许的所有 capability、访问所有挂载。 关键认知：容器内 root ≠ 宿主 root，但也绝不是安全的。能不能逃逸取决于能力集、seccomp、挂载了什么。rootless 模式（第二章）把「逃逸后拿到的身份」从 root 降级成普通用户，这是真实的收益；但它不改变容器内应用仍以自己的 UID 拥有全部权限这一事实，也不解决 secret 泄漏与网络暴露。两者是不同层次的缓解，都要做。",{"id":16799,"title":12370,"titles":16800,"content":16801,"level":1202},"\u002Ftutorials\u002Fdocker#dockerfile-里的-user-与非-root-镜像",[5,12344],"FROM node:22-bookworm-slim\nRUN groupadd --gid 1001 app \\\n    && useradd --uid 1001 --gid app --shell \u002Fusr\u002Fsbin\u002Fnologin --home-dir \u002Fsrv\u002Fshop --no-create-home app\nWORKDIR \u002Fsrv\u002Fshop\nCOPY --chown=1001:1001 . .\nUSER 1001:1001 要点： 用数字 USER 1001:1001，别写 USER app——写名字时依赖镜像里恰好有那个用户，基础镜像一换就 unable to find user app: no matching entries in passwd file（Exited (127) 家族）。 Alpine 系是 addgroup -g 1001 app && adduser -S -D -u 1001 -G app app，参数名与 Debian 完全不同，抄错就构建失败。 先装东西、再切用户（第四章）。切完之后 RUN apt-get 必然权限不足；需要写目录时提前 chown 或 COPY --chown，别在运行时用 root 去修。 现成的非 root 镜像更省事：node 自带 node 用户（UID 1000）、nginxinc\u002Fnginx-unprivileged:1.27-alpine 直接听 8080。",{"id":16803,"title":12464,"titles":16804,"content":16805,"level":1202},"\u002Ftutorials\u002Fdocker#能力裁剪与安全选项",[5,12344],"docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE --security-opt no-new-privileges:true shop-api:1.8.3 选项 作用 说明 --cap-drop=ALL 去掉所有 Linux capability 默认容器给了约 14 个（含 CHOWN、NET_RAW、SETUID…），够用但偏宽 --cap-add=NET_BIND_SERVICE 单独加回「绑 1024 以下端口」 需要 80\u002F443 时唯一该加的；更优做法是监听高位端口由反代转发 --security-opt=no-new-privileges 禁止 setuid 提权 挡住 sudo\u002FSUID 程序这条最常见路径，成本几乎为零，建议默认开 --security-opt=seccomp=default 系统调用白名单 默认档已屏蔽 ~44 个危险调用；个别应用（老 Docker 里的 pthreads、JVM 某些诊断）需要自定义 profile --security-opt=apparmor=docker-default LSM 强制访问控制 有 AppArmor 的发行版默认启用；SELinux 环境用 -v 加 :z\u002F:Z --privileged 放开一切 加全部 capability、关 seccomp 与 AppArmor、能访问所有设备。永远不要为了「让它跑起来」而加它，那是把宿主双手交出",{"id":16807,"title":12625,"titles":16808,"content":16809,"level":1202},"\u002Ftutorials\u002Fdocker#只读根文件系统与最小写入面",[5,12344],"docker run --rm --read-only --tmpfs \u002Ftmp --tmpfs \u002Frun --user 1001:1001 shop-api:1.8.3 --read-only 让「写文件」这件事只在显式挂载的路径上可能，等于把入侵者的持久化路径（放马、改二进制、写 crontab）砍掉。落地顺序很实际：先用 docker diff + 一段试运行收集所有写入路径（第九章），再逐条给 --tmpfs 或卷，最后打开 read_only: true（第十二章）。",{"id":16811,"title":12680,"titles":16812,"content":16813,"level":1202},"\u002Ftutorials\u002Fdocker#镜像扫描与最小基础镜像",[5,12344],"trivy image --severity HIGH,CRITICAL ghcr.io\u002Fexample\u002Fshop-api:1.8.3\ndocker scout quickview nginx:1.27-alpine        # Docker Desktop\u002FHub 内置\ndocker image inspect ghcr.io\u002Fexample\u002Fshop-api:1.8.3 --format '{{.Created}} {{len .RootFS.Layers}}' 扫描解决「已知 CVE」，不解决「未知漏洞」与配置错误；把 trivy 放进 CI 当门（--exit-code 1），比事后在服务器上发现强。基础镜像取舍： 选择 体积 好处 代价 *-bookworm-slim 中 glibc 兼容、包全、CVE 少 仍有 apt 与 shell，调试方便 alpine 小 极小、apk 快 musl 而非 glibc：需要 glibc 的二进制直接失败；预编译 wheel 可能退回源码编译；历史上 musl 的 DNS（线程栈、getaddrinfo）行为与 glibc 有差异，出现「只有生产偶发解析超时」 distroless 很小 无 shell、无包管理器，攻击面最小 无法 exec 调试（第七章的方案全换：用 docker run --rm --entrypoint 或调试专用镜像、nsenter） scratch 最小 只有你的二进制 要自己处理 CA 证书、时区文件、DNS（静态编译才可行） glibc vs musl 的坑在 Python（numpy\u002Fpsycopg 类需编译的包）和任何自带 .so 的 SDK 上出现最多。判断办法：docker exec c ldd \u002Fusr\u002Flocal\u002Fbin\u002Fapp 看链接的是 libc.musl* 还是 libc.so.6。",{"id":16815,"title":12863,"titles":16816,"content":16817,"level":1202},"\u002Ftutorials\u002Fdocker#密钥泄漏为什么-arg-env-run-都不行",[5,12344],"ARG TOKEN\nRUN curl -H \"Authorization: token ${TOKEN}\" https:\u002F\u002Fapi.github.com\u002F...   # 错误示范\nENV DB_PASSWORD=hunter2                                                   # 错误示范\nRUN git clone https:\u002F\u002Fuser:password@github.com\u002Fexample\u002Fprivate.git        # 错误示范\nRUN npm ci --token=${NPM_TOKEN} && rm -rf \u002Froot\u002F.npmrc                    # rm 也救不了 三条都能被捞出来：docker history --no-trunc 直接打印指令文本；docker save 后的层 tar 里文件还在（写时复制：rm 只是遮住）；镜像被 pull 走后你无从审计。「后面删掉」不是修复，因为它只是在上层放了 whiteout。正确做法只有两条：构建期用 --mount=type=secret（第五章），运行期注入（第十三章）。同理，COPY 一个带 .git 的目录会把整个提交历史（含密钥）留在镜像里——.dockerignore 加 .git。 其余几条硬规则： --init \u002F init: true：PID 1 负责收尸，否则僵尸进程累积到 --pids-limit 就起新进程失败。 --pid=container:x \u002F --ipc=container:x 共享：共享 PID 意味着能看见甚至影响邻居的进程（kill、读 \u002Fproc\u002F*\u002Fenviron 里的密钥）；共享 IPC 意味着能读写对方共享内存。除非明确需要（如 sidecar 调试），不要共享。 挂载 \u002Fvar\u002Frun\u002Fdocker.sock 等于交出宿主 root：容器里一句 docker run -v \u002F:\u002Fhost alpine chroot \u002Fhost 就能拿到宿主。真需要「容器里管容器」（CI、Portainer、watchtower），用只读 socket 代理（tecnativa\u002Fdocker-socket-proxy）白名单最小 API，或者用 rootless daemon。",{"id":16819,"title":12979,"titles":16820,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十六生产运行重启策略健康检查优雅停机与备份",[5],{"id":16822,"title":16386,"titles":16823,"content":16824,"level":1202},"\u002Ftutorials\u002Fdocker#restart-四种策略怎么选",[5,12979],"策略 daemon 重启后 应用崩溃 你手动 docker stop 后 no（默认） 不拉起 不拉起 不拉起 on-failure:5 拉起 拉起，最多 5 次 不拉起 unless-stopped 拉起（除非它此前是被手动停的） 拉起 不拉起 always 拉起 拉起 也会拉起（daemon 重启时） 单机生产的答案基本都是 unless-stopped：它尊重你的「我就是要停掉它」这个意图，always 则会在你重启 Docker 之后把已经手动停掉的容器又拉起来（升级维护时这会很难堪）。on-failure 适合一次性任务与迁移脚本（退出码非 0 才重试）。 要注意的盲区：--restart 不处理 unhealthy。健康检查连续失败只会让状态变成 (unhealthy)，容器照跑（第六章）。自愈要么靠应用自己崩溃退出（配合 restart），要么外部加 watcher，要么上编排（第十七章）。",{"id":16826,"title":13103,"titles":16827,"content":16828,"level":1202},"\u002Ftutorials\u002Fdocker#healthcheck-与就绪判断",[5,12979],"docker inspect --format '{{.State.Health.Status}}' shop-api\ndocker inspect --format '{{json .State.Health}}' shop-api | jq '.Log[-3:]'   # 最近三次检查记录 HEALTHCHECK 的四个参数各自管什么：--interval 检查频率、--timeout 单次超时、--retries 连续失败几次算 unhealthy、--start-period 启动宽限期（期间失败不计、状态是 health: starting）。生产最常见的事故是没设 start-period：应用冷启动要 25 秒，前两次检查失败被判 unhealthy，编排层不停重建，永远起不来。 inspect 的 .State.Health.Log 里有每次执行的 Exit、Output、Start\u002FEnd，这是判断「到底是超时还是返回码不对」的唯一依据。另外 Compose 的 condition: service_healthy（第十一章）依赖它。",{"id":16830,"title":13201,"titles":16831,"content":16832,"level":1202},"\u002Ftutorials\u002Fdocker#优雅停机信号怎么传pid-1-问题与解法",[5,12979],"docker stop 的完整时序是：发 SIGTERM → 等 --stop-timeout（默认 10s）→ 还没退出就 SIGKILL。应用要在窗口内做完：停止接受新连接 → 等手头请求结束 → 关连接池 → flush 日志 → exit 0。 失效的原因在第四章埋的伏笔： CMD [\"sh\", \"-c\", \"node app.js\"]      # PID 1 = sh，node 是它的子进程 sh -c 起的子进程不是 PID 1，而 shell 默认不转发收到的 SIGTERM（它只等子进程结束），于是：容器收到信号 → sh 无动作 → 10 秒后 Docker 发 SIGKILL → node 被硬杀 → 退出码 137、请求被截断、事务未提交。同样地，shell 形式 CMD node app.js（不带 JSON 数组）也是经 \u002Fbin\u002Fsh -c 执行，症状一致。 三种解法： CMD [\"node\", \"app.js\"]                 # ① exec 形式：node 就是 PID 1\nENTRYPOINT [\"tini\", \"--\"]              # ② 装 tini（或直接 --init）\nCMD [\"node\", \"src\u002Fserver.js\"] docker run -d --init --stop-timeout 30 shop-api:1.8.3    # ③ --init 由 Docker 注入 tini exec 形式最省事，但要求应用自己处理 SIGTERM（Node：process.on('SIGTERM', …)；Python：signal.signal；Go：signal.NotifyContext；Java：ShutdownHook）。 --init \u002F tini 除了转发信号还负责收尸（僵尸进程），是通用建议默认开的选项（第十二章 init: true）。 若一定要在 CMD 里做变量替换，用 exec 内联替换当前 shell：CMD [\"sh\",\"-c\",\"exec node $ENTRY_FILE\"]——exec 让 node 顶替 sh 成为 PID 1。 验证是否真的收到信号：docker stop 时看日志有没有你的 graceful-shutdown 行，以及 docker inspect --format '{{.State.ExitCode}}' 是 143\u002F137（没处理）还是 0（处理了）。示例 16 把这套对比做成终端演示：同一个镜像，shell 形式 CMD 与 exec 形式 CMD 在 docker stop -t 15 下的不同结局。",{"id":16834,"title":13370,"titles":16835,"content":16836,"level":1202},"\u002Ftutorials\u002Fdocker#日志json-file-的轮转必须设",[5,12979],"默认 json-file 驱动不限制大小：一台机器跑三个月，\u002Fvar\u002Flib\u002Fdocker\u002Fcontainers\u002F\u003Cid>\u002F\u003Cid>-json.log 能长到几十 GB，把磁盘写满（第十八章的 no space left on device 一大半是这个）。 { \"log-driver\": \"json-file\", \"log-opts\": { \"max-size\": \"20m\", \"max-file\": \"3\", \"compress\": \"true\" } } 写进 \u002Fetc\u002Fdocker\u002Fdaemon.json 是全局兜底；单个服务在 Compose 里写 logging（第十二章）。改完只影响之后创建的容器，已有容器要 docker compose up -d --force-recreate。 其他驱动与取舍：syslog（--log-opt syslog-address=udp:\u002F\u002F10.0.0.7:514 --log-opt tag={{.Name}}，走集中式，注意 UDP 会丢）、journald（能 journalctl -u docker 一起看）、local（Docker 自带的轻量二进制日志，daemon 重启不丢，但生态工具读不了）、fluentd（采集侧最常用，配合 Fluent Bit 直接读 json-file 也行）、none（一次性任务用）。采集侧的通用做法：Agent 以 DaemonSet\u002Fsidecar 方式挂 \u002Fvar\u002Flib\u002Fdocker\u002Fcontainers:ro 与 \u002Fvar\u002Frun\u002Fdocker.sock，按 label 打标（第六章 --label）。",{"id":16838,"title":13494,"titles":16839,"content":16840,"level":1202},"\u002Ftutorials\u002Fdocker#时区locale-与镜像版本固定",[5,12979],"FROM node:22-bookworm-slim\nENV TZ=Asia\u002FShanghai\nRUN apt-get update && apt-get install -y --no-install-recommends tzdata && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F* TZ 环境变量在 glibc\u002Fmusl 上都生效，但镜像里必须有 tzdata，否则 TZ 无效退化成 UTC（alpine 要 apk add tzdata）。更稳的做法是把宿主 \u002Fetc\u002Flocaltime 与 \u002Fusr\u002Fshare\u002Fzoneinfo 只读挂进去。日志时间戳与数据库时区不一致，九成是这个原因。locale 上 slim\u002Falpine 默认 C.UTF-8，够 JSON 与 ASCII 用；要中文排序\u002Fstrcoll 才需要生成 locale（或换 Debian 完整版）——别为了一个报错在镜像里装全套语言包。 版本固定与回滚的思路（第十章的 tag 语义在这里落地）：部署清单里记 digest，up -d 前先拉新再切；单机土法滚动： docker compose -f compose.yaml -f compose.prod.yaml up -d --no-recreate api   # 新 api 起在 8081\ndocker inspect --format '{{.State.Health.Status}}' shop-api-2                  # 等 healthy\ndocker exec shop-web nginx -s reload                                          # 上游切到新实例\ndocker stop shop-api && docker rm shop-api 回滚就是把旧 digest 再 up 一次——前提是镜像还留着、且数据库迁移是向后兼容的（迁移脚本要能回滚或双写，这是应用层设计，Docker 帮不了你）。蓝绿在单机 Docker 下的实质就是「两套端口 + 反代切换」，别指望 --restart 帮你做发布。",{"id":16842,"title":13650,"titles":16843,"content":16844,"level":1202},"\u002Ftutorials\u002Fdocker#备份与监控",[5,12979],"单机备份三件套，缺一不可： tar czf \u002Fbackup\u002Fshop-${DATE}.tar.gz -C \u002Fsrv\u002Fshop compose.yaml compose.prod.yaml config nginx secrets\u002Fdb_password.txt\ndocker run --rm --volumes-from shop-db -v \u002Fbackup:\u002Fbackup alpine tar czf \u002Fbackup\u002Fdb-${DATE}.tar.gz \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\ndocker save ghcr.io\u002Fexample\u002Fshop-api:1.8.3 | gzip > \u002Fbackup\u002Fshop-api-1.8.3.tar.gz 配置（文本，进 git 或对象存储）+ 数据（卷，pg_dump 优先于文件级）+ 镜像（tar，保证能重建当时的运行态）。恢复演练比备份本身重要：季度性地在一台干净机器上「只用备份」把站起回来（第十章的 docker load 在这里用）。 监控侧：docker stats 是即时快照、无历史、无告警（第十四章），单机最小方案是 cAdvisor（容器指标）+ node_exporter（宿主）+ Prometheus + Grafana： docker run -d --name cadvisor --restart unless-stopped \\\n  --volume=\u002F:\u002Frootfs:ro --volume=\u002Fvar\u002Frun:\u002Fvar\u002Frun:ro \\\n  --volume=\u002Fsys:\u002Fsys:ro --volume=\u002Fvar\u002Flib\u002Fdocker\u002F:\u002Fvar\u002Flib\u002Fdocker:ro \\\n  --publish 127.0.0.1:8081:8080 --detach=true gcr.io\u002Fcadvisor\u002Fcadvisor:v0.49.1 cAdvisor 需要看到宿主的 \u002Fsys、\u002Fvar\u002Flib\u002Fdocker 与 docker socket——这正好是上面「挂 socket 等于交出 root」的现实权衡：监控系统要有边界（只绑回环、单独网络、--read-only 应用不到它但 no-new-privileges 要加）。告警至少配三条：容器 OOMKilled 次数、unhealthy 状态持续、\u002Fvar\u002Flib\u002Fdocker 使用率。",{"id":16846,"title":13842,"titles":16847,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十七编排与生态compose-的天花板swarm-与-kubernetes",[5],{"id":16849,"title":13846,"titles":16850,"content":16851,"level":1202},"\u002Ftutorials\u002Fdocker#单机-compose-的天花板在哪",[5,13842],"Compose 是「一台机器上的应用描述器」。它的限制不是功能少，而是模型里没有「集群」这个概念： 想做 Compose 单机能做到吗 缺口 跨机器调度 ✗ 没有节点资源视图与放置逻辑 副本自动重分布 ✗（机器挂了服务就挂） 无健康重建、无 failover 滚动更新与自动回滚 土法（第十六章） 无 rollout 状态机 端口冲突（两机同端口） ✗ ports 是宿主级的，扩容即冲突 密钥加密分发 ✗（file driver 落盘） 第十三章那条区别在这里最要命 资源配额与优先级 部分（limits） 无抢占、无队列 服务发现跨机 靠外部 LB 无内置 VIP\u002F负载均衡 所以判断标准很直接：一台机器能装下、且你能接受它挂了就手动恢复，Compose 够；否则要编排。",{"id":16853,"title":13957,"titles":16854,"content":16855,"level":1202},"\u002Ftutorials\u002Fdocker#swarm-的最小可用集",[5,13842],"Swarm 是 Docker 原生的编排，命令风格与 Compose 连续，上手成本远低于 K8s： docker swarm init --advertise-addr 10.0.0.11\ndocker swarm join-token worker          # 拿到 join 命令给其他节点\ndocker node ls\ndocker node update --availability drain node-2        # 下线维护（先迁移副本） docker network create --driver overlay --attachable shop-net\ndocker secret create db_password .\u002Fsecrets\u002Fdb_password.txt\ndocker service create --name shop-api --replicas 3 -p 8080:3000 \\\n  --network shop-net --mount type=volume,source=uploads,target=\u002Fsrv\u002Fshop\u002Fuploads \\\n  --limit-cpu 1 --limit-memory 512M --update-parallelism 1 --update-delay 10s \\\n  --constraint node.labels.zone==cn-east-a \\\n  ghcr.io\u002Fexample\u002Fshop-api:1.8.3@sha256:9c1f0ea7b2d4\n\ndocker service scale shop-api=6\ndocker service update --image ghcr.io\u002Fexample\u002Fshop-api:1.9.0 shop-api\ndocker service ps shop-api --no-trunc           # 每个副本在哪台、为什么失败\ndocker service logs -f shop-api\ndocker service inspect shop-api --pretty 要点：service 而不是容器——你要的是「期望 3 个副本」，不是「起 3 个容器」；滚动更新由 --update-parallelism\u002F--update-delay\u002F--update-failure-action rollback 三个参数决定；--constraint（node.labels.*）做放置约束，--limit-cpu\u002F--limit-memory 让调度器算得准。 overlay 网络的 --attachable 是让单机 docker run 也能加入这个网络（不加则只有 service 能进），排障时非常需要。stack 则是「把 Compose 文件部署成 Swarm 服务」： docker stack deploy -c compose.yaml -c compose.prod.yaml shop\ndocker stack services shop\ndocker stack rm shop stack deploy 只支持 Compose 规范的一个子集：build 被忽略（镜像必须先在 registry）、ports 只能用目标模式、container_name\u002Fvolumes_from\u002Fnetwork_mode: host 不支持、depends_on 被忽略（服务是无顺序的集合）。这几条限制解释了为什么生产上「Compose 文件不等于 Swarm 文件」，需要单独维护。",{"id":16857,"title":14335,"titles":16858,"content":16859,"level":1202},"\u002Ftutorials\u002Fdocker#kubernetes-与-docker-的关系dockershim-为什么被移除",[5,13842],"时间线：K8s 需要一个能「按 CRI 接口管理容器生命周期」的运行时。Docker Engine 不实现 CRI，于是社区写了 dockershim（kubelet 里的翻译层，把 CRI 调用转成 Docker API）。因为维护成本高、行为与 Docker 强耦合，K8s 1.24 起把它从 kubelet 移除。 三个常被误解的事实： 不是「K8s 不支持 Docker 了」。Docker 构建的镜像是标准 OCI 镜像，K8s 照样跑；docker build\u002Fdocker compose（本地开发）继续可用。 K8s 用的是 containerd（正是第一章架构图里 Docker 下面的那一层，第二章）或 CRI-O——Docker 自己也是通过 containerd 跑容器，移除的只是最上面那层 shim。 生态定位：containerd 是库\u002F守护进程（被 Docker、K8s、Singularity 复用）；CRI-O 是专为 K8s 实现的轻量 CRI 运行时；Podman 是无 daemon、可 rootless、CLI 与 Docker 兼容的替代（alias docker=podman 通常就能用，podman compose\u002Fpodman play kube 走 Compose\u002FK8s 文件）；nerdctl 是 containerd 的 Docker 兼容 CLI，常用于 buildx 风格的本地构建。 概念对照表（迁移时最有用的东西）： Compose Kubernetes 说明 services 的一个服务 Deployment（+ ReplicaSet + Pod） K8s 的最小单元是 Pod（可多容器共享 net\u002FIPC，对应「sidecar」） ports \u002F expose Service（ClusterIP\u002FNodePort\u002FLoadBalancer）+ Ingress\u002FGateway 名字即 DNS，跨命名空间用 svc.ns.svc.cluster.local networks Namespace + NetworkPolicy 默认全通，靠 NetworkPolicy 收紧（对应第十二章的 internal） volumes 命名卷 PV \u002F PVC + StorageClass 「谁拥有存储」变成集群对象，跨节点可挂 bind mount hostPath（几乎不该用） 需要它通常说明架构有问题 depends_on + healthcheck initContainer + readiness\u002Fliveness\u002Fstartup probe readiness 决定「进不进 Service 端点」，比 unhealthy 不重启前进一大步 scale \u002F --scale spec.replicas + HPA 自动扩缩是控制器，不是命令 environment \u002F env_file ConfigMap + Secret（envFrom） Secret 默认只是 base64，不是加密；要信封加密或外部密钥系统 configs ConfigMap 挂载 对应「只读挂配置目录」（第十三章） restart: unless-stopped 由 kubelet\u002F控制器保证，无需声明 Pod 挂了自动重建，这是编排层最大差异 profiles 无直接对应（用不同清单\u002FHelm values） docker compose up kubectl apply -f 声明式对齐这一思想一致",{"id":16861,"title":14587,"titles":16862,"content":16863,"level":1202},"\u002Ftutorials\u002Fdocker#什么时候真的需要编排",[5,13842],"需要：多台机器且要自动分布与 failover、副本数会随负载变化、多团队共享集群、需要滚动发布与自动回滚、需要 RBAC 与审计、有状态服务要跨节点存储。 不需要：单机或两三台、服务数量个位数、变更频率低、团队没有 K8s 运维经验（这条最重要——K8s 的运维成本会吃掉它的收益）。中间态「Swarm 或 Compose + 一个反代 + 外部健康检查」常常是最优解。工程上的正确姿势是：先把应用做成容器友好的（第十三章无状态、配置注入、优雅停机、健康检查），这时上不上编排都是可逆决定；反过来，用一个「什么都做不了」的有状态容器去换 K8s，只会把问题从部署搬到运维。",{"id":16865,"title":14605,"titles":16866,"content":1163,"level":1177},"\u002Ftutorials\u002Fdocker#十八排障与速查报错原文原因与命令",[5],{"id":16868,"title":14609,"titles":16869,"content":16870,"level":1202},"\u002Ftutorials\u002Fdocker#报错-原因-怎么办",[5,14605],"报错原文（片段） 真实原因 怎么办 Cannot connect to the Docker daemon at unix:\u002F\u002F\u002Fvar\u002Frun\u002Fdocker.sock. Is the docker daemon running? daemon 没起 \u002F socket 路径不对 \u002F 远程上下文指错 systemctl status docker；docker context ls；ls -l \u002Fvar\u002Frun\u002Fdocker.sock；Desktop 用户先看图标是否在跑 permission denied while trying to connect to the Docker daemon socket 当前用户不在 docker 组，或加了组没重新登录 getent group docker；usermod -aG docker $USER 后重登（第二章），或 newgrp docker no space left on device 宿主磁盘满（多半是容器日志或镜像层），或 tmpfs\u002Fshm 小 df -h + docker system df；先清 docker builder prune、日志（第十六章）、旧镜像；shm_size 单独调 Error response from daemon: driver failed programming external connectivity ... port is already allocated 宿主端口被别的容器\u002F进程占（第八章四步） docker ps --format 找占用者；ss -lntp 找宿主进程；僵尸 docker-proxy 靠重启 docker exec format error 镜像架构与宿主不符（amd64 镜像跑在 ARM） docker image inspect --format '{{.Os}}\u002F{{.Architecture}}'；用 --platform 拉对应架构或构建多架构（第五章） Error: No such image: xxx:tag \u002F image with reference ... was not found 构建失败没产出 tag \u002F 名字或 tag 拼错 \u002F 用了 --output type=oci 没 --load docker images 核对；docker build 加 --progress=plain 看是否真的成功；buildx 记得 --load 或 --push pull access denied for xxx \u002F repository does not exist or may require 'docker login' 私有仓库未登录 \u002F 名字少了 registry 前缀 \u002F 镜像确实不存在 docker login \u003Cregistry>；补全 ghcr.io\u002F、localhost:5000\u002F 前缀（第十章）；确认命名空间大小写 toomanyrequests: You have reached your pull rate limit Docker Hub 匿名限额（第十章） CI 登录、把基础镜像同步到自建 registry、加缓存 error during connect: ... open \u002F\u002F.\u002Fpipe\u002Fdocker_engine: The system cannot find the file specified（Windows\u002FmacOS） Docker Desktop 的 VM 没起来 \u002F 命名管道丢失 \u002F WSL2 未启用 启动 Desktop 等到图标稳定；docker context use desktop-linux；wsl --update；确认 BIOS 虚拟化 Error response from daemon: container ... is not running 对已停止容器执行 exec\u002Fstats 先 docker ps -a 看状态 → logs → --entrypoint sh 手工跑（第七章三步法） OCI runtime create failed: ... apply cgroup \u002Fsys\u002Ffs\u002Fcgroup\u002F... cgroup 驱动冲突（systemd vs cgroupfs）、cgroup v1\u002Fv2 不匹配、或宿主 systemd 版本与 docker 不兼容 docker info 看 Cgroup Driver\u002FVersion；统一驱动；宿主重启后残留 cgroup 未清理时重启 docker docker: 'compose' is not a docker command 装的是 v1 独立二进制，或 CLI 插件目录缺失 docker compose version 失败则装 docker-compose-plugin；旧环境用 docker-compose（带横线）；Linux 检查 ~\u002F.docker\u002Fcli-plugins\u002F network ... not found 网络被 down 删了但容器还在引用，或用了默认网络名 docker network ls；重建网络并 network connect；Compose 项目名变了（第十一章） failed to start daemon: error initializing network allocator: invalid IPv4 network default-address-pools 与宿主网段冲突 daemon.json 调整 bip\u002Fdefault-address-pools 避开 192.168.x error while mounting volume: permission denied bind mount 源目录 SELinux\u002FAppArmor 拦或属主不符（第九章） 加 :z\u002F:Z（SELinux）；对齐 UID；rootless 检查 subuid 容器起来就 Exited (1)，logs 空白 启动即失败且错误写进了文件而非 stdout；或 CMD 不是前台进程 logs --tail 50 → --entrypoint sh 手工跑 → docker inspect .Path .Args 核对命令（第七、十三章） 容器起来就 Exited (137) OOM、docker kill、或 stop 超时（第十四章） inspect 看 OOMKilled；调 -m；看是否 swap 语义写错",{"id":16872,"title":15013,"titles":16873,"content":16874,"level":1202},"\u002Ftutorials\u002Fdocker#dockerfile-指令速查表",[5,14605],"指令 一句话语义 进数据层？ 最常犯的错 FROM 起点\u002F阶段 — 用 latest；忘写 AS 名字 ARG 构建期变量 否 当密钥用；FROM 后忘记重新声明 ENV 构建+运行期变量 元数据 当密钥用；放在第一层导致全失效 COPY 复制上下文文件 是 不放 .dockerignore；写太靠前击穿缓存 ADD 复制 + 自动解压 tar + 拉 URL 是 用它下载（无法清缓存、不校验） RUN 执行命令生成一层 是 多条 apt-get 分开写；rm 前一层的东西 WORKDIR 设置后续相对路径 元数据 用 RUN cd 代替；用相对\u002F~ 路径 USER 切换身份 元数据 放太靠前导致后续装包失败；写用户名不写 UID VOLUME 声明挂载点 元数据 以为它会保留数据；构建期写该目录内容被丢 EXPOSE 文档化端口 元数据 以为它真的开了端口 LABEL 元数据 元数据 放动态值（commit 号）击穿缓存 HEALTHCHECK 自证可用 元数据 不设 start-period；检查打到 127.0.0.1 之外的地址 CMD 默认参数\u002F命令 元数据 用 shell 形式导致 PID 1 收不到信号（第十六章） ENTRYPOINT 主命令 元数据 与 CMD 语义混用；run 传参被整条替换 SHELL 改默认 shell 元数据 切 bash 后镜像里没有 bash #syntax= 选 BuildKit 前端 — 没放在文件最前面 顺序铁律：变化频率从低到高，COPY . . 尽量靠后、元数据指令（ENV\u002FEXPOSE\u002FCMD\u002FENTRYPOINT\u002FUSER）放最后。",{"id":16876,"title":15314,"titles":16877,"content":16878,"level":1202},"\u002Ftutorials\u002Fdocker#命令速查六组",[5,14605],"docker build -t repo\u002Fapp:1.0.0 --pull --no-cache-filter deps .\ndocker buildx build --platform linux\u002Famd64,linux\u002Farm64 -t repo\u002Fapp:1.0.0 --push .\ndocker images; docker image inspect img; docker history --no-trunc img\ndocker pull repo\u002Fapp:1.0.0@sha256:...; docker tag a b; docker push b\ndocker save -o a.tar img; docker load -i a.tar\ndocker rmi img; docker image prune -a --filter until=336h docker run -d --name app --restart unless-stopped -p 127.0.0.1:8080:3000 \\\n  -e TZ=Asia\u002FShanghai --env-file .env.prod -v app-data:\u002Fsrv\u002Fapp\u002Fdata \\\n  --network shop-net --init --read-only --tmpfs \u002Ftmp \\\n  --memory 512m --memory-swap 512m --cpus 1.5 --pids-limit 256 \\\n  --user 1001:1001 --cap-drop ALL --security-opt no-new-privileges img:1.0.0\ndocker ps -a; docker start\u002Fstop\u002Frestart\u002Fkill\u002Fpause\u002Funpause\u002Frm [-f] app\ndocker container prune; docker container ls --filter ancestor=img:1.0.0\ndocker logs -f --since 10m --timestamps app; docker stats --no-stream\ndocker top app; docker diff app; docker events --since 1h --filter event=die\ndocker exec -it app sh; docker cp app:\u002Fpath .\u002F; docker inspect --format '{{.State.Status}}' app\ndocker run --rm -it --entrypoint sh img -c 'node server.js' docker network create --driver bridge --subnet 172.28.0.0\u002F16 shop-net\ndocker network connect [--alias api] shop-net app; docker network disconnect shop-net app\ndocker network ls; docker network inspect shop-net; docker network rm shop-net\ndocker network prune docker volume create --name db-data --opt type=nfs --opt o=addr=...,rw --opt device=:\u002Fsrv\u002Fnfs\ndocker volume ls; docker volume inspect db-data; docker volume rm db-data\ndocker run --rm --volumes-from shop-db -v \"$PWD:\u002Fbackup\" alpine tar czf \u002Fbackup\u002Fdb.tar.gz \u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\ndocker run -d --mount type=volume,source=db-data,target=\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata postgres:16.4\ndocker run -d -v .\u002Fconf:\u002Fetc\u002Fapp:ro nginx:1.27-alpine          # bind mount\ndocker run -d --mount type=tmpfs,tmpfs-size=64m,target=\u002Frun app img:1.0.0 docker compose config                                      # 排错第一步\ndocker compose -f compose.yaml -f compose.prod.yaml up -d --build --force-recreate\ndocker compose ps; docker compose logs -f --tail 100 api\ndocker compose exec api sh; docker compose run --rm migrate node scripts\u002Fmigrate.js\ndocker compose up -d --scale api=3 --profile debug\ndocker compose stop; docker compose down            # 数据保留\ndocker compose down -v --remove-orphans             # -v 删卷，先想清楚 docker system df                # 动手清理前先看这个\ndocker builder prune; docker container prune; docker image prune\ndocker system prune                        # 相对安全：只清已停止\u002F悬空\u002F未用网络\ndocker system prune -a --volumes           # 高危：删所有未引用镜像与卷\ndocker login ghcr.io -U user --password-stdin; docker logout ghcr.io\ndocker manifest inspect img:1.0.0; docker buildx inspect --bootstrap 示例 18 是全教程的收口演练：给出一批真实报错原文，每张卡片对应「先跑哪条命令、看到什么、结论是什么」，点按钮按顺序走完一次线上排障。 回望这十八章：第一到三章解释了「为什么容器能到处跑」，四到五章是造镜像的手艺，六到九章是让一个容器活着并把数据留住，十章解决镜像从哪来，十一到十三章把单容器扩展成一组服务，十四到十五章谈限制与加固，十六章把「能跑」变成「敢上生产」，十七到十八章给出更大规模的答案与出事时的路线。接下来的提升没有捷径：把 \u002Fsrv\u002Fshop 这个例子自己敲一遍，然后用 docker system df 和 docker history 去观察每一个决定留下的痕迹。 html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",1793263560687]