1. 先把 Docker 的参数逻辑理顺:镜像、容器、守护进程三层关系
在真正开始记 docker run 那一大堆--xxx之前,我强烈建议你先明白一件事:Docker 里几乎所有参数都不是在给命令"加选项",而是在给"容器运行环境"和"进程启动方式"写一份完整配置。
docker CLI 只是客户端,你敲的每一条命令都会发给 dockerd 守护进程,由它调用 containerd、runc 等底层组件完成容器创建、资源隔离和文件系统挂载。这意味着命令行里的参数绝大多数会被组装成一份容器配置描述,再交给运行时执行。这个视角一旦建立,看到--cpus会想到 cgroup,看到--network会想到网络命名空间,理解成本会大幅下降,记忆也不再是一堆孤立词条。
1.1 参数最终落在内核的哪些机制上
如果你把一次 docker run 拆开看,所有参数大概落在四个层面:
- 镜像与构建层:使用哪个镜像、哪个 tag、是否拉取、是否指定平台、是否覆盖入口;
- 容器运行时层:容器名、主机名、DNS、网络模式、端口映射、数据卷挂载、rootfs 是否只读;
- 资源与安全层:CPU、内存、PID、文件描述符限制、capability 增删、SELinux/AppArmor 配置、是否特权模式;
- 进程与环境层:环境变量、工作目录、运行用户、主进程命令、健康检查命令、停止信号。
这四层和 Linux 隔离机制严格对应:网络命名空间对应网络参数,mount 命名空间对应 volume 参数,cgroup 对应资源限制,用户命名空间和 capability 机制对应安全参数。所以你在宿主机上执行docker inspect看到的一堆 JSON,其实就是这四层配置的落地结果。
1.2 为什么同一个功能有多个写法
Docker 的 CLI 演进过好几轮,早期参数比较随意,一个短字符经常承载多个含义;后来为了工程化,推出了更严格、更结构化的长参数格式。最典型的就是挂载卷有-v和--mount两种写法,发布端口有-p和--publish。
我的建议是:查全量参数时优先理解长参数,因为它语义明确,还支持更多扩展子参数;短参数适合命令行快速操作,两者并不矛盾。比如-v适合临时调试,--mount适合写进脚本或 compose 配置,可读性完全不同。
2. docker run 参数全拆解:最常用的启动入口
docker run 是使用频率最高、参数也最多的命令。它本质上是 docker create 加 docker start 的合并操作。它后面的所有参数,几乎可以组合出任何你想要的容器运行方式。
2.1 前台交互与后台运行:-d、-it、--rm 的取舍
先看三组最基础、也最容易被搞混的参数:
-d或--detach:容器以后台守护方式运行,启动后立即返回终端,日志不会直接打到当前终端,要用docker logs查看;-i与-t:-i表示保持标准输入打开,-t表示给容器分配伪终端,两者组合成-it,用于需要交互的容器;--rm:容器退出时自动删除容器及其文件系统,适合临时测试,和-d不冲突。
这里有个非常经典的坑:你执行docker run -d centos会发现容器秒退。原因是 CentOS 这类系统镜像默认命令是bash,没有-it分配终端,bash 在非交互模式下读到 EOF 就退出,主进程一结束,容器生命周期就走完了。所以判断容器能否长期运行,关键看主进程是否阻塞式地活着,而不是看加没加-d。
我举个实际例子。某次我在 CI 里写docker run --rm -d --name tempworker redis:7-alpine,然后另一台机器连上去执行 redis-cli 一直失败。查日志发现容器确实在运行,但 redis 默认没配密码,监听地址也没对外开放。这种问题不是参数错误,而是参数组合后的外部表现,但确实是新手最容易在 run 阶段遇到的困惑。
2.2 资源限制参数:CPU、内存、IO 控制在生产环境不能省
生产环境容器不可能裸奔,资源限制必须设置。Docker 的 CPU、内存限制最终都会落到 cgroup 上:
--memory或-m:限制容器最大可用内存,例如--memory=1g。注意它会联动限制 swap,具体行为看--memory-swap;--memory-swap:表示 memory+swap 的总量。比如--memory=1g --memory-swap=1g表示不允许使用 swap;--memory=1g --memory-swap=2g则允许额外使用 1g swap;--cpus:限制容器可使用的 CPU 核心数,支持小数,例如--cpus=1.5;--cpu-shares:相对权重值,默认 1024,值越大在 CPU 争抢时获得的时间片比例越高,但它不是绝对数量限制;--pids-limit:限制容器内 PID 数量,防止某个应用 fork 失控耗尽宿主资源;--ulimit:设置容器内进程的资源限制,比如--ulimit nofile=65536:65536修改文件描述符数量。
特别提醒:--cpus不等于 CPU 亲和性,它只限制 CPU 时间配额。--cpus=1.5表示容器最多使用 1.5 个 CPU 核的总计算时间,但具体跑在哪几个核上由内核调度决定。如果服务对延迟敏感,需要绑定特定核心,请用--cpuset-cpus。这两个参数经常被混为一谈。
内存方面还有个容易踩的坑:默认到上限后内核会触发 OOM,要么回收、要么杀进程。常见做法是设置 JVM-Xmx时留出 Native Memory 和 Metaspace 的余量,再用容器限制兜底。我见过太多容器完全没设内存限制,某个线上服务一波动,直接把整台宿主机打挂,教训相当深刻。
2.3 安全与特权参数:--privileged、--cap-add、--security-opt 的边界
容器不是虚拟机,默认情况下容器内用户对设备、系统调用的访问受 Linux capabilities 机制限制,Docker 会默认屏蔽 mount、sys_time、net_admin 等高危权限。常用参数如下:
--privileged:相当于赋予容器几乎全部 capabilities,同时放开 device cgroup 限制。能让容器随意挂载设备、操作网络、加载内核模块,但隔离性急剧下降,能不用尽量不用;--cap-add:单独增加某个 capability,例如--cap-add=NET_ADMIN,容器内就可以配置 iptables 规则,但不会获得其他权限;--cap-drop:移除默认赋予的 capability,常用于安全加固,比如--cap-drop=ALL先全部去掉,再按需--cap-add;--security-opt:配置 SELinux 标签、AppArmor profile、no-new-privileges等,后者能防止容器内进程通过 setuid 提权;--device:直接把宿主设备映射进容器;--device-cgroup-rule用来动态放行设备规则,比--privileged精细得多。
我自己的运维习惯是:能精确到 capability 就绝不用--privileged。比如某个日志采集容器需要读取块设备做统计,我会把对应设备文件映射进去,再按需加SYS_RAWIO之类能力。看起来麻烦,但这是线上风险控制的基本功。这里还要纠正一个误解:--privileged不是万能的,有些操作涉及内核模块、系统启动参数等更深层的限制,给了权限也未必成功。
2.4 端口与网络参数:-p、-P、--network、--network-alias、--add-host
端口发布是最常见的需求,几乎每个 Web 服务容器都要用到。常用参数拆解:
-p或--publish:格式是[宿主机IP:]宿主机端口:容器端口/协议,例如-p 8080:80、-p 127.0.0.1:8080:80/tcp。不指定协议时默认同时覆盖 tcp 和 udp;多次使用-p可以发布多个端口;-P或--publish-all:把容器内通过 EXPOSE 声明的端口全部随机映射到宿主机高位端口,配合docker port查看;--expose:只声明暴露哪些端口,不实际发布到宿主机,适用于同网络内容器间访问;--network:指定加入哪个网络,常见有 bridge、host、none、overlay、macvlan,以及用户自定义的 bridge 网络;--network-alias:容器在该网络内的额外别名,多容器通过别名解析时很实用;--add-host:往容器/etc/hosts追加记录,形如--add-host=myhost:192.168.1.10。
端口发布最大的坑是:容器内服务只监听127.0.0.1时,即使写了-p也会连接失败。比如很多框架默认 localhost 模式,容器外完全进不去。排查时先进容器确认监听地址是不是0.0.0.0,不要上来就怀疑端口映射写错。另一个容易忽略的点:-p会帮你在宿主机 iptables/nftables 里加端口转发规则,如果规则有冲突或系统ip_forward未开启,映射会失败。我曾在网络隔离很严的主机上部署容器,docker run一直提示端口绑定失败,最后查出来是防火墙策略把转发禁掉了,和 Docker 本身无关。
2.5 环境变量与参数文件:-e 和 --env-file
容器内应用配置大多通过环境变量传入:
-e或--env:直接设置,例如-e TZ=Asia/Shanghai、-e MYSQL_ROOT_PASSWORD=xxx;--env-file:从文件读取环境变量,文件每行KEY=VALUE,支持注释;--env的优先级高于--env-file;--env-file不会自动解析引号里的特殊字符,密码里如果包含空格、$、引号,建议用编码或安全方式处理。
环境变量是否生效完全取决于镜像的启动脚本怎么写,不要想当然认为所有镜像都接受同一套变量。比如跑 MySQL,镜像里的 docker-entrypoint.sh 会读取MYSQL_ROOT_PASSWORD、MYSQL_DATABASE这些变量;跑 Redis,则需要用命令行参数或配置文件方式设置密码,环境变量并不能直接改 redis.conf。遇到变量不生效时,第一反应应该是去看镜像的 entrypoint 脚本,而不是反复改 Docker 参数。
3. 镜像构建、拉取与传输参数:从 build 到 save/load
容器运行之外,镜像相关的参数体系同样庞大。这节我们把拉取、构建、标记、传输这些环节的参数拆开讲清楚。
3.1 docker pull 与多架构平台参数
docker pull本身参数不多,但有一个关键参数容易被忽视:
--platform:指定拉取远程平台的镜像,例如在 amd64 机器上临时拉取 arm64 镜像做交叉验证:docker pull --platform linux/arm64 nginx:alpine;--all-tags:拉取某个仓库的全部 tag,在镜像数量可控的私有仓库里很有用,但公共镜像慎用,会拉下海量数据;--quiet或-q:不打印进度信息,脚本里比较干净。
多架构镜像是当前镜像仓库的主流形态,registry 会根据请求端的架构自动返回对应 manifest。但如果你需要在构建机上为其他平台准备镜像,--platform就派上用场了。这个参数能帮你省掉大量临时换机器测试的时间。
3.2 docker build 的上下文与缓存参数
docker build的参数里,最核心的有几个:
- 上下文路径:命令尾部那个路径,例如
.,Docker 会把该目录整体打包发给守护进程,所以.dockerignore很重要。如果忘了排除node_modules、.git这类目录,每次构建都会传输海量无效文件,速度慢到怀疑人生; -f或--file:指定 Dockerfile 路径,默认是./Dockerfile;-t或--tag:给镜像打标签,可多次使用,例如-t myapp:v1 -t myapp:latest;--build-arg:传入构建期变量,格式--build-arg VERSION=1.2,对应 Dockerfile 里的ARG;--no-cache:禁用构建缓存,排查缓存导致的旧依赖问题时非常有用;--target:多阶段构建中只构建到某个指定阶段。
构建缓存的优化思路很重要。尽量按"缓存友好顺序"排列 Dockerfile:先 COPY 依赖声明文件,再 RUN 安装依赖,最后 COPY 代码。这样代码变化时依赖层能命中缓存。很多初级开发者把 COPY 整个项目放在最前面,改一行代码就要重新安装所有依赖,这是很典型的构建效率陷阱。
另外要注意:ARG值变化会导致后续层缓存失效,所以频繁变化的构建参数应该尽量往后放。多阶段构建配合--target可以只构建测试阶段或产物阶段,在 CI 里能把构建时间压到很短。
3.3 tag、push、save、load 的参数细节
镜像的标记和传输也有一组固定套路:
docker tag:给镜像打多个标签,例如docker tag myapp:1.0 registry.example.com/myapp:1.0;docker push:推送镜像到仓库,--quiet可以让输出干净一些;docker save:把镜像导出为 tar 包,常用参数-o,例如docker save -o nginx.tar nginx:alpine;docker load:从 tar 包导入镜像,常用参数-i,例如docker load -i nginx.tar;docker rmi:删除镜像,-f强制删除,但要注意正在运行容器的镜像不能直接删;docker prune:清理未使用的镜像,例如docker image prune -a删除所有未被容器引用的镜像。
save和load在内网离线部署时几乎是必备技能。我在无外网环境里部署服务,流程通常是这样:先在联网机器上 pull 镜像,再docker save -o导出,scp 到目标机器,然后docker load -i导入,最后 docker run。这里容易犯的错是:docker save -o导出的 tar 包含多个 tag 时,load之后镜像名可能和预期不一致,所以保存前最好先确认 tag 列表。
4. 容器生命周期管理参数:ps、exec、logs、inspect 与清理
容器创建只是第一步,后续的运维操作同样离不开参数。这一节把日常管理命令里值得注意的参数全部过一遍。
4.1 docker ps 的过滤与格式化
docker ps的参数看起来简单,实际信息量很大:
-a:查看所有状态的容器,包括 Exited、Created;-q:只输出容器 ID,脚本里批量处理非常常用,比如docker rm -f $(docker ps -aq)清掉所有容器;-s:显示容器占用的磁盘总大小;--filter或-f:过滤容器,常用写法有--filter status=exited、--filter ancestor=nginx、--filter name=web、--filter label=xxx;--format:自定义输出,Go template 语法,例如docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"。
实际运用中我最常用的是docker ps --filter "status=exited"找出已关闭容器再批量清理。但这里必须强调:docker rm -f $(docker ps -aq)非常危险,会删掉所有容器,包括正在提供服务的容器。公司新同学在开发机敲过一次,把自己的数据库容器删了,当场心态崩了。另外 filter 里的 name 是前缀匹配,不是完全匹配,过滤web会把web1、webapp都匹配出来,自动化脚本里要特别注意。
4.2 docker exec 的参数
docker exec是在运行中的容器里执行新进程,语法和docker run后半部分很像:
-i:保持标准输入打开;-t:分配伪终端,和-i组成-it进入交互式 Shell;-u:指定容器内运行命令的用户,比如-u root或--user 1000:1000;-w:指定工作目录;-e:给这次执行临时注入环境变量,只在本次进程中生效;--env-file:同理,临时生效。
有个很容易被忽略的坑:如果镜像的 Dockerfile 指定了USER app,那docker exec默认也以 app 用户运行,除非显式指定-u root。排查以非 root 身份启动的服务时,一定要记得这个机制,否则你看到的"权限不足"可能不是文件权限,而是用户身份不对。
4.3 docker logs 和 docker inspect
docker logs查看日志,常用参数:
--tail:指定最近行数,例如--tail 100;-f或--follow:跟随输出;--since/--until:按时间过滤,例如--since 5m、--since 2025-01-01T00:00:00;--timestamps:显示时间戳。
docker inspect查看容器或镜像的底层配置,输出是 JSON。配合--format做精简提取:
docker inspect --format '{{.State.Pid}}' 容器名:获取主进程 PID;docker inspect --format '{{json .Mounts}}' 容器名:查看挂载详情;- 常用字段包括
.HostConfig(资源限制、端口绑定)、.NetworkSettings(IP、网关)、.State(运行状态、ExitCode、OOMKilled)。
这里有一个值得培养的习惯:容器异常退出时,不要只盯日志,先跑docker inspect看State.OOMKilled。如果值为true,说明是内存超限被内核杀掉,日志里通常没有直接报错,只会看到进程消失。我排查过某个 Java 服务频繁重启,docker logs里只有 JVM 启动日志,翻到最后一句戛然而止,后来 inspect 一看OOMKilled: true,立刻锁定是内存限制太小,调大后问题消失。不懂 inspect 参数的话,不知道要浪费多少时间在错误方向上。
4.4 停止、删除与资源清理参数
容器停止和删除也有各自的参数细节:
docker stop:默认发 SIGTERM 给主进程,等优雅退出,宽限期默认 10 秒;超时可用-t指定,例如docker stop -t 30 容器名;docker kill:直接发 SIGKILL,也可用--signal指定其他信号;docker rm:删除已停止容器,-f强制删除运行中的容器;-v同时删除关联的匿名卷,这对数据清理很关键;docker system prune:清理未使用的镜像、容器、网络和构建缓存,加-a --volumes会连未引用的数据卷一起清除,务必谨慎。
停止容器的信号机制容易被忽视。如果你的容器主进程是 shell 启动的,shell 可能不会把 SIGTERM 转发给子进程,导致优雅退出失败,最后被强杀。这个问题的源头通常不在 docker 参数,而在镜像的 ENTRYPOINT 写法,后面 Dockerfile 部分会详细说。
5. Dockerfile 编译期参数:镜像不是靠命令堆出来的
镜像构建是整个 Docker 体系中最需要理解参数含义的环节。很多人以为 Dockerfile 就是把 Linux 命令罗列一遍,其实指令参数的选择直接决定镜像层数、缓存命中率、安全属性和最终运行时的进程行为。
5.1 FROM、RUN、COPY、ADD 与构建上下文
FROM:基础镜像来源,可加--platform指定平台,例如FROM --platform=linux/amd64 alpine:3.19;RUN:执行构建命令,两种写法:Shell 形式RUN apt-get update && apt-get install -y xxx和 Exec 形式RUN ["apt-get","install","-y","xxx"];COPY:从构建上下文复制文件进镜像,支持--from在多阶段构建里复制上一阶段产物;ADD:比 COPY 多了解压 tar 和识别远程 URL 的能力,但行为隐式,官方建议默认用 COPY,只有明确需要解压时才用 ADD;--chown:复制文件时直接指定所有者,例如COPY --chown=app:app app.jar /app/app.jar,避免在镜像里多执行一次 chown 生成多余层。
构建上下文之前提过,再强调一层:你运行docker build .时,后面的路径就是上下文,Docker 会把目录打包发送给守护进程。.dockerignore直接影响构建效率。如果一个项目没写.dockerignore,每次构建传一堆日志、依赖、临时文件,CI 慢到怀疑人生,这不是 Docker 本身慢,而是上下文太大。
优化逻辑上,RUN 命令按"更新频率从小到大"排列,经常变化的 COPY 放在靠后位置,能最大程度利用缓存。比如先COPY package.json,再RUN npm install,最后COPY . .,这样代码改动时依赖层缓存依然命中。
5.2 CMD 与 ENTRYPOINT 的两层设计
容器启动的进程行为由 ENTRYPOINT 和 CMD 共同决定:
ENTRYPOINT:定义主进程程序和固定参数,通常不会被 docker run 后面的命令行覆盖,但--entrypoint可以覆盖它;CMD:提供默认参数,可以被 docker run 后面的命令行整体替换;- 两者同时存在时,CMD 的内容会追加到 ENTRYPOINT 后面作为默认参数。
举个例子。假设 Dockerfile 写ENTRYPOINT ["nginx"],CMD ["-g","daemon off;"],那docker run mynginx实际执行的是nginx -g daemon off;。如果执行docker run mynginx -t,CMD 被覆盖为["-t"],最终进程是nginx -t,用来测试配置。反过来,docker run busybox echo hi里的echo hi会整体替换 CMD,而不是 ENTRYPOINT。
这里有个非常隐蔽的坑:ENTRYPOINT nginx这种 Shell 写法,会在容器内先启动一个 shell 再启动 nginx,shell 收到 SIGTERM 时不会自动传给子进程,docker stop无法优雅关闭容器,只能等超时后被强杀。生产镜像建议统一使用 Exec 形式,也就是 JSON 数组写法。这个问题我在排查服务"优雅退出失效"时碰到过好几次,根源都在这里。
5.3 ARG 与 ENV 的边界:构建期和运行期
ARG:只存在于构建过程,用--build-arg从外部传入,对应docker build --build-arg VERSION=1.2 .;ENV:既影响构建过程,也写入镜像的运行时环境变量;ENV变量不会自动传递到 RUN 的 Shell 子进程,需要在同一 RUN 里显式使用或 export;- 可以用 ENV 固化 ARG:
ARG APP_VERSION=1.0 ENV APP_VERSION=$APP_VERSION,这样构建期传入的版本号会固化到运行环境。
还有个常见 bug:--build-arg传参时,必须先在 Dockerfile 里用ARG声明对应名字,否则命令行里传了也会被忽略。很多人遇到--build-arg 不生效,第一反应是命令写法问题,其实是 Dockerfile 里根本没有声明。这个参数传递链路一定要记住。
5.4 HEALTHCHECK、EXPOSE、STOPSIGNAL 等进程参数
HEALTHCHECK:定义健康检查方式,重要参数有--interval检查间隔、--timeout超时、--start-period启动宽限期、--retries连续失败次数。检查结果可通过docker inspect查看;EXPOSE:只声明端口,不发布端口,如果镜像里没写 EXPOSE,-P不会映射任何端口;STOPSIGNAL:设置容器停止时发送的信号,默认 SIGTERM,某些应用需要改成 SIGQUIT 等信号才能优雅退出;WORKDIR:设置工作目录,影响 RUN、CMD、ENTRYPOINT、COPY、ADD 的默认路径;USER:指定最终进程的运行用户,强烈建议生产镜像避免以 root 运行;VOLUME:声明匿名卷挂载点,启动时即使没挂载,也会创建匿名卷;SHELL:修改 Shell 形式指令的默认 Shell。
特别提一下HEALTHCHECK的 start-period。很多人只设置检查间隔,结果容器还在初始化数据库,健康检查就连续失败,调度系统判定不健康,服务被过早杀掉。正确做法是给足够启动时间,比如 MySQL 初始化可能要几十秒,start-period 40s很常见。这个参数不解决应用启动慢的问题,但能避免误杀。
6. 数据卷与网络专项参数:持久化、连通性与隔离
运行状态之外,容器最容易被忽略的两个领域就是卷和网络,它们的参数直接影响数据安全和集群可访问性。
6.1 -v 与 --mount:两种写法与权限问题
挂载卷有两种语法:老式-v简洁,源路径:目标路径[:模式],比如-v /data:/var/lib/mysql、-v myvol:/data(命名卷)、-v /host/path:/container/path:ro(只读挂载)。
--mount是结构化写法,采用type=xxx,source=xxx,target=xxx,readonly的键值对形式,支持 bind、volume、tmpfs、npipe 类型。还能设置 bind-propagation,例如--mount type=bind,source=/host,target=/container,bind-propagation=rshared。生产环境建议尽量用--mount,语义清晰,更适合写进脚本和维护。
卷与权限是这里最大的坑。很多镜像内进程以非 root 用户运行,比如 uid 1000,但宿主机挂载目录属主是 root,容器内进程无法写入。解决方式有两种:一是创建目录后 chown 成与容器内用户一致的 uid;二是用--user参数让容器进程以宿主机某用户运行。如果不想改宿主目录权限,还可以用 SELinux 的:Z或:z标签。总之,挂载卷的权限问题十有八九是 uid 不匹配,docker exec进去执行id一看便知。
6.2 网络驱动与自定义 subnet 参数
Docker 默认提供 bridge、host、none 三种网络,Swarm 模式还会用到 overlay 和 macvlan:
- bridge:默认模式,容器通过虚拟网桥互通,外部访问依赖端口映射;
- host:容器直接使用宿主网络栈,性能和兼容性最好,但没有网络隔离;
- none:容器没有网络接口,适合纯离线计算任务;
- macvlan:让容器获得独立 MAC 地址,从网络设备视角看就是一台真实主机;
- overlay:跨宿主机容器通信时使用,主要在 Swarm 下用。
docker network create常用参数:
--driver:指定网络驱动;--subnet:指定 CIDR,例如--subnet 172.20.0.0/16;--gateway:指定网关;--ip-range:指定动态分配范围;--attachable:允许非 swarm 容器连接。
同一自定义 bridge 网络里的容器可以直接通过容器名互相访问,这是 compose 项目里服务互相连通的基础。如果需要容器固定 IP,必须在docker network create时指定 subnet,再在docker run --ip里指定。这个参数组合在搭建跨主机通信的开发环境里非常常用。
6.3 日志驱动与 log-opts
Docker 日志系统通过 json-file、journald、syslog、fluentd、gelf 等驱动工作。docker run --log-driver指定日志驱动,默认 json-file 会写本地文件,长期运行可能占用大量磁盘。所以必须配置 log-opts:
--log-opt max-size=10m:单个日志文件大小上限;--log-opt max-file=3:最多保留几个文件;--log-opt mode=non-blocking:写入慢时容器不阻塞,丢弃部分日志避免影响业务;--log-opt tag="custom-{{.Name}}":给日志对象打标识,便于汇聚后过滤。
这里特别提醒:默认 json-file 驱动如果没有设置 max-size 和 max-file,生产环境很容易出现日志堆积把磁盘撑爆。我处理过最典型的情况是接口服务一天输出 GB 级日志,容器本身只有 2G 磁盘空间,最后整台宿主机/var/lib/docker分区被写满,所有容器全部故障。根源就是 run 时没给日志参数设限。所以凡是运维入口,务必主动把日志大小限制在合理范围,这比事后写清理脚本更靠谱。
7. Docker Compose 的字段参数:从单容器到服务编排
单容器用 docker run 足够,但一套完整服务通常包含多个容器:应用、数据库、缓存、消息队列。这时用 Compose 描述服务之间的依赖和网络关系更合适。Compose 文件本质上是把 docker run 参数映射成 YAML 字段,但多了编排层概念。
7.1 services 关键字段与 docker run 参数的对应
services 下常用字段:
image:镜像,对应docker pull+ run 的 image 部分;build:构建配置,对应docker build,包含 context、dockerfile、args;container_name:显式指定容器名;ports:发布端口,格式"宿主机端口:容器端口",也可以用 target/published/protocol 长写法;expose:只暴露端口给网络内其他服务;environment:环境变量,类似docker run -e,也支持env_file;volumes:挂载配置,可用短语法或长语法;restart:重启策略,no、always、on-failure、unless-stopped;command:覆盖镜像 CMD;entrypoint:覆盖镜像 ENTRYPOINT;healthcheck:健康检查,字段和 Dockerfile HEALTHCHECK 一致;depends_on:服务启动顺序依赖;deploy:部署资源限制配置。
新版 compose.yaml 已经不再推荐写version字段,早期version: "3"这种写法已经过时,新项目不用再写。这里重点说depends_on:如果只看字面,以为它控制"服务启动完成后再启动下游",但早期版本只控制启动顺序,不保证服务健康。正确的做法是配合 healthcheck 和condition: service_healthy,下游服务才会真正等依赖就绪。
7.2 .env 与变量插值的机制
compose 支持在 .env 文件里定义变量,并在 compose.yaml 中用${VARIABLE}做插值。例如数据库密码可以不再明文写在配置文件里,而是通过 .env 引入,.env 文件可以留在本地不进代码仓库,既保结构又保密级隔离。
这里有个容易混乱的坑:.env文件里的变量和environment字段下的变量是两套机制。environment会直接传给容器,.env只负责 compose.yaml 本身的插值。如果你在 .env 里写了MYSQL_ROOT_PASSWORD,但 compose.yaml 的environment字段没有引用,这个变量不会自动进入容器,很多人在这里兜圈子。
7.3 资源限制与重启策略的高频组合
在 compose 中控制资源限制可以写deploy.resources.limits,但在本地 docker compose 环境下部分字段不一定直接对容器生效。更稳妥的方式是使用 run 时的底层资源限制能力,或者在 compose 里配置明确的配置。实际项目中,我最常用的组合是:
restart: unless-stopped,服务器重启后能自动把服务拉起来;healthcheck设置合理 start-period,避免误判;volumes使用命名卷而不是宿主机路径,便于备份和迁移;networks显式指定自定义网络,把不需要对外暴露的端口只绑定到127.0.0.1。
特别想提醒:不要把数据库放在容器层,一旦容器被删除、升级或异常重建,数据很可能跟着丢失。正确方案是命名卷或宿主机目录持久化。这个参数层面看似简单,背后是容器设计里最核心的"无状态与有状态分离"原则。
8. 高频报错与参数相关的排查记录
最后这部分专门记录我在实际运维中遇到的几个典型问题,它们都和参数理解不到位有关,希望帮你节省排查时间。
8.1 端口映射失败:bind address already in use
这个报错出现时,很多人第一反应是端口被占用,但实际上一半以上情况不是真正的进程占用,而是 docker-proxy 仍在运行,或 iptables 规则残留。处理步骤一般是:
docker ps看是否已有容器占用了端口;lsof -i:8080或ss -tlnp确认宿主端口占用者;- 如果确认规则残留,谨慎重启 docker 服务恢复。
还有一小类场景是容器内服务监听127.0.0.1,即使-p映射关系正确,外部访问仍然失败,因为流量到达容器网卡后,应用没监听容器 IP。排查思路是进容器同时测试curl 127.0.0.1:端口和curl 容器IP:端口,基本一测便知。
8.2 容器启动后退出:Restart (0) 或 Restarting
看到容器一直 Restarting,第一反应用docker logs看日志,方向是对的。但也别忽略docker inspect里State.ExitCode和State.Error:
ExitCode=127:通常是命令不存在,比如镜像里没有 bash,但 entrypoint 写的是 bash。Alpine 镜像默认是 ash,临时验证可以改用/bin/sh或安装 bash;ExitCode=137或143:进程被 SIGKILL/SIGTERM 杀死,和资源限制、stop 超时、OOM 有关;ExitCode=139:段错误,常见于架构不匹配,比如 amd64 宿主上跑 arm64 镜像,这时要检查--platform参数和镜像 tag。
8.3 权限不足:operation not permitted 与 cannot mkdir
这类报错多半是 SELinux 或 capabilities 问题。如果宿主机开启 SELinux,挂载卷后容器内进程访问宿主目录被拒绝,报错可能只是operation not permitted。解决方式要么给目录做好 SELinux 标签,要么挂载时加:z或:Z。
另一个常见操作是容器内执行 mount、iptables 时被拒绝,一般是因为容器默认缺少相关 capabilities。解决办法不是直接--privileged,而是按需加--cap-add,比如需要 iptables 就--cap-add=NET_ADMIN。长期维护下来,按需加权限既能保住隔离边界,也方便未来安全审计。
8.4 磁盘被 /var/lib/docker 占满
这个问题在上文日志参数里提过,再补充清理思路:docker system prune可以清理未引用的镜像、容器、网络和构建缓存,但加-a --volumes时一定要谨慎,因为它会连所有未使用容器引用的数据卷一起删。稳妥流程是先用docker ps -a确认没有需要保留的容器,再分开执行docker image prune和docker volume prune。
如果磁盘已经写满,Docker 守护进程可能已经无法正常工作,需要先腾出空间再恢复服务。平时就应通过 log-opts 和定期清理策略把风险窗口控制在最小。
这篇笔记基本覆盖了我日常运维和开发中使用 Docker 的绝大多数参数场景。个人体会是,参数本身只是表象,真正值得琢磨的是参数背后的资源隔离、文件挂载、进程模型这三个核心逻辑,只要把这三块想明白,碰到陌生参数时也能马上猜到它属于哪个层面、会带来什么影响。最后分享一个小习惯:每学一个新参数,我都会用docker inspect去看这个容器创建后的实际配置,等于把参数翻译成可观测的底层状态,这个反查过程比背十遍命令都管用。