news 2026/10/23 5:43:18

Docker容器化部署入门与排错:镜像、数据卷、网络、编排实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker容器化部署入门与排错:镜像、数据卷、网络、编排实战

Docker 这个东西,我在刚开始带团队那会儿,几乎是每个新人绕不开的一道坎。有人觉得它就是个"高级虚拟机",有人以为它是运维专属工具,还有人在本地跑通了镜像一到服务器就各种报错。说白了,容器化部署这件事,概念理解不到位,命令背得再熟也只是照猫画虎。这篇内容我打算从零讲起,把 Docker 的镜像、容器、数据卷、网络、编排这几块拆开揉碎,讲清楚每个动作背后的动机,配上可以直接抄的命令和踩过的坑。适合完全没接触过容器的人,也适合用过一阵但一直没搞懂"为什么这么设计"的朋友。看完你至少能做到:独立把一个应用打包成镜像、跑起来、挂上数据、连上数据库,遇到起不来、连不通、磁盘爆了这类问题,知道从哪儿下手查。

1. 先搞明白 Docker 到底在解决什么问题

1.1 那句"我电脑上明明能跑"背后的真实矛盾

做开发的人几乎都经历过这个场面:功能在自己机器上跑得好好的,交到测试环境就报错,一查是 Python 版本差了一个小版本,或者某个系统库的版本不一样,再或者是环境变量没配、时区不对、字体缺失。这类问题表面看是"环境问题",根子上是应用的运行环境没有被当成交付物的一部分。

传统做法是写一份安装文档,一二三四五列出要装什么依赖、配什么参数,然后指望每个人照着做一遍完全一致。现实是这份文档永远滞后,新人按着它装,装完发现还是差一点,于是口口相传补上"哦对了还得改那个配置",久而久之环境就变成了一团没人说得清的手工制品。

Docker 换了个思路:把应用连同它依赖的运行环境,一起打包成一个不可变的镜像。这个镜像在谁的机器上跑,里面装的都是同一套东西,操作系统版本、库文件、环境变量、启动命令,全都固化在里面。于是"环境"从一个需要人工复现的过程,变成了一个可以传输、可以版本管理的文件。这个转变看着简单,实际是整个容器化部署体系的地基。

我个人的判断标准很直接:一个项目如果还需要靠文档告诉别人怎么装环境,那它就没真正完成交付。把这些写进 Dockerfile,才是可复现的。

1.2 容器和虚拟机不是一回事,别混着理解

很多人第一次听容器,脑子里浮现的是虚拟机。两者确实都是"隔离运行环境",但实现方式和代价完全不同。

虚拟机是把硬件虚拟化,在宿主机上跑一个完整的操作系统内核,再在里头跑应用。一台机器开三四个虚拟机,每个都要吃掉一份内核内存、一份系统盘,启动时间按分钟算。容器不一样,它直接复用宿主机的内核,靠内核提供的隔离能力(命名空间做隔离、控制组做资源限制)把进程隔开。所以容器里跑的不是一个"完整系统",而是一个进程,只不过这个进程以为自己在一台独立机器上。

这个差异带来的实际影响非常大:容器启动是秒级甚至毫秒级的,镜像体积可以做到几十兆,一台普通服务器上跑十几个容器毫无压力。代价是容器的隔离强度不如虚拟机,它跟宿主机共享内核,所以不能拿它当安全沙箱用,跑不可信代码还是得上虚拟机。

注意:镜像里那个"看起来像操作系统"的东西,其实只是一份精简的文件系统,不含内核。这就是为什么你的容器不能装一个跟自己内核版本冲突的驱动模块。

理解这点之后,很多现象就顺了:容器里ps看不到宿主机进程、容器里改sysctl会报权限不足、容器时间跟着宿主机走,都是同一个原因。

1.3 镜像、容器、仓库:三个词必须区分清楚

这三个概念是最容易混的,我用做菜的类比说一下。

镜像是菜谱加上预处理好的半成品,只读,不能改。它由一层一层的文件系统叠起来,每一层对应构建过程中的一个动作。分层的好处是复用:两个镜像都基于同一个基础镜像,那一层就只用存一份,拉取的时候也只用拉没拉过的部分。

