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 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被强制终止内存超限或手动 kill143收到正常终止信号通常是 stop 导致137 这个码要特别注意。它表示进程收到 SIGKILL最常见的原因是内存超过限制被系统干掉了。这时候去docker inspect看OOMKilled字段是不是 true如果是调大内存限制或者查应用是不是有内存泄漏别在日志里瞎找。3.3 进容器调试exec 和 attach 别用错容器起不来或者行为异常总想进去看看。两种方式docker exec -it web /bin/sh docker attach webexec是在容器里新开一个进程开个 shell 进去看看文件、测测网络退出时不影响主进程。这是首选99% 的场景都用它。attach是接到主进程的标准输入输出上你看到的日志就是主进程的输出这时候按 CtrlC 会把信号直接发给主进程很可能把业务进程干掉。用attach要退出又不杀进程得用CtrlP然后CtrlQ这个组合键不好记我一般是干脆不用 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 TZAsia/Shanghai PYTHONUNBUFFERED1 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日志时间戳跟本地差八小时排查问题时能把人看晕。PYTHONUNBUFFERED1是让 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 --frombuilder /src/dist /usr/share/nginx/html这样最终镜像里没有 node、没有源码、没有 node_modules只有一个静态文件目录加一个 nginx体积从七八百兆降到几十兆。体积小的直接好处是拉取快、启动快、攻击面小。用多阶段构建时有个细节--frombuilder后面的路径必须是绝对路径写相对路径会找不到文件。另外阶段名用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_PASSWORDxxx 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_HOSTdb myapp:1.0.0DB_HOSTdb就能连上不用管 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 -vup -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的问题五分钟就能定位不用再瞎猜。