欢迎光临
我们一直在努力

Docker进阶,从构建镜像到Compose编排

Docker 进阶,从构建镜像到 Compose 编排

上一篇讲了怎么用别人的镜像跑容器。pull 一个 nginx,run 起来,端口映射一下,浏览器能访问了。日常用现成镜像跑服务,这套流程已经够用了。

但你自己写的项目呢?

你写了个 Node 应用,同事要在自己电脑上跑,得装 Node 18、装依赖、配环境变量、改端口。写一份十步的安装文档发过去,对方少装一步就炸。说实话这种事情我经历过太多次了,每次都是「我这边明明能跑啊」的绝望循环。

与其写安装文档,不如写一个 Dockerfile。

这篇解决三件事。怎么把你的项目打包成镜像,怎么管数据不让它丢,怎么用 Compose 一键拉起多容器。

用 Dockerfile 造自己的镜像

Dockerfile 是什么。一个纯文本文件,里面是一系列指令,Docker 逐条执行这些指令来构建镜像。把它想象成一份从零搭建环境的清单,每条指令做一件事。

看一个最小的 Dockerfile。

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

逐条说。FROM 是基础镜像,一切从这里开始。你不会从零开始装系统,而是基于一个已经有 Node 18 的精简版 Linux。alpine 是精简版的代名词,体积小,够用。WORKDIR 设置工作目录,后续指令都在这个目录下操作,类似 cd 进去再干活。COPY 先把 package.json 拷进去。RUN npm install 在构建时执行,安装依赖。然后 COPY . . 把源代码拷进去。EXPOSE 声明容器暴露的端口,注意这是文档性质的声明,不自动映射,外部还是访问不到。CMD 是容器启动时执行的命令。

这里有几个新手特别容易踩的坑,我自己全踩过。

COPY 和 ADD 有什么区别。COPY 就是简单的复制文件,ADD 额外支持自动解压 tar 包和从 URL 下载文件。你猜哪个更容易出问题?ADD。因为它的隐式行为让人困惑,你以为只是拷文件,它帮你解压了,你还不知道。所以推荐默认用 COPY,需要解压时再用 ADD,行为明确。

CMD 和 ENTRYPOINT 的区别,这个更关键,也是新手最容易迷糊的地方。简单理解,CMD 是默认命令,可以被 docker run 后面的参数覆盖。ENTRYPOINT 是固定入口,docker run 后面的参数会作为 ENTRYPOINT 的参数追加。

你想想看,这个区别最常踩的坑是这样的。你写了 CMD ["node", "app.js"],然后 docker run myapp sh 想进去看看,结果你的 app.js 根本没跑,因为 CMD 被 sh 覆盖了。如果你用 ENTRYPOINT ["node", "app.js"],同样的命令会变成 node app.js sh,参数被追加到后面,行为完全不同。

最佳实践是组合用,ENTRYPOINT 固定程序,CMD 给默认参数。这样既固定了入口,又允许参数被覆盖。

构建命令很简单。

docker build -t myapp:v1 .

-t 给镜像打标签,最后的 . 是构建上下文路径。这个点很重要,多说两句。