容器是照着菜谱真正炒出来的那盘菜。它在镜像之上加了一个可写层,你在容器里改文件、装东西,都写在这一层里。容器删掉,这一层也就没了。所以"在容器里改配置然后重启容器"这种操作,重启后配置就丢了,除非你把配置目录挂到外面,或者重新构建镜像。

仓库是放镜像的地方,给你上传和下载用。公共仓库里有大量现成的官方镜像,自己做的镜像也可以推到私有仓库,供其他机器拉取。

概念是否可写生命周期常见操作
镜像只读,分层长期保存,可版本化构建、打标签、推送
容器顶层可写随创建删除启动、停止、进入、删除
仓库只读(对拉取方)长期保存登录、推送、拉取

这张表建议记牢。后面所有"为什么改了不生效""为什么重启就没了"的问题,八成都能从这张表里找到答案。

2. 环境搭建:装之前先想清楚装在哪

2.1 选安装方式的几个实际考量

安装本身不复杂,但要先确认一件事:你是装在 Linux 服务器上,还是装在自己的开发机上。这两条路的差异不小。

Linux 服务器上,一般是直接装引擎,通过系统包管理器或者官方提供的安装脚本。要注意的是,不同发行版对包的处理方式不同,装完之后服务是否自启动、日志落在哪、配置目录在哪,都有细微差别,装完第一时间去确认这些,比事后排查省事得多。

macOS 和 Windows 上,因为内核不是 Linux,跑不了原生容器,需要一层轻量虚拟化。桌面版安装包把这一层和命令行工具都带上了,装完开箱可用,适合本地开发和验证。缺点是资源占用比纯命令行环境高一些,长期开着一堆容器会明显吃内存,本地开发时建议按需启停,别让它常驻后台。

还有一种情况值得单独说:如果你只是想在本地跑一两个服务验证一下,不想在机器上装一堆东西,也可以考虑用云端的一次性环境,用完即弃,不污染本地。这类做法在临时验证、演示场景里挺实用。

安装完成后,第一件事是验证:

docker version docker info

docker version会分别输出客户端和服务端的信息。如果只看到客户端、服务端那一栏报错,说明引擎没起来,去检查服务状态和日志,别急着往下走。

2.2 权限配置:别顺手给自己挖个坑

Linux 上默认只有 root 能操作 Docker,普通用户执行会报权限错误。网上最常见的解法是把当前用户加进 docker 组:

sudo usermod -aG docker $USER

加完要重新登录才生效。这里必须说清楚一件事:加入这个组,基本等同于给了你 root 级别的权限。因为你可以启动一个挂载了宿主机根目录的容器,然后在里面任意改文件。这个操作在生产机器上是不能随便给的,多用户共用的机器上尤其要谨慎。

个人开发机图方便,加就加了。服务器上我更推荐两种做法:要么就用普通用户加 sudo 执行有限的命令,要么用无 root 模式把引擎跑在用户空间里。后者的配置略麻烦一点,但安全性高一个量级,涉及对外的机器上值得花这个时间。

还有一个容易忽略的点:引擎的配置文件通常在/etc/docker/daemon.json。这个文件不存在是正常的,需要自己新建。改完配置必须重启服务才生效,重启会导致所有正在跑的容器停止,除非配置了自动重启策略。所以改配置这件事尽量挑业务低峰期做,或者提前把容器设成自动重启。

2.3 拉取慢和磁盘被占满,两个绕不开的前置问题

镜像拉取慢是很常见的困扰。原因是默认从公共仓库拉,链路长。解决办法是配置镜像加速地址,在daemon.json里加上:

{ "registry-mirrors": ["https://your-mirror.example.com"] }

配置完重启服务,docker info输出里能看到加速地址列表才算生效。这里提醒一句,加速地址会变化,用之前确认它是可用的,别照抄一份过期配置然后怀疑自己网络有问题。

第二个坑是磁盘。默认情况下镜像和容器数据都堆在/var/lib/docker,这个分区通常不会给太大,跑几个月就容易满。我一般会在装机阶段就把数据目录挪到容量大的盘上:

{ "data-root": "/data/docker" }

