1. 全貌梳理:明明只是两条命令,为什么让人这么晕
接手容器化部署有一段时间之后,我发现自己频繁在一个问题上卡壳:团队里讨论docker run和docker exec的人不在少数,但真正能把-i、-t、-d这三个参数组合用明白的,反而不多。最典型的对话就是:
“容器起来了,进去看看。”
“你等一下,我docker exec -it进去。”
“不对,我要的是docker run -itd那个容器,别搞混了。”
这两条命令都是容器技术里最常用的入口,但它们的角色完全不同:docker run是创建新容器并启动它,docker exec是进入一个已经存在的运行中容器去执行额外命令。很多人以为它们可以互换,实际上用错了场景,轻则操作完发现容器被停掉,重则把生产环境里正在跑的服务搞挂。
这篇文章会把整个流程拆开讲清楚。从最基础的安装环境准备开始,先把 Docker 装干净,再集中解决 run 和 exec 的定位差异,然后把 -i、-t、-d 三个参数逐项拆解,结合不同业务的真实场景说明什么时候该用哪种组合,最后整理一份我在实际运维中反复遇到的故障排查清单。适合刚接触容器化部署的人,也适合已经在用 Docker 但对参数细节模棱两可的开发者,看完至少能少踩几个坑。
2. Docker 安装方案解析:把环境一次配到位
安装这件事看起来简单,真到自己上手时容易在版本、权限、镜像源、服务管理这几个环节反复折腾。下面按通用环境梳理一套稳妥的安装思路,Linux 和 Windows/macOS 分开讲。
2.1 Linux 环境下安装 Docker 的两种主流选择
常见的发行版上安装 Docker,经验上就两条路:用系统包管理器装发行版自带的 docker 包,或者使用官方提供的安装脚本。我自己的建议很明确——优先用官方脚本,不用发行版自带的旧包,原因是发行版仓库里的 docker 版本更新滞后,少则落后两三个大版本,多则落后一年多。而容器生态迭代很快,新版不仅修复了大量稳定性问题,还引入了更合理的资源管理机制和网络策略,多个容器并发时的表现差别很明显。
用包管理器的典型流程大致是这样:先更新软件索引,然后安装 docker 相关的包,再启动系统服务,设置开机自启。这里我不写具体发行版的命令,因为不同发行版命名和操作细节有差异,建议以官方文档为准。重点是安装完一定要执行docker info验证一下,如果打印出的信息里有错误内容,说明服务或者权限有问题,不要急着往下配置。
官方脚本方式更省心,下载执行脚本后会自动完成软件源配置、依赖安装、服务启动一整套动作。执行完同样跑一遍docker version和docker info,能正常输出版本号和系统信息就说明核心组件已经就位。
2.2 Windows 和 macOS 的安装要点
桌面端环境通常直接安装配套的容器客户端程序,这类客户端会提供一个图形界面,托盘右键就能管理容器的启停,这对前端或者测试人员特别友好。需要注意几个细节:
第一,这类客户端默认分配的内存不是很大的话,跑多个容器会频繁触发容器 OOM 或者无响应,建议把资源配额往上调整,具体数值取决于你机器内存总量,至少留出 4GB 给容器引擎是比较通用的做法。
第二,文件挂载时如果目录没有共享给容器客户端,容器内看到的是空目录或者直接报错。所以把要挂载的宿主目录先加入到共享列表里,再启动容器。
第三,桌面端吃内存比较明显,跑大数据量任务时电脑会明显卡顿,这是正常现象,不需要额外排查,按需调整资源分配即可。
2.3 镜像加速配置与安装验证
安装完成后有一个步骤几乎必做——配置镜像加速。默认的官方仓库拉取镜像对很多网络环境来说都偏慢,几个 GB 的基础镜像下载到一半断掉也是常事。解决办法是在容器引擎的配置文件里设置 registry-mirror 为可用的公共加速地址,一般选离你机房最近的公共镜像源就行。
配置文件位置和格式不同客户端略有差异,核心是这段 JSON 结构:
{ "registry-mirrors": ["https://替换为可用的镜像加速地址"] }修改完重启容器引擎进程再执行docker info,输出里如果能看到 Registry Mirrors 一段,说明配置生效。
安装验证除了看版本信息,还有一个更直观的测试:拉取一个极小镜像并运行它,如果终端能打印出欢迎信息,说明引擎、网络、镜像源整条链路都是通的,接下来就可以正常创建业务容器了。
2.4 安装阶段最容易踩的三个坑
装完之后绕不开的几个问题,我逐个说:
第一个是权限问题。刚安装完用当前用户直接跑 docker 命令,通常会提示权限不足,本质是当前用户不在 docker 组里。两个解决办法:每次命令前加 sudo,或者把当前用户加入 docker 组后重新登录。长期使用建议走后者,因为写脚本和自动化任务时不可能每条命令都带 sudo。
第二个是服务未启动。明明刚安装完,执行 docker info 却报连不上引擎,大部分情况是服务没有启动成功。先检查执行状态,再尝试手动启动,如果启动时报 socket 相关错误,多半是残留的旧配置文件导致的,清理干净再试。
第三个是版本太旧。我用过一个发行版仓库自带的 docker,运行一个含流量转发功能的容器时直接出现内核模块异常,换用较新版本后问题消失。所以如果后续在使用中遇到奇怪的内核或者网络错误,可以先怀疑版本兼容性,升级到相对稳定的新版本再观察。
提示:安装环节的目标不是"能敲 docker --version",而是让
docker run hello-world这类镜像能正常跑起来。任何一步执行失败都要停下来查清楚,不要带病继续。
3. docker run 和 docker exec 到底差在哪
现在进入核心问题。run 和 exec 这两条命令,从英文含义上就能看出差异:run 是运行、创建,exec 是在已有环境里执行。但实际使用时,不少人会下意识把两者当成同一类命令去换用,结果自然是出各种怪问题。
3.1 定位对比:创建容器 vs 钻进容器
docker run的逻辑是:"基于某个镜像,新建一个容器,然后启动它"。它关心的是容器从无到有的过程。每次执行 docker run,都相当于重新创造一个全新容器,即使镜像完全相同,这个容器的文件系统也是独立的,你在里面做任何改动都不会影响镜像本身。
docker exec的逻辑则是:"找到某个已经存在的容器,在它内部额外执行一条命令"。它不创建新容器,而是复用正在运行的那个容器环境。典型用法是进入容器内部查看日志、修改配置、调试进程,或者执行某个临时任务。
下面这个表格把关键差异列出来,日常最容易混淆的点都在这:
| 对比维度 | docker run | docker exec |
|---|---|---|
| 核心动作 | 创建并启动新容器 | 在已运行容器内执行命令 |
| 前提条件 | 本地有对应镜像即可 | 目标容器必须处于运行状态 |
| 容器数量影响 | 每次执行都会多出一个新容器 | 不会新增容器 |
| 退出影响 | 退出可能让容器停止(取决于参数) | 退出不会停止原容器 |
| 典型场景 | 部署服务、初始化环境 | 进入容器调试、查日志、改配置 |
3.2 最容易搞反的两个使用场景
我见过不止一次的错误用法是这样的:服务起不来,有人执行docker run -it 镜像名进到一个新容器里查看配置文件,改完退出,然后一脸困惑地回来说"容器没有保存我的修改"。原因很简单:那个全新容器只是镜像的一个临时副本,退出后如果没有做提交,所有改动都随着容器销毁而丢失。正确的做法应该是先在宿主机上修改好挂载的配置文件,或者用 docker exec 进入正在运行的容器去排查。
另一个相反的错误是:容器已经停掉了,有人还用 docker exec 想进去操作,结果系统提示无法附加到已停止的容器。这是必然的,exec 的前提是容器正在运行。如果容器已经退出,要用的是 docker start 先把容器拉起来,或者直接 docker run 重建。
这两个场景记牢一个原则就可以避免大部分混乱:run 管"生",exec 管"活在运行中的容器"。想在已有环境里临时做点什么,用 exec;想从头创建一个环境,才用 run。
3.3 结合生命周期理解 exec 的局限性
从容器生命周期看,run 创建出来的容器有三种状态:运行中、已退出、已暂停。而 exec 只对第一类有效。这也解释了为什么很多自动化脚本里会出现这样的模式:先用 docker run 启动容器,再用 docker exec 持续注入任务指令,最后用 docker stop 优雅收尾。
还有一个细节容易被忽略:docker exec 进入容器时,工作目录默认不是容器里你上次所在的路径,而是构建镜像时指定的工作目录。如果你习惯性地假设"我还在刚才那个目录",可能执行命令时提示找不到文件。我在调试脚本时就踩过这个坑,后来每次 exec 进去第一件事就是 cd 到目标路径,或者直接在 exec 命令里带上-w参数指定工作目录。
注意:让 exec 执行一条命令和进入交互式 Shell 是两回事。前者用完即走,适合脚本自动化;后者持续占用终端,适合人工排查。下面的参数组合讲解里,-it 就是专门为后者准备的。
4. -i、-t、-d 三个参数逐个拆解与组合实战
参数部分是最容易让人迷糊的地方,因为单看一个参数都能看懂,组合起来就晕。这里不背参数说明,我按实际含义一条条过。
4.1 -i:标准输入打开,决定你能不能"喂"东西
-i,全名 interactive,作用是保持容器的标准输入打开。不加 -i 时,容器处于非交互模式,你无法向容器内进程传递输入,很多需要读输入的命令根本没法工作。加上 -i 之后,终端输入可以直接送到容器进程里。
打个生活化比方:容器是一个对讲机,-i 就是把对讲机的"按下说话"按钮按住不放。没有这个动作,你在另一头喊破嗓子,对方也听不见。
实用场景很典型:需要在容器里执行一个会询问确认信息的命令,比如初始化脚本让你输入数据库密码,或者需要向某个交互式程序持续喂数据,这时候必须带 -i。
需要注意,-i 本身不提供终端体验,它只是打通了输入通道。此时如果你只加 -i 不加 -t,你会发现能输入东西,但回显和操作体验很别扭,因为缺少了终端能力。所以实际使用中 -i 很少单独出现,基本都是和 -t 搭配。
4.2 -t:伪终端分配,决定交互体验
-t,全名 tty,作用是给容器分配一个伪终端。伪终端就是模拟出来的终端设备,有了它,容器内部程序才会认为自己是连在一个终端上的,从而友好地输出交互界面、颜色、光标控制等。
没有 -t 时,即使你能输入内容,程序输出也可能是一团乱麻,因为很多程序检测不到终端就会改变输出方式。有了 -t,你才能真正"像操作一台远程服务器"那样操作容器。
这里要插一个重要细节:-t 带来的伪终端是有副作用的。分配 tty 之后,容器内标准输出从行缓冲变成块缓冲,很多程序的日志输出会有延迟,甚至日志内容里出现一堆^[[0m之类的转义字符,这是终端控制码被记录下来的结果。后面 4.5 小节会详细讲这个问题。
4.3 -d:后台化,决定容器怎么活
-d,全名 detached,作用是让容器在后台运行并打印容器 ID。这是生产环境部署服务最常用的参数:不加 -d,容器在前台运行,终端会一直被占用,你 Ctrl+C 一按,容器就停了;加了 -d,容器放到后台跑,终端立刻交还给你,你可以继续做其他事情。
用生活场景理解:-d 就像你把一个程序放到服务器后台跑,用 nohup 或者系统服务托管,不再占用你的前台终端。对长时间运行的 Web 服务、队列消费进程来说,这是必须的。
但 -d 也有一个容易让人误判的地方:加了 -d 不等于容器一定正常运行。如果容器里主进程执行完就退出,即使有 -d,容器也会立刻变成退出状态。因为容器的生命周期是由主进程决定的,主进程退出,容器就结束。这一点很多人会理解成"后台运行",实际上后台只是不占用终端,不代表进程永远存活。
4.4 组合场景逐项解读:-it / -d / -itd / 前台执行
参数组合的核心逻辑就是三个开关的自由搭配。我按最常见的几种逐个说。
-it 组合:这是进入交互式 Shell 的标准姿势。创建容器并直接进入,可以敲命令、看输出、调配置。适合临时调试和开发验证。比如:
docker run -it 镜像关键字 bash执行后你会直接落在容器内部的 Shell 里。这时候注意:你运行的这个 bash 是容器的主进程。当你输入 exit 退出,容器的主进程结束,容器会停止。如果想保留容器,退出时可以先按 Ctrl+P 再按 Ctrl+Q,这是 detach 键,可以把终端分离而不停止容器。
-d 单独用:这是后台运行服务的最常见方式。启动后终端返回一长串容器 ID,容器在后台持续运行。需要进入容器时再配 docker exec 使用。适合 Nginx、Redis、后端服务这类长驻进程。
-itd 三件套:后台运行同时保持标准输入打开并分配伪终端。看起来是"后台运行,还能随时交互",实际上这是组合里最需要谨慎使用的方案。它会占用多余的 tty 资源,而且进程输出因为终端缓冲导致日志收集困难。我的看法是:除非你要启动一个本身就依赖交互式终端的后台进程,否则尽量避免 -itd。很多场景下 -itd 只是让容器"看起来正常"地活着,实际并没有比 -d 多出什么好处,反而拖累日志输出。
不加任何参数的前台运行:直接 docker run 执行,命令在前台跑,日志实时刷屏,Ctrl+C 直接停止。适合开发时观察输出,不适合正式部署。
4.5 与 -itd 相关的输出坑:为什么 logs 里的日志带着转义符
这里展开一个实际遇见过多次的典型问题。用docker run -itd启动的容器,在宿主机上用docker logs查看日志,常常能看到这样的内容:
^[[0m[INFO]^[[0m service started ^[[1;32m2026-01-01 10:00:00^[[0m request received^[[0m这类符号是终端控制序列,正常在没有终端的情况下不会出现。根源就是容器创建时被分配了 tty,程序检测到终端后输出了颜色和样式控制码,而这些控制码被原样写进了日志。问题不在程序本身,而在于你给了它一个终端环境。
处理方案有两个:一是启动容器时不带 -t,即用 -d 或 -i 单独搭配;二是在程序侧强制关闭颜色输出,比如很多日志组件都有环境变量可以直接禁用 ANSI 颜色。我倾向于从源头解决,不带 -t,让日志保持纯文本格式,后续解析日志做告警时能省很多事。
| 参数组合 | 是否后台 | 是否可交互 | 推荐用途 | 日志友好度 |
|---|---|---|---|---|
| 无参数 | 否 | 否 | 快速试跑测试程序 | 一般 |
| -i | 否 | 半交互(无终端) | 需要喂数据的管道 | 较高 |
| -it | 否 | 是 | 调试、进入容器 Shell | 一般 |
| -d | 是 | 否 | 生产环境后台服务 | 最高 |
| -itd | 是 | 是 | 特殊交互后台进程 | 低 |
这个表格是我实际项目里的默认选择依据。维护时间越长的服务,越倾向于用 -d 保持日志干净;开发调试时用 -it 提高效率;-itd 只在极少数特殊诉求下启用。
5. 实操现场:三个典型场景完整走一遍
理论说完,直接进入复现场景。下面三个例子来自我实际处理过的部署排查任务,每一步都可执行,参数选择也附了理由。
5.1 场景一:一次性交互调试(用 -it)
有一次排查一个初始化脚本的问题,脚本运行中会提示输入确认信息,但实际执行时一直卡在等待输入的阶段。这个场景就是 -it 的典型使用。
docker run -it 镜像关键字 /bin/bash进入容器后手动执行初始化脚本,能看到每一步的交互提示,也能根据报错信息即时修改配置。调试完成后退出 Shell,容器随之停止。因为这是临时调试环境,停止掉完全不影响业务,反而避免残留无用容器占用资源。
这里有一个经验细节:如果只是想在容器里快速执行一条命令,不打算呆在 Shell 里,可以直接写成docker run --rm 镜像关键字 命令行,命令执行完容器自动清理,不残留任何东西。加 --rm 是好习惯,尤其在你追求"测完即走"的干净环境时。
5.2 场景二:部署后台服务(用 -d)
正式部署一个 Web 服务时,容器必须长时间运行,不能占用终端。这里用 -d 配合端口映射启动:
docker run -d \ --name app-server \ -p 8080:80 \ 镜像关键字参数说明:--name给容器起固定名字,便于后续 docker stop、docker start 操作,不用记那串随机字符串;-p 8080:80表示宿主机的 8080 端口映射到容器的 80 端口,外部请求通过宿主机的 8080 进来。
启动后立刻在宿主机执行curl http://127.0.0.1:8080/验证服务是否响应。如果响应正常,再看日志确认无异常。这种部署方式下,即使你关闭终端、重启宿主机上的外壳进程,服务本身依然在后台运行,不受终端退出影响。
这里要特别提醒:-d只是把容器放到后台,如果你用docker ps看到容器处于退出状态,说明主进程没跑起来,得用日志排查。很多新手把 -d 当成长生不老的保证,这是认知误区。
5.3 场景三:先起容器再进容器改配置(exec 配合)
服务已经在后台运行,但需要临时改一个运行参数,改完还要立即生效,重启容器代价太大。这时候用 docker exec 进入容器执行命令:
docker exec -it app-server /bin/bash进入后修改配置文件、执行重载指令,然后退出。这个 exec 会话产生的任何改动都不会影响容器的运行状态,退出也不会停止容器。如果需要执行一条不进入 Shell 的命令,直接:
docker exec app-server 需要执行的命令这种模式在自动化运维里很常用:定时任务定期进入容器清理临时文件、批量执行脚本、刷新缓存,都可以用非交互的 docker exec 完成,完全不必占用额外的终端资源。
5.4 容器退出自动重启等策略补充
还有一个和容器存活强相关的参数值得一并说明:--restart。它的作用是在容器异常退出时由引擎自动拉起容器,对生产环境服务非常重要。
如果启动了 Web 服务容器,但忘了加 --restart,进程因为未知原因崩掉后容器就永远退出了,只有人工介入才能恢复。加上策略后,比如:
docker run -d \ --name app-server \ --restart unless-stopped \ -p 8080:80 \ 镜像关键字unless-stopped表示只要不是人工 docker stop 停止的,容器异常退出都会自动重启。这个策略和 -d 搭配是生产环境的黄金组合:后台运行保证不占终端,自动重启保证高可用性。
提示:生产环境建议统一使用 unless-stopped 或 always 策略。区别在于:always 即使手动 docker stop 后,在引擎重启时也可能再次拉起;unless-stopped 则尊重你的人工停止操作。按运维期望选择。
6. 常见问题与排查技巧实录
实践过程中必然会遇到各类问题。下面按高发频率整理一份排查速查表,每个问题都附实际操作过的解决思路。
6.1 容器启动后秒退
表现:docker run 执行完,容器立刻处于退出状态,docker ps 看不到它。
原因几乎都指向同一个点:主进程执行完就结束了。容器不是虚拟机,它不负责维护自身的存在,只负责跑主进程。主进程退出,容器即终止。如果镜像默认命令是一个脚本,脚本执行完没有进入常驻循环,就会立刻退出。
排查步骤:先看完整日志docker logs 容器名,定位退出前最后一条输出;然后确认启动命令是否应该是带阻塞的常驻命令;最后考虑是否需要调整命令,比如在命令末尾追加 sleep 或前台运行模式。对生产环境,还要检查是不是启动时缺少了 -d,导致进程其实在前台被终端中断。
6.2 exec 时报"容器未运行"
表现:执行 docker exec 时提示无法在已停止的容器上执行命令。
这是个高频错误,通常发生在部署脚本里:先 docker stop 停止容器做维护,维护完忘了 docker start,直接跑 docker exec。解决思路前面说过,先把容器拉起来再 exec。如果容器数据不重要,直接 docker run 重建一个也行。
还有一个容易忽视的点:容器处于暂停状态时,exec 同样进不去。先确认 docker ps 显示的状态列,确保是 Up 而不是 Pause 或 Exited。
6.3 端口占用与冲突
表现:容器启动失败,报错信息里含 address already in use 或 bind 失败。
原因是宿主机上端口已被占用。排查方法:用系统命令查看监听端口占用的进程,找到冲突来源后,要么停掉占用进程,要么更换容器映射的宿主端口。注意,容器内部端口是可以相同的,冲突只发生在宿主端口侧。
经验做法:项目里统一维护端口分配表,每个服务固定一段端口区间,能有效避免多人协作时互相踩端口。我见过几次因为端口冲突导致测试环境互相干扰的事故,最后靠表格规范才根治。
6.4 权限、磁盘、时区、日志等细节问题
权限问题:容器内用户对挂载目录没有写权限,通常表现为启动时创建目录或文件失败。解决方式是调整挂载目录的宿主权限,或用-u指定容器内运行用户。不建议直接给目录 chmod 777,不安全和严谨。
磁盘空间不足:长时间运行的容器日志文件会把磁盘占满。表现为 docker 无法创建容器,甚至 pull 镜像都失败。除了清理不用的镜像、容器,还必须对容器日志大小做限制。启动容器时加日志驱动参数,或统一配置日志轮转策略,是避免磁盘告警的有效手段。
时区问题:容器默认使用 UTC 时间,业务日志和宿主机时间对不上。排查问题时往往差 8 小时,让人误判事件时间。解决办法:启动容器时通过参数挂载宿主时区文件,或者设置TZ=Asia/Shanghai环境变量。这类细节特别影响跨团队排查问题,建议从一开始就统一。
日志内容不实时输出:如果你启动容器时不带 -t,部分程序的日志会延迟输出,这是因为没有终端时程序启用了块缓冲。这和 4.5 节反着来,之前是从日志干净角度考虑,现在是从实时性考虑。真需要实时日志又不想要转义符,可以在启动命令里强制程序使用行缓冲,很多运行时环境都提供禁用缓冲的选项。
| 问题 | 原因 | 解决思路 |
|---|---|---|
| 容器启动后秒退 | 主进程执行结束 | 查看日志,改为常驻命令 |
| exec 进不去 | 容器已停止或暂停 | 先 start 再 exec |
| 端口被占 | 宿主端口冲突 | 查占用进程,换端口或停冲突 |
| 挂载目录无权限 | 用户或权限不匹配 | 指定用户或调整权限 |
| 磁盘占满 | 日志膨胀、镜像残留 | 清理资源,限制日志大小 |
| 时区不一致 | 默认 UTC | 挂载时区或设置环境变量 |
7. 一些经验总结和新手建议
写到这里,该说的核心内容都讲完了。按我的习惯,最后分享几条自己在实际操作中总结出来的体会,不算什么高深理论,但都是实打实用时间换来的。
首先,命令参数记得再多,不如先想清楚一个问题:你这次操作是"从零创建一个环境",还是"进入一个已经存在的环境"。这两个方向定了,run 和 exec 就不会用错,-itd 三个参数到底要不要带也会清晰得多。实际工作中我见过太多人死记硬背参数,真到故障现场却不知道怎么组合,根源就是没有先做这个简单判断。
其次,写自动化部署脚本时,尽量让启动命令明确可预期。服务类容器统一用-d配合--restart unless-stopped,调试类容器统一用-it配合--rm,避免用 -itd 造成日志混乱和资源浪费。保持这种一致性,团队之间交接时能少很多沟通成本。
最后分享一个实用小技巧:如果发现容器日志刷不出来,但容器进程确实在跑,先检查自己是不是用了 -it 启动;如果日志一堆乱码转义符,先检查自己是不是用了 -itd 启动。这两个参数组合问题占据了容器日志排障的一半场景。把日志采集成体系的业务系统,尤其要把 tty 分配这个因素纳入日志接入规范考虑。
容器化的学习曲线不算陡,但细节很多。安装、run、exec、参数组合这几个主题看似基础,却是后续网络、存储、编排所有复杂能力的根基。把这些基础场景在本地环境亲手验证一遍,远比追着新技术跑更值得投入时间。