#mermaid-svg-y09VL9NWc9T0kTLV{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y09VL9NWc9T0kTLV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y09VL9NWc9T0kTLV .error-icon{fill:#552222;}#mermaid-svg-y09VL9NWc9T0kTLV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y09VL9NWc9T0kTLV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y09VL9NWc9T0kTLV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y09VL9NWc9T0kTLV .marker.cross{stroke:#333333;}#mermaid-svg-y09VL9NWc9T0kTLV svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y09VL9NWc9T0kTLV p{margin:0;}#mermaid-svg-y09VL9NWc9T0kTLV .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-y09VL9NWc9T0kTLV .cluster-label text{fill:#333;}#mermaid-svg-y09VL9NWc9T0kTLV .cluster-label span{color:#333;}#mermaid-svg-y09VL9NWc9T0kTLV .cluster-label span p{background-color:transparent;}#mermaid-svg-y09VL9NWc9T0kTLV .label text,#mermaid-svg-y09VL9NWc9T0kTLV span{fill:#333;color:#333;}#mermaid-svg-y09VL9NWc9T0kTLV .node rect,#mermaid-svg-y09VL9NWc9T0kTLV .node circle,#mermaid-svg-y09VL9NWc9T0kTLV .node ellipse,#mermaid-svg-y09VL9NWc9T0kTLV .node polygon,#mermaid-svg-y09VL9NWc9T0kTLV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-y09VL9NWc9T0kTLV .rough-node .label text,#mermaid-svg-y09VL9NWc9T0kTLV .node .label text,#mermaid-svg-y09VL9NWc9T0kTLV .image-shape .label,#mermaid-svg-y09VL9NWc9T0kTLV .icon-shape .label{text-anchor:middle;}#mermaid-svg-y09VL9NWc9T0kTLV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-y09VL9NWc9T0kTLV .rough-node .label,#mermaid-svg-y09VL9NWc9T0kTLV .node .label,#mermaid-svg-y09VL9NWc9T0kTLV .image-shape .label,#mermaid-svg-y09VL9NWc9T0kTLV .icon-shape .label{text-align:center;}#mermaid-svg-y09VL9NWc9T0kTLV .node.clickable{cursor:pointer;}#mermaid-svg-y09VL9NWc9T0kTLV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-y09VL9NWc9T0kTLV .arrowheadPath{fill:#333333;}#mermaid-svg-y09VL9NWc9T0kTLV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-y09VL9NWc9T0kTLV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-y09VL9NWc9T0kTLV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y09VL9NWc9T0kTLV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-y09VL9NWc9T0kTLV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y09VL9NWc9T0kTLV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-y09VL9NWc9T0kTLV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-y09VL9NWc9T0kTLV .cluster text{fill:#333;}#mermaid-svg-y09VL9NWc9T0kTLV .cluster span{color:#333;}#mermaid-svg-y09VL9NWc9T0kTLV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-y09VL9NWc9T0kTLV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-y09VL9NWc9T0kTLV rect.text{fill:none;stroke-width:0;}#mermaid-svg-y09VL9NWc9T0kTLV .icon-shape,#mermaid-svg-y09VL9NWc9T0kTLV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y09VL9NWc9T0kTLV .icon-shape p,#mermaid-svg-y09VL9NWc9T0kTLV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-y09VL9NWc9T0kTLV .icon-shape .label rect,#mermaid-svg-y09VL9NWc9T0kTLV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y09VL9NWc9T0kTLV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-y09VL9NWc9T0kTLV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-y09VL9NWc9T0kTLV :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

打包发送

逐条执行 Dockerfile

缓存命中则跳过

构建上下文 (当前目录)

Docker 引擎

Layer: FROM node:18-alpine

Layer: RUN npm install

Layer: COPY . .

Layer: CMD

完整镜像 myapp:v1

构建上下文是 Docker 在构建时能看到的文件范围。你写 docker build -t myapp:v1 .,那个 . 表示把当前目录作为上下文传给 Docker 引擎。Docker 会把这个目录里的所有文件打包发给引擎,所以如果你不加 .dockerignore,node_modules、.git 这些都会被传过去。node_modules 可能几百 MB,每次构建传一遍,又慢又大。。。

.dockerignore 文件就是干这个的,跟 .gitignore 一样排除不需要的文件。

node_modules
.git
*.log
.env

说真的,构建缓存优化是 Dockerfile 里最值得花心思的地方。Docker 逐条执行 Dockerfile,每条指令生成一层。如果某条指令的输入没变,Docker 直接用缓存跳过。所以指令顺序很重要,把容易变的放后面,不容易变的放前面。

你看上面那个 Dockerfile,先 COPY package.json 再 RUN npm install 最后 COPY . .,这个顺序是故意设计的。你改了源代码,只有 COPY . . 这一层需要重建,npm install 那层有缓存直接跳过。如果你先 COPY . . 再 npm install,改一行代码就要重新装一遍依赖,那个等待时间够你喝杯咖啡了。

这个分层的思路其实跟工业流水线的逻辑是一致的。把不变的工序放前面,变的工序放后面,减少每次变更需要重做的工序数。Dockerfile 的指令顺序就是你的生产流水线,排得好,构建快,排不好,每次等半天。

数据持久化,容器删了数据还在

前面讲过,容器删了,可写层就没了,数据全丢。你跑了个 MySQL 容器,存了好几天的数据,某天 docker rm 了容器,数据全没了???

别笑,这个事情太常见了。

容器是临时的,用完即弃的。但数据不是。Docker 给了两种持久化方案。

