Docker常用命令深度解析:原理、陷阱与实战避坑指南 1. 这不是命令手册而是一张 Docker 容器操作的“生存地图”你打开终端敲下docker run hello-world屏幕刷出一行绿色文字——恭喜你已经跨过了 Docker 最高那道心理门槛。但接下来呢当你要把本地写好的 Python 脚本塞进容器、想让 MySQL 和你的后端服务一起启动、发现某个容器卡死却连日志都捞不出来、或者被同事一句“你这个镜像怎么没暴露端口”问得哑口无言时光靠run和ps就像只带一把瑞士军刀去修整栋楼的电路。我做过不下 30 个涉及 Docker 的交付项目从某高校实验室的轻量级教学环境搭建到某公司内部微服务 CI/CD 流水线重构踩过的坑几乎能填平一个小型泳池。这些坑里80% 都不是技术原理有多难而是对几个基础命令的理解停留在“能跑就行”的表层——比如你以为docker stop是温柔关机其实它默认只给容器 10 秒时间优雅退出超时就直接kill -9你以为docker build只是打包代码却没意识到每一行RUN指令都在生成一个新镜像层层数越多构建越慢、推送越卡、磁盘占用越疯长。这篇内容不讲 Docker 架构图不画 daemon/client 分层模型就聚焦在你每天真实会敲、会查、会卡壳的那二十来个命令上。我会告诉你每个命令背后的真实意图、它在什么场景下是救命稻草、又在什么情况下是埋雷开关会拆解docker exec -it里那两个字母为什么缺一不可会算清楚--memory512m到底是给容器划了多少内存以及为什么有时候设了也白设。无论你是刚配好docker --version的新手还是已经用docker-compose.yml写出花来的老手只要你还在终端里和容器打交道这张“生存地图”里的标记就是你下一次排查问题时最可能用上的路标。2. 命令设计逻辑为什么是这组命令而不是另一套2.1 Docker 命令体系的底层哲学面向资源生命周期管理Docker 的命令不是随机堆砌的它严格遵循 Linux 系统中“一切皆文件”的延伸理念——一切皆资源。镜像Image、容器Container、卷Volume、网络Network这四大核心对象在 Docker CLI 中各自拥有独立、语义清晰的命令族。这种设计不是为了炫技而是为了精准匹配运维人员的真实工作流你不会在同一个操作里既拉取镜像又启动容器再创建网络你总是在某个明确阶段专注处理某一类资源。比如docker image命令族docker image ls,docker image rm专管镜像的“生老病死”它和docker container命令族docker container start,docker container stop完全隔离。这种隔离带来两个关键好处一是权限控制可以做到极致精细比如 CI/CD 流水线里构建节点只需要image pull和image build权限运行节点则只需container run和container logs权限二是错误定位极其高效当你执行docker container ls却看不到预期容器时问题一定出在容器生命周期环节如启动失败、已退出绝不会是镜像没拉下来这种上游问题。我见过太多团队把所有命令混着用docker ps查不到容器就立刻去docker images里翻镜像结果浪费半小时才发现容器根本就没成功run起来——这就是违背了命令体系设计哲学的典型代价。2.2 “常用”二字的残酷真相高频≠安全简洁≠无坑所谓“基本常用命令”往往是最容易被滥用的。以docker run为例它的语法看似简单docker run [OPTIONS] IMAGE [COMMAND] [ARG...]但 OPTIONS 里藏着至少 15 个影响系统稳定性的关键参数。新手最爱用的-d后台运行和-p端口映射组合表面看是让服务“跑起来”实则暗藏三重风险第一-d启动后容器若因依赖缺失立即崩溃你根本看不到任何报错只能靠docker logs去捞而很多初学者甚至不知道这个命令的存在第二-p 8080:80这种映射如果宿主机 8080 端口已被占用Docker 不会报错而是静默失败容器照样启动只是服务对外不可达第三也是最隐蔽的-p默认绑定到0.0.0.0所有网卡在生产环境等同于把服务直接暴露在公网这在过去三年我参与的安全审计中是排进前三的配置漏洞。再看docker rm它和docker rmi的命名差异极小但后果天壤之别前者删容器数据可能丢失后者删镜像影响所有基于此镜像的容器。我曾亲眼目睹一位资深运维在凌晨三点压力下手抖少敲了一个i执行了docker rm my-app而非docker rmi my-app导致正在运行的测试环境容器被强制删除整个 QA 团队停工两小时。所以“常用”命令的真正价值不在于它多好记而在于你是否清楚每一次敲击背后系统正在为你做哪些不可逆的操作。2.3 为什么没有“万能命令”Docker 的分治思想与现实妥协Docker 故意不提供类似docker manage all这样的聚合命令这是经过深思熟虑的工程妥协。想象一个场景你需要将一个包含 Web 前端、API 后端、Redis 缓存、PostgreSQL 数据库的完整应用部署到服务器。如果用单条命令实现它需要解析复杂的依赖关系、处理不同组件的启动顺序、协调网络策略、管理持久化数据路径……这样的命令会臃肿到无法维护且一旦出错调试成本极高。Docker 的选择是“分而治之”用docker network create先搭好私有网络用docker volume create预置数据卷再用四个独立的docker run命令分别启动各组件并通过--network和--volume参数将它们精准挂载。这种看似繁琐的方式换来的是极致的可追溯性——任何一个组件启动失败你都能立刻定位到是run命令的哪个参数错了而不是在一团浆糊的聚合命令日志里大海捞针。我在某公司重构其老旧部署脚本时将原先一个 200 行的deploy.sh里面混着 wget、tar、chmod、service start 等各种命令替换成 4 个清晰的docker run命令加一份docker-compose.yml上线后故障平均定位时间从 47 分钟缩短到 6 分钟。这不是 Docker 在增加你的工作量而是在用结构化的命令设计为你购买未来的时间保险。3. 核心命令逐层拆解从敲下回车那一刻开始3.1 镜像管理pull、build、ls、rm—— 你的容器“原材料工厂”镜像是容器运行的基石所有操作都始于它。但很多人忽略了镜像本身就是一个需要精细管理的“原材料工厂”。docker pull看似只是下载实则有两大陷阱。第一是标签Tag的隐式行为当你执行docker pull nginxDocker 默认拉取nginx:latest标签。但latest并非永远最新它可能指向一个半年前的稳定版也可能指向昨天刚发布的、尚未经充分测试的开发版。我建议永远显式指定标签比如docker pull nginx:1.23.3或docker pull python:3.11-slim。第二是认证问题私有仓库如 Harbor拉取时若未提前docker login命令会卡住或静默失败。实操中我习惯在 CI/CD 脚本开头强制加入docker info | grep Username检查登录状态避免构建中途因认证失败而中断。docker build是最常被误解的命令。它的核心不是“编译”而是“按指令逐层构建”。每一条Dockerfile指令FROM、COPY、RUN都会生成一个中间镜像层。关键点在于缓存机制Docker 会逐行比对Dockerfile如果某一行指令及其之前的所有层都未改变就直接复用缓存层跳过后续构建。这意味着COPY . /app这种把整个目录拷进去的指令只要代码有任意一个字符改动就会使该层及之后所有层全部失效重新构建。我的优化方案是把变化频率低的步骤如安装依赖放在前面变化频率高的如拷贝源码放在后面。例如先COPY requirements.txt并RUN pip install -r requirements.txt再COPY . /app这样改了代码但没改依赖pip 安装那层就能复用构建速度提升 3-5 倍。docker image ls和docker image rm是清理战场的利器。但docker image ls默认只显示最近创建的镜像那些悬空dangling镜像——即没有被任何容器或标签引用的中间层——会被隐藏。它们不显示但真实占着磁盘。我每次构建完新镜像必先执行docker image ls -f danglingtrue查看悬空镜像再用docker image prune清理。注意prune默认是交互式加-f参数才自动确认这点在自动化脚本里必须写死否则会卡住。提示docker system df是你的磁盘空间“CT 扫描仪”。它能清晰列出镜像、容器、卷各自占了多少空间以及有多少是可回收的Reclaimable。我把它设为每日巡检脚本的第一行比df -h精准十倍。3.2 容器生命周期run、start、stop、rm—— 掌控容器的“呼吸节奏”容器的启停不是简单的开关而是一套精密的“呼吸节奏”控制系统。docker run是起点也是最复杂的命令。它的灵魂在于--restart策略。no默认意味着容器崩溃就彻底躺平always会无限重启哪怕你手动stop了也会被拉起on-failure:5则只在非零退出码时重启最多 5 次。生产环境中我几乎从不选always因为无限重启可能掩盖真正的程序缺陷比如内存泄漏导致 OOM 后反复崩溃。更稳妥的是on-failure:3配合监控告警让问题暴露出来。另一个关键参数是--oom-kill-disable它禁用 Linux OOM Killer 对容器的杀戮。听起来很诱人千万别乱开这等于给容器发了一张“免死金牌”当它疯狂吃内存时宿主机可能因内存耗尽而整体卡死。正确的做法是结合--memory和--memory-swap严格限制让 OOM Killer 在必要时果断出手保全整台机器。docker stop和docker kill的区别是生死线。stop发送SIGTERM信号给容器 10 秒可通过--time修改优雅退出程序可以在此期间关闭连接、保存状态kill则直接发送SIGKILL强制终结没有任何缓冲。我坚持一个铁律日常管理只用stop只有在stop超时后容器仍僵死才用kill。曾经有个 Node.js 服务因为未正确处理SIGTERMstop后一直卡在 10 秒倒计时我误以为它挂了立刻kill结果导致数据库连接池未释放下游服务集体报错。后来我们在代码里加了process.on(SIGTERM, () { server.close(); })stop就变得无比可靠。docker rm的-fforce参数是把双刃剑。它能强制删除正在运行的容器但代价是数据丢失。如果你的容器挂载了--volume或--mount数据还在卷里还算幸运但如果数据只存在容器的可写层即没挂载卷rm -f就是永久删除。我的经验是永远先docker stop再docker rm把-f当作最后的急救手段而不是日常操作。为此我甚至写了个小 aliasalias drmfecho Use docker stop first! If you must, type: docker rm -f把它加到.bashrc里每次手滑想敲drmf终端就会弹出警告。3.3 容器内务管理exec、logs、cp—— 进入容器的“三把钥匙”当容器运行起来你不能只在外面看必须能进去“把脉问诊”。docker exec、docker logs、docker cp就是这三把钥匙每把都有独特纹路。docker exec的-it组合是理解容器交互模式的钥匙。-iinteractive保持 STDIN 打开让你能输入-ttty分配一个伪终端让命令行有颜色、能用方向键。少了-i你敲不了命令少了-tls --colorauto这种带颜色的输出就变成乱码。但更重要的是exec启动的进程是容器主进程的子进程它共享同一个 PID 1即容器的入口进程。这意味着如果你在exec里执行kill 1整个容器会立刻终止——这在调试时是个危险但有效的“硬重启”方式。我常用docker exec -it container sh进入一个精简的 shell然后用ps aux查看进程树用netstat -tuln看端口监听用df -h看磁盘这比在宿主机上猜要准确得多。docker logs是容器的“黑匣子”。它的-ffollow参数像 tail -f 一样实时追加日志--tail 100可以只看最后 100 行。但最大误区是认为logs能看到所有输出。事实是如果容器进程的标准输出stdout和标准错误stderr没有被 Docker daemon 捕获比如程序自己重定向到了/var/log/app.log文件logs就是空的。这时你必须用docker exec进去手动查看文件。我遇到过最棘手的一次是 Java 应用的日志框架 Logback 配置了异步写入和文件滚动logs里只有启动信息真正的业务日志全在容器内的app.log里。解决方案是在Dockerfile中把日志文件路径VOLUME [/var/log/myapp]挂载出来或者干脆让应用把日志输出到 stdout这才是云原生的最佳实践。docker cp是数据搬运工。docker cp container:/path/to/file ./local/file可以把容器里的文件拷出来docker cp ./local/file container:/path/to/file则反向拷入。但它有个致命限制只能拷文件或目录不能拷符号链接symlink的目标文件。我曾试图用cp把一个配置文件拷进容器结果发现容器里那个路径是个软链cp只拷了软链本身没拷目标文件导致服务启动失败。后来我改用docker exec -i container sh -c cat /path/to/file ./local/file用管道方式写入完美绕过软链问题。3.4 网络与存储network、volume、inspect—— 揭开容器“看不见的连接”容器不是孤岛它们需要网络互通、数据持久。docker network和docker volume命令就是管理这些“看不见的连接”的手术刀。docker network的核心是驱动driver。bridge默认用于单机多容器通信host直接使用宿主机网络性能最好但失去隔离none完全禁用网络适合纯计算任务。我最常用的是自定义 bridge 网络docker network create --subnet172.20.0.0/16 my-net。这样做的好处是容器可以通过服务名如redis互相访问Docker 内置 DNS 会自动解析不用记 IP。而且--subnet指定网段能避免和宿主机或其他网络冲突。有一次我们线上环境多个服务共用默认 bridge 网络IP 分配混乱A 服务重启后拿到 B 服务的旧 IP导致请求错发花了半天才定位。从此所有项目都强制使用自定义网络。docker volume是数据持久的基石。docker volume create my-data创建一个命名卷docker run -v my-data:/data nginx将其挂载到容器内/data。关键点在于命名卷由 Docker 管理路径在/var/lib/docker/volumes/下你无需关心具体位置而绑定挂载bind mount-v /host/path:/container/path则是直接映射宿主机目录路径完全透明。我的原则是生产环境一律用命名卷因为它更安全、更可移植开发调试时可用绑定挂载方便快速修改代码。另外docker volume inspect my-data能查到卷的绝对路径这在需要手动备份或修复数据时至关重要。docker inspect是终极诊断工具。它返回一个 JSON包含容器/镜像/网络/卷的所有元数据。docker inspect container里NetworkSettings.Networks.network-name.IPAddress是容器 IPMounts数组列出了所有挂载点State.Status显示当前状态running/exited/paused。我常用docker inspect -f {{.NetworkSettings.IPAddress}} container这种 Go 模板语法直接提取 IP写进自动化脚本里。它比docker network inspect查到的信息更聚焦、更实时。4. 实战场景还原从零搭建一个可调试的 Flask 应用容器4.1 场景设定一个需要频繁修改代码、查看日志、并保证数据不丢的 Web 服务假设你正在开发一个 Flask 应用功能是记录用户提交的表单数据到 SQLite 数据库。开发过程中你希望代码改完能秒级生效不用每次都build新镜像能随时进入容器看数据库文件服务崩溃时能立刻看到错误堆栈SQLite 数据库文件必须持久化关机重启也不能丢。这正是docker run的经典组合拳场景。4.2 步骤拆解每一步背后的“为什么”和“怎么做”第一步准备基础环境# 创建项目目录 mkdir flask-demo cd flask-demo # 初始化 Git好习惯便于版本追踪 git init # 创建最小 Flask 应用 app.py cat app.py EOF from flask import Flask, request, render_template_string import sqlite3 import os app Flask(__name__) DB_PATH /data/app.db def init_db(): conn sqlite3.connect(DB_PATH) conn.execute(CREATE TABLE IF NOT EXISTS submissions (id INTEGER PRIMARY KEY, name TEXT, email TEXT)) conn.commit() conn.close() app.route(/, methods[GET, POST]) def index(): if request.method POST: name request.form[name] email request.form[email] conn sqlite3.connect(DB_PATH) conn.execute(INSERT INTO submissions (name, email) VALUES (?, ?), (name, email)) conn.commit() conn.close() return Submitted! return render_template_string( form methodpost Name: input typetext namenamebr Email: input typeemail nameemailbr input typesubmit valueSubmit /form ) if __name__ __main__: init_db() app.run(host0.0.0.0:5000, debugTrue) EOF这里的关键是DB_PATH /data/app.db把数据库路径硬编码到/data为后续挂载卷做准备。debugTrue开启 Flask 调试模式支持代码热重载。第二步创建并挂载命名卷# 创建一个专门存数据库的卷 docker volume create flask-db # 启动容器关键参数解析 # -v $(pwd):/app:ro 将当前目录代码以只读方式挂载到容器 /app避免容器内误删代码 # -v flask-db:/data 将命名卷挂载到 /data确保数据库持久化 # -p 5000:5000 将容器 5000 端口映射到宿主机 5000 # --rm 容器退出后自动清理避免垃圾堆积开发时很干净 # -w /app 设置工作目录为 /app让 python app.py 能找到文件 docker run -d \ --name flask-dev \ -v $(pwd):/app:ro \ -v flask-db:/data \ -p 5000:5000 \ --rm \ -w /app \ python:3.11-slim \ sh -c pip install flask python app.py注意-v $(pwd):/app:ro这里的:roread-only非常重要。它防止容器内运行的 Python 进程意外修改了你本地的源代码保证了开发环境的纯净。--rm是开发利器每次stop后自动清理省去手动rm的麻烦。第三步实时调试与日志追踪# 查看实时日志-f 表示跟随--tail 50 只看最后 50 行避免刷屏 docker logs -f --tail 50 flask-dev # 在另一个终端进入容器内部 docker exec -it flask-dev sh # 在容器里你可以 # 1. 查看数据库文件是否存在 ls -l /data/ # 2. 用 sqlite3 命令行工具直接查询数据需先 apt-get install sqlite3 # 3. 查看进程确认 python 进程是否在运行 ps aux | grep python # 4. 退出容器 exit此时你可以在本地编辑app.py修改 HTML 模板或添加新路由保存后由于 Flask 的debugTrue容器内的服务会自动重载无需重启容器。这就是开发效率的质变。第四步模拟故障与恢复# 故意制造一个错误在 app.py 里加一行 syntax error比如在末尾加个 print( # 保存后观察 docker logs -f 的输出你会立刻看到 SyntaxError 堆栈 # 修复代码保存服务自动恢复 # 如果服务彻底崩溃用 inspect 查看最终状态 docker inspect flask-dev | grep -A 5 Status # 如果需要用 cp 备份数据库 docker cp flask-dev:/data/app.db ./backup/app.db.$(date %s)4.3 关键参数与配置的深度验证参数/配置验证方法预期结果实际意义-v $(pwd):/app:ro在容器内执行touch /app/test.txtPermission denied 错误证明只读挂载生效保护本地代码-v flask-db:/datadocker volume inspect flask-db查看Mountpoint然后ls该路径看到app.db文件证明数据库文件真实存在于命名卷中而非容器可写层--rmdocker ps -a | grep flask-dev容器停止后ps -a输出为空证明自动清理生效避免开发环境残留垃圾容器-p 5000:5000宿主机执行curl http://localhost:5000返回 HTML 表单证明端口映射和网络连通性正常这个流程不是教科书式的理想路径而是我在某次紧急修复客户演示环境时用 12 分钟完成的真实复现。它把run、logs、exec、volume这几个最常用命令拧成了一股解决实际问题的合力。5. 常见问题与独家避坑指南那些文档里不会写的血泪教训5.1 “容器启动了但访问不了”—— 端口与网络的七宗罪这是新手最高频的“灵异事件”。我整理了一份速查表覆盖了 95% 的同类问题现象可能原因排查命令解决方案curl: (7) Failed to connect容器内服务监听127.0.0.1而非0.0.0.0docker exec -it container netstat -tuln | grep :port修改应用配置监听0.0.0.0:portcurl: (52) Empty reply from server容器内服务启动成功但未真正监听端口docker exec -it container ss -tuln | grep :port检查应用日志确认服务进程是否真在运行curl: (7) Failed to connect宿主机防火墙拦截了端口sudo ufw status(Ubuntu) 或sudo firewall-cmd --state(CentOS)sudo ufw allow 5000或开放对应端口curl: (7) Failed to connectDocker daemon 的 iptables 规则损坏sudo iptables -t nat -L | grep portsudo systemctl restart docker重载规则curl: (7) Failed to connect使用了--network host但应用监听127.0.0.1netstat -tuln | grep :port改为监听0.0.0.0:port或换用 bridge 网络注意netstat在 slim 镜像中可能不存在优先用ss -tuln它是netstat的现代替代品更轻量、更快。5.2 “磁盘爆了”—— 镜像与容器日志的无声吞噬者Docker 的日志驱动默认是json-file它会把容器 stdout/stderr 全部写入/var/lib/docker/containers/id/id-json.log。这个文件会无限增长直到撑爆磁盘。我见过最夸张的案例一个日志狂魔的 Java 服务三天生成了 42GB 的单个日志文件而docker system df却只显示“Reclaimable: 0B”因为日志文件被 Docker 认为是“正在使用中”。根治方案启动时限制日志大小docker run --log-driver json-file --log-opt max-size10m --log-opt max-file3 ...表示单个日志文件最大 10MB最多保留 3 个轮转文件。全局配置修改/etc/docker/daemon.json{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }然后sudo systemctl restart docker。这是生产环境的必备配置。 3.清理历史日志find /var/lib/docker/containers/ -name *.log -size 100M -exec ls -lh {} \;找出大日志truncate -s 0 file清空不要rmDocker 还在写入。5.3 “为什么我的环境变量不生效”—— Docker 中变量传递的三重迷雾环境变量传不进容器通常卡在三个地方第一重docker run -e的作用域-e VARvalue只对容器启动时的ENTRYPOINT/CMD进程有效。如果CMD是sh -c python app.py那么VAR在sh进程里存在但在python子进程中除非sh显式导出否则python看不到。解决方案-e VARvalue后CMD改为[sh, -c, export VAR$VAR python app.py]或直接在Dockerfile里ENV VARvalue。第二重.env文件的加载时机docker run --env-file .env会加载文件里的变量但文件必须是 Unix 格式LF 换行Windows 的 CRLF 会导致变量值末尾多一个^M字符echo $VAR会显示异常。用dos2unix .env转换。第三重docker-compose的变量覆盖docker-compose.yml里environment:下的变量会覆盖--env-file和-e的同名变量。顺序是-e--env-fileenvironment:。务必检查你的yml文件。5.4 “docker build太慢了”—— 构建加速的五个实战技巧.dockerignore是金矿创建.dockerignore文件排除node_modules/,.git/,*.md,tests/等不需要的文件。它能阻止这些文件被发送到 Docker daemon大幅减少上下文传输时间。我一个项目加了.dockerignore后构建上下文从 1.2GB 降到 8MB上传时间从 3 分钟降到 2 秒。多阶段构建Multi-stage BuildDockerfile里用FROM golang:1.21 AS builder编译二进制再FROM alpine:3.18作为运行时COPY --frombuilder /app/myapp /usr/local/bin/myapp。这样最终镜像只有几 MB不含任何编译工具链。利用构建缓存把apt-get update apt-get install和pip install这些耗时操作放在COPY源码之前。因为COPY指令最易变动放前面会让缓存失效。使用国内镜像源在Dockerfile的FROM后加--platform linux/amd64如果需要并在RUN中替换源如RUN sed -i s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g /etc/apk/repositories。BuildKit 加速在docker build前加DOCKER_BUILDKIT1环境变量启用新一代构建引擎支持并行构建、更好的缓存命中率。DOCKER_BUILDKIT1 docker build .。5.5 “容器里中文乱码”—— 字符集与 locale 的隐形战场在ubuntu:20.04或debian:11镜像里locale默认是POSIX没有 UTF-8 支持导致ls列出中文文件名是??.txtPython 打印中文报UnicodeEncodeError。解决方案是在Dockerfile中加入# 安装 locales 包 RUN apt-get update apt-get install -y locales rm -rf /var/lib/apt/lists/* # 生成 en_US.UTF-8 和 zh_CN.UTF-8 RUN locale-gen en_US.UTF-8 zh_CN.UTF-8 # 设置默认 locale ENV LANGen_US.UTF-8 ENV LANGUAGEen_US:en ENV LC_ALLen_US.UTF-8对于 Alpine 镜像用apk add --no-cache tzdata然后cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime再echo Asia/Shanghai /etc/timezone。时区和 locale 是容器里最容易被忽视的“基础设施”但它们出问题会让你的整个应用表现诡异。6. 我的个人体会从“会用”到“用好”的最后一公里在某次为某公司做 Docker 专项培训时一位架构师问我“你说这么多命令和技巧有没有一个最核心的心法”我想了想说“有就八个字容器是进程不是虚拟机。” 这句话听起来像废话但它是我踩了无数坑后提炼出的终极心法。当你把容器当成一个轻量级虚拟机你会下意识地想在里面装vim、htop、net-tools想用systemctl管理服务想ssh进去折腾——这完全背离了 Docker 的设计初衷。容器应该是一个单一关注点的进程一个 Nginx 容器就只干 HTTP 服务这一件事一个 Redis 容器就只干缓存这一件事。它的生命周期应该和这个进程的生命周期完全一致。docker stop就是kill这个进程docker logs就是tail -f这个进程的 stdout。一旦你接受了这个设定exec就不再是“登录系统”而是“调试进程”volume