
docker run用好了Docker这工具才算真正上手。很多人学Docker第一步就是docker run但真到了生产环境、本地联调、跑中间件的时候一条docker run命令能玩出的花样远比想象中多——它不只是“把镜像跑起来”这么简单而是集镜像创建、资源隔离、网络映射、数据持久化、生命周期控制于一体的核心入口。这篇文章我打算从一条docker run命令的完整拆解开始把前台交互、后台守护、端口映射、数据卷、环境变量、生命周期管理这些看起来零散的知识点串成一条线再配合我实际踩过的坑和排查思路把容器从“能用”带到“好用”的层面。无论你是刚接触Docker的新手还是已经在用docker compose但想搞懂底层原理的开发者这都值得花几分钟读完。1. 容器运行前的准备镜像、运行时与docker run的底层逻辑1.1 一条docker run命令背后的完整链路很多人把docker run当成“启动容器”的指令这没错但不够准确。实际上docker run docker create docker start的组合它在执行时会先基于指定镜像创建一个可写容器层再把这个容器启动起来。这意味着每执行一次docker run就会产生一个新的容器实例即使镜像完全相同容器之间也是完全隔离的。这个底层逻辑解答了一个新手常问的问题为什么我改了容器里的文件重启容器后又变回原样了因为容器层是可写的但它的生命周期和容器绑定容器被删除后这一层也随之消失。而镜像层是只读的永远不会因为容器内的操作被改变。docker run [OPTIONS] IMAGE [COMMAND] [ARG...]这个语法结构里OPTIONS是运行参数IMAGE是要运行的镜像COMMAND是在容器内执行的命令。举个例子docker run -d --name nginx-demo -p 8080:80 nginx:alpine这一行命令做了四件事基于nginx:alpine镜像创建容器命名为nginx-demo把宿主机的8080端口映射到容器的80端口以守护模式在后台运行。命令执行后一个nginx服务就在本机跑起来了访问http://localhost:8080就能看到nginx的欢迎页。有人会觉得这些参数背起来麻烦其实不用背理解逻辑后就顺了。核心就几个维度运行方式前台还是后台、网络端口怎么暴露、存储数据怎么持久化、资源CPU内存限制、交互要不要进入容器终端。1.2 前台运行与后台守护-it和-d的取舍docker run默认在前台运行容器终端会直接附着到容器的标准输出上。这种方式适合需要观察实时日志的场景比如调试脚本、跑一次性任务。但如果是web服务、数据库这类需要长期运行的进程就应该用-d参数让容器在后台运行终端不会被占用。# 前台运行适合调试 docker run nginx:alpine # 后台运行适合服务常驻 docker run -d nginx:alpine实际使用中-it和-d组合出现频率极高。我举个最典型的例子运行一个Ubuntu容器并进入它的shelldocker run -it --name ubuntu-test ubuntu:22.04 bash-i表示保持标准输入打开interactive-t表示分配一个伪终端tty。这两个参数必须配合使用单独用-i只能交互但没有终端效果单独用-t画面会乱。当你执行exit退出shell后容器也会停止因为容器的生命周期由PID 1进程决定——bash退出就没有存活进程了。有个反直觉的点如果只用-d运行一个Ubuntu镜像而不指定命令容器会立刻退出。因为Ubuntu镜像的默认命令是bash但它在非交互模式下立即返回容器没有常驻进程就自动终止了。这时候用docker ps -a能看到一个Exited状态的容器这是每个新手都会遇到的“秒退”问题后面我会详细说排查方法。2. 交互式容器实战进入容器、执行命令与日志查看2.1 进入运行中容器的三种方式attach、exec和nsenter容器跑起来之后想进去看进程、改配置、排查问题时最常用的是docker exec。它的设计初衷是在运行中的容器里执行新命令不会影响容器原有的主进程。# 在运行中的容器里执行一条命令 docker exec nginx-demo ls /etc/nginx # 进入容器的交互式shell docker exec -it nginx-demo sh注意docker exec和docker attach的区别。attach是把当前终端附着到容器的主进程上看到的是PID 1进程的输出退出时会导致容器停止。而exec是另起一个进程exit退出不会影响容器运行。所以日常排查时我极少用attach全部用exec——安全、可控、不误伤容器。nsenter是一个更底层的工具通过Linux内核的namespace直接进入容器的命名空间不需要容器进程本身支持。在Docker之外排查容器问题时很管用比如容器内没有bash/sh或者容器主进程崩溃时nsenter可以从宿主机层面进入容器的网络命名空间做网络抓包定位。不过日常用exec就够nsenter属于进阶武器。2.2 分离模式下的日志查看-d参数与docker logs配合用-d把容器放到后台后日志输出不会直接显示在终端上这时要用docker logs来查看。# 查看容器全部日志 docker logs nginx-demo # 实时跟踪日志输出 docker logs -f nginx-demo # 显示最后50行并加时间戳 docker logs --tail 50 -t nginx-demodocker logs的底层是把容器内PID 1进程的stdout和stderr重定向到宿主机上的日志文件中。有个常见误区容器内应用如果直接把日志写到文件比如logs/app.log而不是输出到标准输出docker logs是看不到的。这也是12-factor应用推荐日志走stdout的原因——Docker原生日志机制只认标准输出和标准错误。生产环境里我一般还会配置json-file日志驱动的大小轮转避免容器日志无限增长撑爆磁盘docker run -d --log-driver json-file --log-opt max-size10m --log-opt max-file3 nginx这样单个日志文件最大10MB最多保留3个超过就自动切割。不加这个限制的后果我是见过的——一个没配日志轮转的容器运行两周日志文件膨胀到20多GB直接把宿主机磁盘写满。2.3 交互式容器的标准姿势先exec再操作进入容器操作的正确姿势很重要。我见过不少同事直接在容器里安装编辑器、修改配置然后容器一删全部白干。容器应该当作“一次性进程”来对待所有改动都应该通过镜像构建或挂载卷来固化。如果确实需要临时修改容器内配置做验证正确流程是# 1. 进入容器 docker exec -it mysql-test bash # 2. 修改配置 vim /etc/mysql/my.cnf # 3. 退出容器 exit # 4. 重启容器使配置生效 docker restart mysql-test但要注意这种修改在容器重建后就会丢失。所以排查问题可以这么干正式环境一定要把配置改动合并到镜像或挂载卷里。这也是为什么我用docker compose时习惯把配置文件用volume挂载出来改宿主机文件后重启容器就能生效不用进容器折腾。3. 数据卷挂载与网络映射打通容器和外界的通道3.1 数据持久化为什么必须用-v或--mount容器的可写层生命周期太短容器一删数据就没了这对数据库这类应用是灾难。解决这个问题的手段是数据卷volume或绑定挂载bind mount。# 具名卷数据由Docker管理推荐用于生产 docker run -d --name mysql-vol -v mysql-data:/var/lib/mysql mysql:8.0 # 绑定挂载把宿主机目录映射到容器方便调试 docker run -d --name nginx-bind -v /home/user/html:/usr/share/nginx/html:ro nginx:alpine具名卷和绑定挂载的区别我用一句话概括具名卷把数据放在Docker管理的目录下可通过docker volume inspect查看具体路径宿主机操作不直观但更干净绑定挂载直接把宿主机目录暴露给容器改代码、改配置立刻生效开发环境非常方便。有个关键参数是:ro只读挂载。把不需要容器写入的目录挂载成只读能减少误操作风险也能提升安全性。比如配置目录挂载为只读容器内即使被入侵也无法篡改配置。生产环境的MySQL容器必须做数据持久化这不用多解释。单纯用docker run跑一个不带volume的MySQL容器一删数据库全部消失这我见过不止一次了。3.2 端口映射和网络模式-p到底做了什么-p参数把宿主机的端口流量转发到容器的端口上。底层是Docker通过iptables的DNAT规则实现端口转发。# 宿主机8080 - 容器80 docker run -d -p 8080:80 nginx:alpine # 绑定特定IP只允许本机访问 docker run -d -p 127.0.0.1:8080:80 nginx:alpine # 随机分配宿主机端口 docker run -d -P nginx:alpine-P参数是随机映射容器所有暴露的端口到宿主机的高位端口实际项目中很少用但快速起服务测试时可以配合docker port命令查看映射关系。网络模式也值得展开说。默认的bridge模式是NAT网络容器有独立IP但无法直接从宿主机外部访问必须通过端口映射暴露。而host模式直接共享宿主机的网络命名空间容器不拥有独立IP监听端口就是宿主机的端口性能更好但隔离性变差。# host模式的nginx直接监听宿主机80端口 docker run -d --network host nginx:alpine容器访问外部地址的问题也常被人问。默认bridge模式下容器通过NAT访问外网是没问题的只要宿主机能上网。但容器访问宿主机上的服务时不能用localhost而要用host.docker.internal这个特殊域名。macOS和Windows的Docker Desktop直接支持Linux下需要在docker run时加参数docker run -d --add-hosthost.docker.internal:host-gateway nginx这个细节在开发和联调时非常实用。比如容器内要连接宿主机上的MySQL直接用localhost是连不上的加上这行参数后就能用host.docker.internal这个域名访问宿主机服务。3.3 多容器通信自定义bridge网络才是正经方案容器之间需要互相通信时用端口映射太绕了正确姿势是创建自定义bridge网络让容器通过网络名直接互相访问。# 创建自定义网络 docker network create my-net # 两个容器都加入同一个网络 docker run -d --name mysql --network my-net mysql:8.0 docker run -d --name app --network my-net -p 8080:8080 app-image # app容器里可以直接用mysql这个主机名访问数据库自定义bridge网络内置了DNS解析容器名自动变成可解析的主机名。这比通过IP通信靠谱多了因为容器重建后IP会变但主机名不变。这个机制也是docker compose默认的网络行为——compose里的服务名就是容器间的访问地址。这里我补充一个容易踩的坑默认的bridge0网络不支持容器间的DNS解析两个容器想互相访问只能靠IP。所以在多容器场景下一定要用自定义网络不要依赖默认bridge。4. 容器生命周期管理启动、停止、重启与资源管控4.1 容器状态的完整流转created到deleted我见过一张图把容器状态流转画得很清楚但平时不用记太多理解这几种状态就够了状态含义如何进入created已创建未启动docker createrunning运行中docker startexited已停止主进程退出或docker stoppaused已暂停docker pausedead异常状态无法正常停止特殊情况deleted已删除docker rmdocker run创建并启动容器后状态是running。容器PID 1进程退出后状态变为exited。容器不会自动被删除除非加了--rm参数。# 创建但不启动 docker create --name test nginx:alpine # 启动已存在的容器 docker start test # 停止容器 docker stop test # 强制停止 docker kill teststop和kill的区别值得说清楚。stop会先向容器主进程发送SIGTERM信号默认等待10秒可以通过-t修改等待时间超时后再发SIGKILL强制终止。kill则直接发送SIGKILL相当于进程被“处决”没有优雅退出机会。# stop和kill的区别 docker stop -t 30 nginx-demo # 给30秒优雅退出时间 docker kill nginx-demo # 立即强制终止生产环境停止服务时优先用docker stop给业务程序足够的收尾时间——比如数据库的刷盘、消息队列的确认、HTTP连接池的排空。直接kill很容易丢数据这个教训我在生产环境里领教过。4.2 --rm参数和容器清理如果只是临时跑个容器做测试建议加上--rm参数。容器停止后自动删除避免一堆Exited状态的死容器堆积在docker ps -a里。# 临时测试容器退出后自动清理 docker run --rm -it -v $(pwd):/app node:18 npm install但要注意--rm参数不能和-d一起用在某些需要重启的场景加了--rm的容器在stop后就会被删除再docker start也找不到了。还有--rm和docker commit是冲突的容器都删了还commit什么。定期清理宿主机上的无用容器和镜像是个好习惯# 删除所有已停止的容器 docker container prune # 清理悬空镜像 docker image prune # 一键清理所有未使用的容器、网络、镜像、构建缓存 docker system prune -adocker system prune -a这个命令威力很大它会删掉所有没有被容器使用的镜像包括你之前拉下来但暂时没用到的。执行前一定要确认没有需要的镜像。我吃过这个亏一次清理把本地的日志分析镜像删了重新拉取花了不少时间。4.3 资源限制防止容器吃掉宿主机全部内存默认情况下容器不受资源限制可以无限使用宿主机的CPU和内存。这在多容器共存的机器上是隐患——一个内存泄漏的容器就能拖垮整台机器。docker run提供了资源限制参数# 限制内存使用 docker run -d --memory512m --memory-swap512m --oom-kill-disablefalse nginx # 限制CPU使用 docker run -d --cpus1.5 nginx # 限制CPU份额相对权重 docker run -d --cpu-shares1024 nginx--memory参数限制容器最大内存使用量--memory-swap限制内存加交换分区的总量。这里有个容易踩的坑如果只设置了--memory而不设置--memory-swap--memory-swap会默认等于--memory的两倍容器实际可以使用内存加swap的总量会超过预期。所以严格限制内存时两个参数要一起设置。--oom-kill-disable表示当容器内存超限时是否允许内核OOM Killer直接杀死容器进程。默认false内存超限容器会被杀掉。如果设置成true容器会更稳定但可能宿主机内存被打爆。我建议保留默认的OOM Kill行为让系统自动保护同时配合--memory限制来约束容器。CPU的限制在业务负载较高时非常关键。--cpus1.5表示容器最多使用1.5个CPU核心。注意这个参数是CPU时间的比例不是固定的核心数容器可以在多核机器上使用任意核心但总CPU时间被限制在1.5核的范围内。4.4 容器重启策略让服务挂了也能自动恢复Docker支持通过--restart参数设置容器的重启策略这在服务常驻场景下非常实用。策略行为no默认值容器退出后不自动重启on-failure[:max-retries]非正常退出时重启可限制最大重启次数always无论什么原因退出都自动重启unless-stopped自动重启但手动停止后不会重新启动# 失败自动重启最多重试5次 docker run -d --restart on-failure:5 nginx # 宕机重启后自动拉起容器 docker run -d --restart unless-stopped nginx我用得最多的是unless-stopped。它和always的区别在于Docker服务重启时always策略的容器一定会被拉起来而unless-stopped策略下如果容器之前是被手动docker stop停止的就不会被自动启动。这个细节在生产环境很重要——主动停掉的维护窗口不希望机器重启后又自动跑起来。4.5 暂停与恢复pause和unpause的使用场景docker pause和stop看起来都让容器“停”了实际上完全不同。pause是通过cgroups冻结容器内所有进程进程还在内存里但不再被调度执行。stop则是让主进程收到信号后退出相当于整个容器生命结束。# 暂停容器 docker pause nginx-demo # 恢复容器 docker unpause nginx-demopause的典型场景是数据库备份时冻结写入或者排查问题时暂时冻结容器状态。它比stop快得多因为不需要经历进程退出的流程但会占用内存不适合长期暂停。我实际使用中pause用得不多stopstart基本能覆盖日常需求。5. 环境变量配置与容器内应用连接5.1 -e参数向容器注入配置的正确姿势很多官方镜像都支持用环境变量来配置应用。最典型的是MySQL通过-e参数指定root密码、创建数据库、创建用户docker run -d --name mysql-test \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEmyapp \ -e MYSQL_USERapp_user \ -e MYSQL_PASSWORDapp_pass \ mysql:8.0环境变量是容器向应用传递配置的最常用方式因为它在不同环境下可以灵活变化。开发环境、测试环境、生产环境只要用不同的-e参数就能复用同一个镜像而不需要为每个环境重新构建镜像。这正是docker镜像“构建一次到处运行”理念的关键支撑。Redis主从架构也能通过环境变量或命令行参数快速搭建# 启动主节点 docker run -d --name redis-master redis:7 redis-server --requirepass masterpass # 启动从节点并指定主节点 docker run -d --name redis-slave redis:7 redis-server \ --slaveof redis-master 6379 \ --masterauth masterpass从节点和主节点在同一个自定义网络里用容器名redis-master直接访问。整个Redis主从复制环境两条命令就搭起来了这比手动安装配置高效得多也适合在本地模拟生产环境做测试。5.2 环境变量和配置文件的选择环境变量适合配置简单的标量值但配置项特别多的时候把整个配置文件挂载进去更合理。MySQ L的my.cnf、Nginx的nginx.conf、Redis的redis.conf这些复杂配置我都推荐用-volume挂载而不是堆几十个环境变量。docker run -d --name nginx-conf \ -v /home/user/nginx.conf:/etc/nginx/nginx.conf:ro \ -p 80:80 nginx:alpine有个细节很多人不知道MySQL的官方镜像在第一次启动时会通过docker-entrypoint.sh执行一些初始化逻辑如果挂载的配置文件里有语法错误容器可能反复重启。这时候第一件事不是改配置而是看日志定位到底是配置解析失败还是其他原因。我后面会在常见问题里专门讲这个排查思路。6. 容器安全与镜像安全的基本实践6.1 权限控制不要用root跑容器默认情况下容器内的用户是root这在宿主机上对应着root权限存在安全隐患。如果容器被攻破攻击者就有了宿主机root身份的基础。安全实践里有一条基本原则容器内使用非root用户运行进程。# 指定容器内运行用户为uid 1000 docker run -d --user 1000:1000 nginx也可以在镜像构建时用Dockerfile创建一个专用用户FROM node:18 RUN useradd -m appuser USER appuser COPY app.js /app/ CMD [node, /app/app.js]有些镜像默认就非root运行比如nginx官方镜像的最新版本已经是nginx用户运行了这也是官方推荐的做法。但我见过一些第三方镜像内部还是root所以拉取镜像时要关注镜像的维护者、下载量、安全扫描报告。镜像安全是容器安全的地基基础镜像不干净容器里装什么防护都白搭。6.2 只读文件系统和capability收敛一个提高安全性的简单做法是把容器的根文件系统挂载为只读只把需要写入的目录用volume挂载出来通过截断写入来减少攻击面例如等保加固后系统就会要求只读挂载根目录。docker run -d --read-only -v /data:/var/lib/mysql mysql:8.0容器默认拥有一组Linux capability如果不需要某些特权能力可以用--cap-drop把它们去掉比如禁用SETUID、NET_RAW等。这些属于进阶安全加固项但了解了对理解容器的隔离边界很有帮助。7. 常见问题与排查技巧实录7.1 容器启动后立刻退出Exited状态这是docker run新手遇到最多的问题。现象是docker run之后docker ps看不见容器docker ps -a也不显示状态但容器内看不到任何东西。原因通常有三种一是前台模式下容器内没有常驻进程。比如docker run ubuntu不指定命令或者指定的命令执行完就退出容器就随之终止了。这种情况可以加-d或用tail -f /dev/null保活。二是应用启动失败比如MySQL初始化出错、配置错误、端口被占用。这时候第一件事是看日志docker logs mysql-error日志会直接告诉你失败原因我遇到过的大部分秒退问题都是这个原因——不是Docker的问题而是应用自己没起来。三是指令执行错误仓库地址写错或者镜像名打错了。这时候会提示找不到镜像则拉取失败如果拉下来了但秒退就属于前两类。7.2 排查思路别乱先状态再日志最后资源我一个朋友用docker run起了一个PostgreSQL容器反复Exited折腾了一个小时最后发现是宿主机磁盘满了。这个排查顺序很重要先看容器状态和退出码再看应用日志最后查宿主机磁盘和内存。# 查看容器退出码 docker inspect postgres-test --format{{.State.ExitCode}} # 退出码137 被SIGKILL杀死通常是内存超限 # 退出码1 应用自身报错需要看docker logs # 退出码0 正常退出说明主进程退出了退出码137的常见场景是内存超限被OOM Killer杀掉。如果容器设置了--memory限制第一反应应该是调大内存限制或者优化应用内存占用而不是反复重启。7.3 端口绑定冲突怎么办启动容器时提示port is already allocated说明宿主机端口被占用了。排查方法# 查看端口占用 lsof -i :8080 # 或者 netstat -tlnp | grep 8080 # 查看是哪个容器在占用 docker ps --format {{.Names}}\t{{.Ports}}找到占用方后要么停掉占用端口的容器要么换一个端口重新映射。还有一种情况是Docker自己用的端口和宿主机的其他服务冲突了这就需要在/etc/docker/daemon.json里调整Docker的启动参数比如修改默认的bridge网段。7.4 容器里访问外网失败容器默认bridge模式下可以访问外网但如果宿主机开启了严格的防火墙规则容器NAT流量可能被拦截。排查方法# 进入容器测试网络 docker exec -it nginx-test ping www.baidu.com # 如果在容器里ping不通但宿主机可以检查iptables的FORWARD链 iptables -L FORWARD -n容器通信失败时先判断是DNS问题还是网络不通ping不通域名但ping通IP说明DNS解析有问题ping不通IP说明路由或iptables有问题。Docker Desktop在Windows上偶尔会遇到虚拟网卡配置问题导致容器网络异常重启Docker Desktop或者在设置里重置网络往往能解决。7.5 镜像下载慢的应对思路拉镜像慢是另一个高频痛点原因是从默认的Docker Hub拉取镜像访问速度受网络环境影响。常规做法是配置国内镜像加速器在Docker Desktop或/etc/docker/daemon.json中设置registry-mirrors可以明显加快镜像拉取速度。这个配置属于运维常识不是灰色操作Docker的镜像加速机制本身也是官方支持的标准功能。8. 实操总结一个典型的docker run完整场景我把上面这些知识点串成一个典型的实战场景在本机用Docker运行一个MySQL 8.0绑定指定端口设置数据持久化配置密码和数据库并限制资源使用。# 1. 创建自定义网络 docker network create app-net # 2. 运行MySQL容器 docker run -d \ --name mysql-app \ --network app-net \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEappdb \ -v mysql-data:/var/lib/mysql \ --memory1g \ --restartunless-stopped \ mysql:8.0 # 3. 查看容器状态 docker ps # 4. 查看MySQL日志 docker logs --tail 50 mysql-app # 5. 进入容器执行SQL docker exec -it mysql-app mysql -uroot -p # 6. 停止并启动容器 docker stop mysql-app docker start mysql-app这条命令把所有关键参数都用上了-d后台运行、--name命名、--network指定网络、-p端口映射、-e配置环境变量、-v持久化数据、--memory限制内存、--restart设置重启策略。跑起来之后无论外部应用还是容器内其他服务都能通过mysql-app这个容器名访问数据库。我个人在实际操作中的体会是docker run虽然参数多但真正高频的核心参数就那几个。把每个参数的底层逻辑想明白比死记硬背一堆命令有效得多。比如--rm和-v为什么需要配合使用、-it为什么必须成对出现、--restartunless-stopped和always有什么区别——这些理解透了Docker的很多操作就是顺手的事。最后再分享一个小技巧每次docker run之前先想清楚“这个容器是临时的还是永久的”。临时验证用--rm -it干净利落服务常驻用-d --restart volume保障稳定。养成这个习惯后你会发现Docker为你解决的问题远多于你背的参数。