#mermaid-svg-8vWOoYAoe8thAt1B{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8vWOoYAoe8thAt1B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8vWOoYAoe8thAt1B .error-icon{fill:#552222;}#mermaid-svg-8vWOoYAoe8thAt1B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8vWOoYAoe8thAt1B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8vWOoYAoe8thAt1B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8vWOoYAoe8thAt1B .marker.cross{stroke:#333333;}#mermaid-svg-8vWOoYAoe8thAt1B svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8vWOoYAoe8thAt1B p{margin:0;}#mermaid-svg-8vWOoYAoe8thAt1B .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-8vWOoYAoe8thAt1B .cluster-label text{fill:#333;}#mermaid-svg-8vWOoYAoe8thAt1B .cluster-label span{color:#333;}#mermaid-svg-8vWOoYAoe8thAt1B .cluster-label span p{background-color:transparent;}#mermaid-svg-8vWOoYAoe8thAt1B .label text,#mermaid-svg-8vWOoYAoe8thAt1B span{fill:#333;color:#333;}#mermaid-svg-8vWOoYAoe8thAt1B .node rect,#mermaid-svg-8vWOoYAoe8thAt1B .node circle,#mermaid-svg-8vWOoYAoe8thAt1B .node ellipse,#mermaid-svg-8vWOoYAoe8thAt1B .node polygon,#mermaid-svg-8vWOoYAoe8thAt1B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8vWOoYAoe8thAt1B .rough-node .label text,#mermaid-svg-8vWOoYAoe8thAt1B .node .label text,#mermaid-svg-8vWOoYAoe8thAt1B .image-shape .label,#mermaid-svg-8vWOoYAoe8thAt1B .icon-shape .label{text-anchor:middle;}#mermaid-svg-8vWOoYAoe8thAt1B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8vWOoYAoe8thAt1B .rough-node .label,#mermaid-svg-8vWOoYAoe8thAt1B .node .label,#mermaid-svg-8vWOoYAoe8thAt1B .image-shape .label,#mermaid-svg-8vWOoYAoe8thAt1B .icon-shape .label{text-align:center;}#mermaid-svg-8vWOoYAoe8thAt1B .node.clickable{cursor:pointer;}#mermaid-svg-8vWOoYAoe8thAt1B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8vWOoYAoe8thAt1B .arrowheadPath{fill:#333333;}#mermaid-svg-8vWOoYAoe8thAt1B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8vWOoYAoe8thAt1B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8vWOoYAoe8thAt1B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8vWOoYAoe8thAt1B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8vWOoYAoe8thAt1B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8vWOoYAoe8thAt1B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8vWOoYAoe8thAt1B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8vWOoYAoe8thAt1B .cluster text{fill:#333;}#mermaid-svg-8vWOoYAoe8thAt1B .cluster span{color:#333;}#mermaid-svg-8vWOoYAoe8thAt1B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8vWOoYAoe8thAt1B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8vWOoYAoe8thAt1B rect.text{fill:none;stroke-width:0;}#mermaid-svg-8vWOoYAoe8thAt1B .icon-shape,#mermaid-svg-8vWOoYAoe8thAt1B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8vWOoYAoe8thAt1B .icon-shape p,#mermaid-svg-8vWOoYAoe8thAt1B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8vWOoYAoe8thAt1B .icon-shape .label rect,#mermaid-svg-8vWOoYAoe8thAt1B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8vWOoYAoe8thAt1B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8vWOoYAoe8thAt1B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8vWOoYAoe8thAt1B :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

Bind Mount (绑定挂载)

宿主机 /home/user/html

容器 /usr/share/nginx/html

Volume (数据卷)

Docker 管理区 (你不用管路径)

容器 /data

Volume,数据卷,由 Docker 引擎管理,存在 Docker 自己的数据目录里。你不需要关心它具体在宿主机上的哪个路径,Docker 全权管理。跨平台兼容性好,适合数据库这种你不需要直接碰文件的场景。你只管往容器里写,Docker 帮你存在一个安全的地方。

Bind Mount,绑定挂载,直接把宿主机的某个目录挂进容器。你指定一个绝对路径,容器里就能看到宿主机上这个路径的文件。适合开发场景,你把源代码目录挂进容器,改了代码容器里立刻生效,不用重新构建镜像。

用法都是通过 -v 或 –mount 参数。

# Volume 方式,mydata 是数据卷名
docker run -d -v mydata:/var/lib/mysql mysql:8

# Bind Mount 方式,宿主机路径:容器路径
docker run -d -v /home/user/html:/usr/share/nginx/html nginx:1.25

Volume 的管理命令。