改之前要先把旧数据停服务后迁过去,不然已有的镜像和容器会"消失"。迁移命令大致是先systemctl stop docker,用rsync同步目录,改配置,再启动。生产机器上这个动作要写进变更流程,别在业务跑着的时候干。

提示:>COPY . /app RUN pip install -r /app/requirements.txt

这么写,你改一行业务代码,COPY那层的缓存就失效了,后面装依赖的层跟着全部重跑,一次构建几分钟。改一下顺序:

COPY requirements.txt /app/ RUN pip install -r /app/requirements.txt COPY . /app

依赖清单不常变,装依赖的层就能长期命中缓存,改代码时只重跑最后那一层,构建时间从几分钟降到几秒。这个调整看着微不足道,实际是镜像构建优化的第一课。

查看分层情况用docker history 镜像名,能看到每一层的大小和创建指令。经常能看到某一层莫名其妙几百兆,回头一查是apt装完没清缓存,或者把.git、node_modules、测试数据一起拷进去了。这类问题靠docker history一眼就能发现。

对应地,.dockerignore文件必须写。它跟.gitignore语法类似,作用是构建时排除文件。不写它的后果是构建上下文变得巨大,每次构建都要把整个目录传给引擎,慢且容易把本地配置、密钥、日志一起打进镜像。

3.2 容器生命周期命令的实操节奏

日常用得最多的就是这几个:

docker run -d --name web -p 8080:80 -v /data/web:/usr/share/nginx/html nginx:1.25 docker ps docker logs -f web docker stop web docker rm web

拆开看每个参数的意义,比死记硬背有用。-d是后台运行,不加这个参数容器会占着当前终端,日志直接刷屏,退出终端容器就停。--name给容器起名,不起名的话引擎会随机分配一个,事后想stop还得先ps查一遍,很麻烦。-p 8080:80是端口映射,冒号前是宿主机端口,冒号后是容器内端口,写反了就是一个都连不上。-v是挂载,宿主机路径在前。nginx:1.25是镜像加标签,标签一定要写,不写默认拿latest,下次再拉可能就是另一个版本,环境又不可复现了。

容器的状态流转大概是:created(创建了没启动)→running→exited。docker run一步到位做了创建和启动。想先创建再启动,用docker create加docker start,这种用法在需要预热或者先挂载再启动的场景里有意义。

docker ps只看运行中的,docker ps -a能看到已停止的。排错第一步永远是docker ps -a,看容器是不是启动后立刻退出了。退出码很有信息量:

退出码常见原因排查方向
0主进程正常结束是不是跑完就退的一次性任务
1应用自身报错docker logs看启动日志
125引擎层面参数错误检查命令参数拼写
126命令不可执行权限或 shebang 问题
127命令找不到镜像里没这个可执行文件
137被强制终止内存超限或手动 kill
143收到正常终止信号通常是 stop 导致

137 这个码要特别注意。它表示进程收到 SIGKILL,最常见的原因是内存超过限制被系统干掉了。这时候去docker inspect看OOMKilled字段是不是 true,如果是,调大内存限制或者查应用是不是有内存泄漏,别在日志里瞎找。

3.3 进容器调试:exec 和 attach 别用错

容器起不来或者行为异常,总想进去看看。两种方式:

docker exec -it web /bin/sh docker attach web

exec是在容器里新开一个进程,开个 shell 进去看看文件、测测网络,退出时不影响主进程。这是首选,99% 的场景都用它。attach是接到主进程的标准输入输出上,你看到的日志就是主进程的输出,这时候按 Ctrl+C 会把信号直接发给主进程,很可能把业务进程干掉。

用attach要退出又不杀进程,得用Ctrl+P然后Ctrl+Q,这个组合键不好记,我一般是干脆不用 attach,日志就用docker logs -f看,效果一样还安全。

还有个实用技巧:镜像里如果连sh都没有(比如某些极简基础镜像),exec进去会失败。这种时候可以用docker cp把文件拷出来看,或者临时基于这个镜像起一个带 shell 的容器,把可疑目录挂进去排查。

docker inspect是另一个高频工具,输出是 JSON,信息量很大。查 IP 用--format过滤:

