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 infodocker 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"的问题五分钟就能定位,不用再瞎猜。