docker volume create mydata # 创建命名数据卷
docker volume ls # 列出所有数据卷
docker volume inspect mydata # 看数据卷详情,挂载点在哪
docker volume rm mydata # 删数据卷

命名数据卷和匿名数据卷的区别值得说一下。-v mydata:/path 是命名卷,名字叫 mydata,好管理。-v /path 不写容器内路径时 Docker 会创建匿名卷,ID 是一长串哈希,你 ls 出来都不知道哪个是哪个。反正我建议用命名卷,别给自己找麻烦。

容器间数据共享也简单,多个容器挂载同一个命名卷就行。一个容器写,另一个容器读,数据同步。

网络配置,让容器互相通信

你跑了个 Web 容器和一个 API 容器,Web 怎么调用 API?

默认情况下容器之间是隔离的,互相看不到。每个容器有自己的内部 IP,但你不知道 IP 是多少,而且重启后可能变。

Docker 有三种网络模式。

bridge 是默认模式。Docker 在宿主机上创建一个虚拟网桥 docker0,每个容器分配一个内部 IP,容器之间通过网桥通信。这是最常用的模式。

host 模式,容器直接用宿主机的网络栈,没有网络隔离。性能最好但安全性差,端口直接占用宿主机端口。

none 模式,容器没有网络,完全隔离。很少用。

#mermaid-svg-1BRx3G7yHZehh4TY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1BRx3G7yHZehh4TY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1BRx3G7yHZehh4TY .error-icon{fill:#552222;}#mermaid-svg-1BRx3G7yHZehh4TY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1BRx3G7yHZehh4TY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1BRx3G7yHZehh4TY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1BRx3G7yHZehh4TY .marker.cross{stroke:#333333;}#mermaid-svg-1BRx3G7yHZehh4TY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1BRx3G7yHZehh4TY p{margin:0;}#mermaid-svg-1BRx3G7yHZehh4TY .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1BRx3G7yHZehh4TY .cluster-label text{fill:#333;}#mermaid-svg-1BRx3G7yHZehh4TY .cluster-label span{color:#333;}#mermaid-svg-1BRx3G7yHZehh4TY .cluster-label span p{background-color:transparent;}#mermaid-svg-1BRx3G7yHZehh4TY .label text,#mermaid-svg-1BRx3G7yHZehh4TY span{fill:#333;color:#333;}#mermaid-svg-1BRx3G7yHZehh4TY .node rect,#mermaid-svg-1BRx3G7yHZehh4TY .node circle,#mermaid-svg-1BRx3G7yHZehh4TY .node ellipse,#mermaid-svg-1BRx3G7yHZehh4TY .node polygon,#mermaid-svg-1BRx3G7yHZehh4TY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1BRx3G7yHZehh4TY .rough-node .label text,#mermaid-svg-1BRx3G7yHZehh4TY .node .label text,#mermaid-svg-1BRx3G7yHZehh4TY .image-shape .label,#mermaid-svg-1BRx3G7yHZehh4TY .icon-shape .label{text-anchor:middle;}#mermaid-svg-1BRx3G7yHZehh4TY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1BRx3G7yHZehh4TY .rough-node .label,#mermaid-svg-1BRx3G7yHZehh4TY .node .label,#mermaid-svg-1BRx3G7yHZehh4TY .image-shape .label,#mermaid-svg-1BRx3G7yHZehh4TY .icon-shape .label{text-align:center;}#mermaid-svg-1BRx3G7yHZehh4TY .node.clickable{cursor:pointer;}#mermaid-svg-1BRx3G7yHZehh4TY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1BRx3G7yHZehh4TY .arrowheadPath{fill:#333333;}#mermaid-svg-1BRx3G7yHZehh4TY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1BRx3G7yHZehh4TY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1BRx3G7yHZehh4TY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1BRx3G7yHZehh4TY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1BRx3G7yHZehh4TY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1BRx3G7yHZehh4TY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1BRx3G7yHZehh4TY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1BRx3G7yHZehh4TY .cluster text{fill:#333;}#mermaid-svg-1BRx3G7yHZehh4TY .cluster span{color:#333;}#mermaid-svg-1BRx3G7yHZehh4TY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1BRx3G7yHZehh4TY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1BRx3G7yHZehh4TY rect.text{fill:none;stroke-width:0;}#mermaid-svg-1BRx3G7yHZehh4TY .icon-shape,#mermaid-svg-1BRx3G7yHZehh4TY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1BRx3G7yHZehh4TY .icon-shape p,#mermaid-svg-1BRx3G7yHZehh4TY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1BRx3G7yHZehh4TY .icon-shape .label rect,#mermaid-svg-1BRx3G7yHZehh4TY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1BRx3G7yHZehh4TY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1BRx3G7yHZehh4TY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1BRx3G7yHZehh4TY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