docker inspect -f '{{.NetworkSettings.IPAddress}}' web docker inspect -f '{{.State.Status}} {{.State.OOMKilled}}' web

养成用--format的习惯,比在几百行 JSON 里翻找快得多。

4. 用 Dockerfile 把应用打包成镜像

4.1 指令逐条拆解,每条都说明白为什么

一份典型的 Dockerfile 长这样:

FROM python:3.11-slim WORKDIR /app ENV TZ=Asia/Shanghai PYTHONUNBUFFERED=1 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "main.py"]

FROM选基础镜像。这里选slim版本而不是完整版,因为完整版带了一堆编译工具和文档,体积能差好几倍。但要注意slim和alpine的区别:alpine更小,用的是另一套 C 库,某些预编译的二进制包在上面跑不起来,需要自己编译,编译一次能耗掉半小时。我的经验是,能用 slim 就别用 alpine,除非你对体积极度敏感并且验证过所有依赖都能装。

WORKDIR设工作目录,等价于后面的指令都在这个目录下执行。不设的话默认在根目录,COPY和RUN的路径都得写全,容易出错。

ENV设环境变量。TZ这条特别重要,不设的话容器里时间是 UTC,日志时间戳跟本地差八小时,排查问题时能把人看晕。PYTHONUNBUFFERED=1是让 Python 的输出不缓冲,不加这个,docker logs可能半天看不到一行日志,你会以为程序卡死了。

RUN是构建时执行。这里加了--no-cache-dir,避免 pip 缓存留在镜像里白占空间。同理,如果用了系统的包管理器,安装完记得在同一个RUN里清缓存,因为分层机制下,后面一层删文件并不会让前面那层的体积变小。

CMD是容器启动时的默认命令,用数组形式(exec form)而不是字符串形式。字符串形式会被包一层 shell 执行,导致主进程变成 shell,信号传不到真正的应用进程,docker stop时应用收不到终止信号,只能等超时被强杀,优雅退出逻辑全部失效。这个坑非常隐蔽,表现是"停止容器要等十秒",很多人以为是引擎慢,其实是写法问题。

EXPOSE只是声明,不做实际映射,真正对外还得靠-p。写它的价值是让读 Dockerfile 的人知道这个服务监听哪个端口。

4.2 构建命令和标签规范

docker build -t myapp:1.0.0 -f Dockerfile .

-t打标签,格式是名字:版本。版本号别用 latest,尤其是要上服务器的镜像。我见过太多次"本地测试好的 latest,到服务器上拉到的是半年前推的旧版",排查半天。用日期加序号,或者干脆用代码提交号,都能保证唯一性。

-f指定 Dockerfile 路径,默认是当前目录的Dockerfile。项目里如果有多个 Dockerfile(比如开发用、生产用各一份),这个参数就派上用场了。

最后那个.是构建上下文,意思是把当前目录作为上下文传给引擎。注意它不是"在当前目录构建"的意思,而是"把这个目录的内容发过去"。所以上下文越大越慢,.dockerignore一定要配。

构建完验证一下镜像大小:

docker images myapp

如果发现体积远超预期,用docker history myapp一层层看,通常是某个RUN装多了东西。我遇到过一次镜像 2G,最后定位到是把整个数据目录拷进去了,加一行.dockerignore直接降到 200M。

4.3 多阶段构建:把编译环境和运行环境分开

编译型语言和前端项目尤其需要多阶段构建。原理是在一个 Dockerfile 里写多个FROM,前面阶段负责编译,后面阶段只拷编译产物。

FROM node:20-alpine AS builder WORKDIR /src COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:1.25-alpine COPY --from=builder /src/dist /usr/share/nginx/html

这样最终镜像里没有 node、没有源码、没有 node_modules,只有一个静态文件目录加一个 nginx,体积从七八百兆降到几十兆。体积小的直接好处是拉取快、启动快、攻击面小。

用多阶段构建时有个细节:--from=builder后面的路径必须是绝对路径,写相对路径会找不到文件。另外阶段名用AS定义,起个有意义的名字,别用默认的序号,回头改起来容易乱。

提示:构建参数用ARG传,运行时配置用ENV。ARG只在构建阶段有效,不会留在最终镜像里;ENV会固化进镜像,谁拉下来都能看到。所以密钥、令牌这类东西绝对不能用ENV,构建参数也别用,正确做法是运行时通过环境变量或者密钥文件注入。

5. 数据持久化与网络:容器真正可用的前提

5.1 三种挂载方式的选择逻辑

容器删了数据就没了,这是新手最容易吃亏的地方。解决办法是挂载,把数据放在容器外面。三种方式:

方式写法数据位置适用场景
绑定挂载-v /host/path:/container/path宿主机指定目录配置文件、代码热更新
数据卷-v myvol:/container/path引擎管理的目录数据库、需要备份的数据
临时文件系统--tmpfs /tmp内存缓存、临时文件

绑定挂载最直观,宿主机路径你自己定,文件直接能看能改。缺点是依赖宿主机的目录结构,换台机器路径不一样就跑不起来,可移植性差。配置文件、本地开发的代码目录用这个很方便。

数据卷由引擎管理,位置在数据目录下的volumes里,具体路径不用你操心。好处是可移植、能统一备份、多个容器能共享。数据库的数据目录强烈建议用数据卷,别用绑定挂载。原因是权限和 SELinux 标签这类问题,绑定挂载容易踩,数据卷省心。

docker volume create dbdata docker run -d --name db -v dbdata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=xxx mysql:8.0

这里有个特别容易踩的坑:宿主机目录的权限和容器内用户的 UID 不匹配。比如容器里用非 root 用户跑,UID 是 1000,挂上去的宿主机目录属主是 root,容器一写就报权限拒绝。表现是应用日志里一堆Permission denied。解决方式要么提前chown好宿主机目录,要么在 Dockerfile 里把容器内用户的 UID 调整成跟宿主机一致。这个坑几乎每个用绑定挂载跑非 root 容器的人都会撞一次。

还有一个隐蔽的坑:从宿主机挂一个空目录到容器里的配置目录,容器启动时发现配置没了,直接报错退出。因为挂载是覆盖式的,宿主机目录空,容器里原有内容就被盖掉了。正确做法是先把默认配置拷出来再挂,或者用docker run时通过--mount的bind-propagation之类参数控制,但最省事的还是先拷出来。

5.2 端口映射和网络模式

端口映射的写法前面提过,这里补充一个安全细节:

-p 8080:80 # 监听所有网卡 -p 127.0.0.1:8080:80 # 只监听本机

前者意味着同一网络里的其他机器都能访问这个端口。本地调试用的数据库、管理后台,一律用后者,前面加127.0.0.1。这个习惯能避免很多"我的数据库怎么被扫了"的事故。

网络模式常用的有几种:

  • bridge:默认模式,容器有独立网络栈,通过端口映射对外。
  • host:容器直接用宿主机网络,没有端口映射这一步,性能略好但端口会冲突,隔离性差。
  • none:只有回环,完全断网,用于纯计算任务。
  • 自定义网络:自己建一个桥接网络,把相关容器放进去,容器之间可以用容器名互相访问。

最后这个最实用。默认桥接网络下容器之间只能用 IP 互访,容器重启 IP 会变,配置就失效了。自定义网络自带 DNS 解析,直接用服务名当主机名:

docker network create appnet docker run -d --name db --network appnet mysql:8.0 docker run -d --name api --network appnet -e DB_HOST=db myapp:1.0.0

DB_HOST=db就能连上,不用管 IP。这个机制在编排多服务项目时是基础,值得单独练一遍。

5.3 容器访问宿主机服务的正确姿势

有个高频问题:容器里要连宿主机上跑的服务,地址写什么?写127.0.0.1一定不通,因为那是容器自己的回环。Linux 上一般是走默认网桥的网关地址,通常在172.17.0.1这个段,具体用docker network inspect bridge查网关字段确认。桌面版环境提供了专门的主机名,可以直接用。

不过更推荐的做法是:别让容器去连宿主机上的服务。要么把那个服务也容器化,放进同一个网络;要么从宿主机连容器,方向反过来。前者干净,后者简单。容器连宿主机这种拓扑,配置里写死 IP,换环境就崩,属于技术债。

6. 用编排文件管多服务项目

6.1 一个文件描述整个项目