端口映射 8080 → 172.17.0.2:80

宿主机

docker0 网桥 (172.17.0.1)

容器A: nginx (172.17.0.2)

容器B: mysql (172.17.0.3)

外部请求 → 宿主机:8080

端口映射上一篇讲过,-p 8080:80,宿主机端口映射到容器端口。这里深入讲一下原理。Docker 在宿主机上通过 iptables/NAT 规则做转发,外部请求到宿主机的 8080,被 NAT 规则转发到容器的 80 端口。容器内部只知道自己监听了 80,不知道外面经过了 NAT。

但默认 bridge 网络有个问题。容器之间只能通过 IP 互相访问,IP 不固定,容器重启可能变。你总不能在代码里写死一个 IP 吧,那也太脆弱了。

解决办法是创建自定义 bridge 网络。自定义网络下,Docker 内置 DNS 解析,容器之间可以用容器名互相访问。

docker network create mynet
docker run -d –name web –network mynet nginx:1.25
docker run -d –name api –network mynet myapi:v1

这样在 web 容器里,你可以直接用 api 这个名字访问 api 容器。Docker 帮你做了 DNS 解析,不管 api 容器的 IP 怎么变,名字永远指向它。这个东西后面 Compose 里会自动帮你搞定,但理解原理很重要,出问题的时候你知道去哪查。

network 的管理命令。

docker network ls # 列出所有网络
docker network create mynet # 创建自定义网络
docker network inspect mynet # 看网络详情,哪些容器连着
docker network connect mynet myweb # 把已有容器加入网络
docker network rm mynet # 删网络

Docker Compose,多容器一键编排

你的应用可能不止一个服务。Web 前端、API 后端、Redis 缓存,三个服务。手动跑的话要敲三条 docker run,每条带一堆参数,还要先建网络、建数据卷。每次部署都敲一遍,敲错一个参数整个应用就炸了。

这种事情干多了你就会想,能不能把所有配置写到一个文件里,一条命令全拉起来。Docker Compose 就是干这个的。

看一个典型的 docker-compose.yml。

services:
web:
build: ./web
ports:
"8080:80"
depends_on:
api
environment:
API_URL=http://api:3000

api:
build: ./api
ports:
"3000:3000"
depends_on:
redis
environment:
REDIS_URL=redis://redis:6379

redis:
image: redis:7alpine
volumes:
redisdata:/data
ports:
"6379:6379"

volumes:
redis-data:

#mermaid-svg-CUq0y5llz5mMtG5Z{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CUq0y5llz5mMtG5Z .error-icon{fill:#552222;}#mermaid-svg-CUq0y5llz5mMtG5Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CUq0y5llz5mMtG5Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CUq0y5llz5mMtG5Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CUq0y5llz5mMtG5Z .marker.cross{stroke:#333333;}#mermaid-svg-CUq0y5llz5mMtG5Z svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CUq0y5llz5mMtG5Z p{margin:0;}#mermaid-svg-CUq0y5llz5mMtG5Z .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z .cluster-label text{fill:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z .cluster-label span{color:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z .cluster-label span p{background-color:transparent;}#mermaid-svg-CUq0y5llz5mMtG5Z .label text,#mermaid-svg-CUq0y5llz5mMtG5Z span{fill:#333;color:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z .node rect,#mermaid-svg-CUq0y5llz5mMtG5Z .node circle,#mermaid-svg-CUq0y5llz5mMtG5Z .node ellipse,#mermaid-svg-CUq0y5llz5mMtG5Z .node polygon,#mermaid-svg-CUq0y5llz5mMtG5Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CUq0y5llz5mMtG5Z .rough-node .label text,#mermaid-svg-CUq0y5llz5mMtG5Z .node .label text,#mermaid-svg-CUq0y5llz5mMtG5Z .image-shape .label,#mermaid-svg-CUq0y5llz5mMtG5Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-CUq0y5llz5mMtG5Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CUq0y5llz5mMtG5Z .rough-node .label,#mermaid-svg-CUq0y5llz5mMtG5Z .node .label,#mermaid-svg-CUq0y5llz5mMtG5Z .image-shape .label,#mermaid-svg-CUq0y5llz5mMtG5Z .icon-shape .label{text-align:center;}#mermaid-svg-CUq0y5llz5mMtG5Z .node.clickable{cursor:pointer;}#mermaid-svg-CUq0y5llz5mMtG5Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CUq0y5llz5mMtG5Z .arrowheadPath{fill:#333333;}#mermaid-svg-CUq0y5llz5mMtG5Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CUq0y5llz5mMtG5Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CUq0y5llz5mMtG5Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CUq0y5llz5mMtG5Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CUq0y5llz5mMtG5Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CUq0y5llz5mMtG5Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CUq0y5llz5mMtG5Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CUq0y5llz5mMtG5Z .cluster text{fill:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z .cluster span{color:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CUq0y5llz5mMtG5Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CUq0y5llz5mMtG5Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-CUq0y5llz5mMtG5Z .icon-shape,#mermaid-svg-CUq0y5llz5mMtG5Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CUq0y5llz5mMtG5Z .icon-shape p,#mermaid-svg-CUq0y5llz5mMtG5Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CUq0y5llz5mMtG5Z .icon-shape .label rect,#mermaid-svg-CUq0y5llz5mMtG5Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CUq0y5llz5mMtG5Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CUq0y5llz5mMtG5Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CUq0y5llz5mMtG5Z :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

depends_on

depends_on

http://api:3000

redis://redis:6379

docker-compose.yml

web (build, :8080→:80)

api (build, :3000)

redis (image, :6379)

redis-data (命名卷)

自动创建 bridge 网络三个服务用服务名互访

逐个说关键字段。services 定义所有服务,每个服务就是一个容器。build 用本地 Dockerfile 构建镜像,值是 Dockerfile 所在目录。image 直接用现成镜像,不用构建。ports 端口映射,跟 docker run 的 -p 一样。volumes 数据卷挂载,命名卷在顶层 volumes 里声明。environment 设环境变量。depends_on 控制启动顺序。

这里有个特别经典的坑,坦率的讲我当年也踩过。depends_on 只保证启动顺序,不保证服务就绪。你的 API depends_on redis,Compose 会先启动 redis 再启动 api。但 redis 启动了不代表它准备好接受连接了,MySQL 启动后还要花几秒钟做初始化。API 在 MySQL 还没就绪的时候就去连,连不上直接报错退出。。。

解决方式是在应用层加重试逻辑,或者写一个等待脚本,轮询检测服务是不是通了再往下走。别指望 Compose 帮你搞定这件事,它只管启动顺序,不管就绪状态。

常用命令。

docker compose up -d # 构建镜像 + 启动所有服务(后台)
docker compose up -d –build # 强制重新构建镜像
docker compose down # 停止并删除所有容器和网络
docker compose down -v # 同时删除数据卷
docker compose logs -f web # 跟踪某个服务的日志
docker compose exec web bash # 进入某个服务的容器
docker compose ps # 看所有服务状态
docker compose restart api # 重启某个服务

也有几个坑。改了 Dockerfile 后直接 up 不会自动重新构建,要加 –build,不然跑的还是旧镜像,你改了半天代码发现没生效,人都傻了。down 默认只删容器和网络,数据卷会保留,加 -v 才删卷。compose 文件里 build 和 image 可以同时用,image 给构建出来的镜像命名,方便后续引用。

两篇就到这

回顾一下。第一篇学会了用镜像跑容器、管理容器生命周期,解决了用别人的东西的问题。第二篇学会了写 Dockerfile 造自己的镜像、用 Volume 管数据、用 Network 管通信、用 Compose 做多容器编排,解决了造自己的东西并管理复杂度的问题。

从手动敲 docker run 到写 compose 文件一键拉起,这个过程其实就是一个从手工作坊到流水线的转变。每条 docker run 命令都是手工操作,依赖你的记忆和准确度。compose 文件把所有配置固化下来,你声明要什么,Docker 帮你做到。在生产环境里你能重复跑、能审计、能版本控制,这些是手工敲命令给不了的。

还有一些高级话题没展开。多阶段构建做镜像瘦身、安全加固、CI/CD 集成、私有仓库搭建。这些在实际项目中会用到,但不在入门到进阶的范畴里。把基础打扎实,后面这些自然能接上。

以上,既然看到这里了,如果觉得不错,随手点个赞或收藏吧,如果想第一时间收到推送,也可以关注一下~
谢谢你看我的文章,我们下次再见。

赞(0)
未经允许不得转载:171主机测评 » Docker进阶,从构建镜像到Compose编排
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址