单容器用命令跑还行,一旦有应用、数据库、缓存三四个服务,命令会长到没法维护。编排文件解决这个问题:

services: api: build: . ports: - "127.0.0.1:8000:8000" environment: DB_HOST: db REDIS_HOST: cache depends_on: db: condition: service_healthy restart: unless-stopped db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: change_me MYSQL_DATABASE: app volumes: - dbdata:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 cache: image: redis:7-alpine restart: unless-stopped volumes: dbdata:

这份文件里几个点值得展开。

depends_on加condition: service_healthy是关键。只写服务名的话,它只保证启动顺序,不保证对方真的能提供服务。数据库容器启动到能接受连接有好几秒空档,应用这时连上去就是连接拒绝,然后退出。加上健康检查条件,编排会等到对方真的健康才启动依赖方。这个改动能把一类"第一次启动必挂,重启就好"的玄学问题彻底消掉。

healthcheck的test命令要在容器内可执行。用mysqladmin ping这类内置工具最稳,别依赖外部脚本。

restart: unless-stopped让容器异常退出后自动拉起,但如果是我手动stop的,就不自动拉。生产环境基本都要配,不配的话机器重启或者进程崩溃就再也不会起来了。

6.2 常用命令和几个容易误会的点

docker compose up -d docker compose ps docker compose logs -f api docker compose down docker compose down -v

up -d后台启动全部服务,首次会构建镜像。down停止并删除容器和网络,但保留数据卷。down -v会连数据卷一起删,这条命令要格外小心,数据库数据就没了,别在生产环境手滑。

另一个误会:改了文件里镜像版本或者环境变量,只restart是不生效的。restart只是重启现有容器,配置变更需要up -d重新创建容器。这个差别我见过很多人搞混,改了半天配置发现没反应,以为自己改错文件了。

docker compose config可以校验文件语法并输出解析后的完整配置,变量替换的结果也能看到,排查"环境变量没生效"这类问题很好用。

6.3 上生产前必须补的几件事

编排文件在本地跑通很轻松,上生产要补的东西不少。

资源限制要加,不然一个服务内存泄漏能把整台机器拖垮:

deploy: resources: limits: memory: 512M

日志要限大小。默认的日志驱动不限制,一个高频打日志的服务几天就能写满磁盘:

logging: driver: json-file options: max-size: "10m" max-file: "3"

这两条属于"不写迟早出事"的配置。我在一个内部项目上就吃过亏,一个服务疯狂打错误日志,磁盘满了之后数据库直接写不进去,排查了两小时才发现是日志文件几十个 G。

再加一条:镜像标签固定版本,别用 latest。生产环境拉镜像的时候,不确定自己拉到了什么版本,出问题没法复盘。

7. 常见问题与排查实录

7.1 容器启动就退出,从哪开始查

这是最高频的问题。排查顺序建议固定下来,别乱试。

第一步,docker ps -a看状态和退出码,对着前面那张表判断方向。第二步,docker logs 容器名看输出,注意有些应用日志走文件不走标准输出,这种情况日志里可能是空的。第三步,docker inspect看State里的Error和OOMKilled。第四步,实在看不出来,把镜像的启动命令改成sleep 3600或者覆盖成sh,进去手工执行原来的启动命令,报错会直接打在眼前。

有一个很典型的原因:容器的主进程必须是前台运行的。有些人在启动脚本里写service xxx start或者用&后台跑,脚本一结束,容器就认为主进程退出了,直接停。正确做法是让真正的前台进程作为容器的 1 号进程,比如直接CMD ["nginx", "-g", "daemon off;"]。

7.2 端口连不上,分层排查

从外到内一层层查,别跳步。

先确认容器在跑:docker ps。再确认端口映射对不对:docker port 容器名。然后从宿主机上测:curl 127.0.0.1:端口。宿主机通、外部不通,多半是防火墙或者只监听了127.0.0.1。宿主机也不通,进容器测:docker exec -it 容器名 sh -c "curl 127.0.0.1:容器内端口"。

容器内通、宿主机不通,通常是两个原因:应用只监听了容器内的127.0.0.1而不是0.0.0.0,这种在容器场景下必然连不上,因为请求是从外面进来的,不在同一个回环里;或者端口映射写反了。前者是最常见的,改应用配置让它监听所有网卡即可。

容器互联不通,先确认是不是在同一个自定义网络里,再用容器名互 ping。默认桥接网络下容器名是不解析的,这个必须记住。

7.3 磁盘告警和清理的正确手法

磁盘满了先看占用分布:

docker system df

会分别列出镜像、容器、数据卷、构建缓存占了多少。清理要分情况,别上来就prune -a。

docker image prune # 清悬空镜像(无标签的) docker container prune # 清已停止的容器 docker builder prune # 清构建缓存 docker system prune -a # 清所有未使用的镜像和缓存

最后一条威力很大,会把所有没被容器使用的镜像都删掉,包括你精心构建的、只是暂时没跑的那些。生产机器上执行前一定确认清楚,或者干脆不用这条,用前面几条有针对性的。

还有一类空间占用容易被忽略:容器日志。一个跑了几周的容器,日志文件可能几个 G。查一下:

du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail

从根上解决还是靠前面说的日志大小限制,历史遗留的可以临时清空文件(truncate -s 0 文件),别直接rm,删了文件句柄还开着,空间不会释放。

注意:数据卷是prune默认不动的(需要显式加--volumes),这点设计得比较稳妥。但也意味着卷会一直占着空间,定期用docker volume ls看一下有没有废弃的,确认无用再删。

7.4 一份速查表

现象最可能原因一句话解法
启动即退出,退出码 1应用自身报错看日志,手工执行启动命令复现
退出码 137内存超限被强杀查 OOMKilled,调限制或查泄漏
停止容器要等十秒CMD 用了字符串形式改成数组形式
改了代码容器里没变没重新构建镜像重新 build 并重建容器
挂载目录写入权限拒绝UID 不匹配统一宿主与容器用户 UID
外部访问不了端口只监听 127.0.0.1改为监听 0.0.0.0
容器名互相 ping 不通不在同一自定义网络建网络并加入
磁盘满镜像、缓存、日志堆积docker system df定位后分项清理
日志时间差八小时未设时区设TZ环境变量
首次启动连不上数据库依赖未就绪加健康检查 + 条件依赖

8. 我个人踩出来的几条经验

先说一个很多人纠结的点:镜像要不要追求极致小。我的看法是,除了要频繁分发、或者出口带宽紧张的场景,普通业务镜像控制在几百兆以内就够用了,为了省几十兆去折腾 alpine 的兼容性问题,投入产出比不高。真正该抠的是构建速度,改一行代码等五分钟才能看到效果,这个才是天天在消耗的成本。

第二个经验是关于调试习惯。我现在看任何容器问题,第一反应都是先把它变成一个能交互的环境。覆盖启动命令进容器、手工跑一遍、把每一步拆开验证,比盯着日志猜要快得多。日志能告诉你结果,但告诉不了你过程。

第三个,关于"容器里能不能改东西"。答案是能改,但要有意识:这是临时验证,改完记得落回镜像或者挂载。我见过太多人直接在容器里改配置,重启后发现全没了,又来问是不是引擎有 bug。不是 bug,是设计如此。

最后一个技巧,善用--rm和一次性容器。临时跑个命令、验证个工具,加--rm让容器跑完自动删除,别让一堆exited的僵尸容器堆在ps -a里,时间长了看着就烦,也占着磁盘。常用的调试方式是这样:

docker run --rm -it --network appnet myapp:1.0.0 sh

进去直接在同网络里测连通性,测完退出,干干净净。这个习惯建立起来之后,很多"connect refused"的问题五分钟就能定位,不用再瞎猜。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 5:41:49

Linux TCP编程中errno与连接状态的5个硬核陷阱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:41:09

Kubernetes Pod启动从90秒优化到8秒:本地Harbor私有镜像仓库加速实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:40:36

ESP32-S3量产交付:Windows一键烧录包完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:40:09

DDU显卡驱动彻底清理指南:原理、实操与避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:40:03

汽车ECU BootLoader升级实战:PBL/SBL架构与UDS刷写全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:39:23

Ubuntu固定IP配置全攻略:Netplan原理、实操与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华