刚学 Docker 那几天,我最常干的蠢事就是——容器跑完之后手一抖docker stop了,转头想再进去看一眼,敲docker ps,结果屏幕上干干净净,一个字都没有。那一刻我脑子里只有一个念头:完了,容器是不是被我删了?镜像是不是也得重装?后来才知道,容器只是"睡了",不是"没了",用一条docker start就能把它重新叫醒。这篇就把"如何启动一个已经停止的容器"这件事从头到尾讲透:为什么停止后容器还在、start和run到底差在哪、start -a -i什么场景用、启动后又秒退怎么排查、端口为什么还可能连不上,以及几条我踩过坑之后养成的日常习惯。内容面向刚上手的新手,不需要你懂内核原理,跟着敲命令就能复现。
1. 先弄清楚容器"停止"之后到底还在不在
1.1 docker ps 里看不到,不等于容器被删了
新手最大的误解,就是把docker ps的空输出当成"我的容器没了"。其实docker ps这个命令的默认行为是只列出正在运行(Up)的容器,停止的、创建了但没启动的,它一律不显示。你要看全部,得加-a,也就是docker ps -a,-a是--all的缩写。
敲一下就知道差别有多大:
# 只看运行中的,大概率是空的 docker ps # 看全部,包括 Exited 的 docker ps -a输出里你会看到一列STATUS,写着类似Exited (0) 3 days ago。这句话的信息量其实很大,它是一个"停止状态 + 退出码 + 停止时间"的组合。只要这一行还在,就说明容器实体依然躺在你的磁盘上,随时可以被docker start拉起来。
我习惯用一个类比:镜像像是买房时的样板间图纸,容器像是照着图纸实际盖出来的一套房子。你docker stop只是把房子里的灯关了、人赶出来了,房子本身还在原地;只有docker rm才是真把房子拆了。而docker ps相当于只给你看"现在亮着灯的房子",当然看不到那些关了灯的房子。
所以第一件事记住:要判断一个容器是不是还存在,永远用docker ps -a,不要用docker ps。
1.2 停止为什么不会丢数据:可写层还在原地
很多人担心stop之后数据丢了,这个担心可以放下。这里得说说 Docker 的分层结构,理解了它,后面很多坑你都能自己解释。
一个容器的文件系统由两部分叠起来:底下是镜像的只读层,上面是 Docker 在启动容器时给它单独挂的一层可写层。你在容器里新建的文件、改过的配置、装的包,全都落在那个可写层里,而可写层只属于这个容器自己,别的容器看不到。
docker stop做的事情是:给容器里的主进程发一个停止信号,等它退出,然后容器进入Exited状态。整个过程完全不碰可写层。所以你下次docker start起来,之前写到/tmp、/var/log、改过的nginx.conf,只要是在可写层里的,都还在。
真正会毁掉可写层的只有docker rm。这一点和虚拟机特别像:关机不丢盘,删虚拟机才丢盘。
顺带把几个容易混淆的命令摆在一起对照,这个表我建议你抄进笔记:
| 命令 | 作用对象 | 对容器的后果 | 数据是否保留 |
|---|---|---|---|
docker stop | 运行中的容器 | 进程优雅退出,状态变 Exited | 保留 |
docker kill | 运行中的容器 | 直接强杀进程,状态变 Exited | 保留 |
docker start | 已停止的容器 | 重新拉起主进程 | 保留 |
docker restart | 运行中的容器 | stop 再 start 的组合动作 | 保留 |
docker rm | 已停止的容器 | 删除容器实例和可写层 | 丢失 |
docker rmi | 镜像 | 删除镜像,和容器不是一回事 | 视情况 |
看最后两行要注意区分:rm删的是容器,rmi删的是镜像。新手经常混,删错了对象还以为自己操作的是同一个东西。
1.3 Exited 后面那个数字,其实是排错的第一个线索
Exited (0)、Exited (1)、Exited (137)这些括号里的数字是退出码,来自容器里那个主进程退出时返回的状态。它不会直接告诉你"哪里错了",但能帮你快速缩小范围。
Exited (0):主进程自己正常结束了。这里的关键词是"正常"——对一次性任务(跑个脚本、导一次数据)这是好事;对 nginx、redis 这种本该长期跑的服务,这说明它的主进程根本没打算常驻,八成是你的启动命令配错了。Exited (1):应用自己报错退出了,通常是配置问题、依赖缺失、端口被占。看日志去,docker logs里一般有明确的报错行。Exited (137):这个数字是 128 + 9,9 就是SIGKILL。含义是被强制杀掉了,常见于docker kill、内存超限被系统干掉(OOM)。如果是 OOM,docker inspect里的OOMKilled字段会是true。Exited (143):128 + 15,收到SIGTERM退出,也就是docker stop在超时前正常停掉的结果。
记不住也没关系,只要养成习惯:先看退出码判断方向,再去docker logs找具体原因。
2. docker start 到底做了什么
2.1 start 和 run 的分工:一个复用旧容器,一个新建容器
这是新手最容易混的一对命令,我用一句话概括:docker run等于"创建 + 启动"两步,每次都造一个新容器;docker start只启动已经存在的旧容器,不创建新的。
你第一次跑docker run -d --name web nginx,Docker 干了这些事:找一个叫 nginx 的镜像,基于它创建出一个容器(随机或指定名字),然后启动它。这条命令背后其实包含了docker create加docker start两个动作。
第二次你要是再敲一遍同样的docker run -d --name web nginx,会直接报错:容器名web已经被占用了。因为run又想创建一个叫web的容器,名字冲突。这时候正确的做法是:
docker start web它不会新建任何东西,只是把那个已经停止的web容器原地唤醒。容器 ID 不变、名字不变、可写层内容不变、端口映射不变,一切照旧。
这个区别的重要性在于排错思路。你发现容器起不来了,如果用的是run,那每次都在一个新容器上试,上次改的文件、装的包全都白搭;如果用start,改一次试一次都在同一个容器上,问题定位快得多。
2.2 容器怎么被"唤起":它得有一个不肯退出的主进程
理解start的底层逻辑,关键是搞懂"容器为什么能一直运行"。答案是:容器里必须有一个前台常驻的主进程在扛着,Docker 把它的 PID 视为 1。只要这个进程不退,容器就一直是Up;它一退,容器立刻变Exited。
这就是无数新手翻车的地方。拿 Ubuntu 镜像举例,你敲:
docker run -d --name u1 ubuntu然后docker ps一看,没有。docker ps -a一看,Exited (0)。为什么?因为 ubuntu 镜像的默认命令是/bin/bash,你没有给它分配终端也没给它输入,这个交互式 shell 拿不到任何输入,就自己退出了。shell 一退,容器自然跟着停。
想让它待着,就得给它一个不结束的活干,或者给它一个能接输入的终端:
# 方式一:让它跑一个不退出的命令 docker run -d --name u1 ubuntu sleep infinity # 方式二:开一个交互终端,退出前它一直活着 docker run -it --name u2 ubuntu服务型镜像就没这个问题。nginx 镜像的默认命令就是前台启动 nginx,redis 的默认命令就是前台跑 redis-server,mysql 也是。这些主进程天生不退出,所以用-d跑起来之后容器稳稳地Up。
理解到这一层,你就能自我解释了:容器 stop,本质上就是主进程结束了。所以 start 能不能成功,取决于它的主进程这次能不能撑住。如果一个容器的启动命令本身就是"跑完就退"的类型,那start多少次它都会立刻再退——这不是start有问题,是容器设计如此。
2.3 start 的三个参数:-a、-i、-d,以及什么时候该用 exec
docker start默认是"后台启动",也就是加了-d的效果。它还有两个有用的参数,配合交互式容器时特别关键:
-a(--attach):把容器的标准输出、错误输出接回你的当前终端,你能实时看到它的打印。-i(--interactive):把标准输入接回来,你能往里敲东西。- 两个一起用
-a -i,就等于"重新坐到这台机器面前"。
于是我前面那个u2容器(用-it启动、然后被我Ctrl+P Ctrl+Q剥离或者直接停掉的),可以这样重新接进去:
docker start -a -i u2这时你又能看到熟悉的 shell 提示符了,退出时敲exit,容器再次停止。
但我得提醒一句:别把start -a -i当成日常进入容器的常规手段。它适合"我就是来重新接管这个一次性交互容器"的场景。真正跑服务的容器(nginx、redis、mysql)里,你更该用:
docker exec -it web bashexec的特点是在已经运行的容器里新开一个进程,你的操作和容器的主进程互不干扰。你退出 exec 的 shell,容器该跑还是跑。而start -a -i是把整个容器的主进程交到你手上,你一按Ctrl+C,主进程可能就被你停了,容器跟着挂。
这张表帮你快速决定:
| 场景 | 推荐命令 | 原因 |
|---|---|---|
| 后台服务重启 | docker start web | 默认后台,主进程自己扛 |
| 重看一个交互容器的输出 | docker start -a web | 只接输出,不接管输入 |
| 重新接管一个 bash 容器 | docker start -a -i u2 | 需要能敲命令 |
| 进入正在跑的服务容器里看文件 | docker exec -it web bash | 不动主进程,安全 |
| 只是想看日志 | docker logs -f web | 最轻量,不干扰进程 |
3. 手把手:把停止的容器启动起来的完整操作
3.1 先精准找到目标容器
在start之前,你得先知道要给谁发指令。目标可以用名字或ID指定。名字是你在run时用--name取的,或者 Docker 随机分配的;ID 是一长串十六进制,一般取前几位能唯一区分就行。
日常找容器,我最常用的几条命令:
# 全部容器,含已停止 docker ps -a # 只看状态是 Exited 的 docker ps -a --filter "status=exited" # 只显示最近创建的 3 个 docker ps -a -n 3 # 只要 ID,方便脚本里用 docker ps -aq # 自定义输出列,看名字和状态 docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"--filter这个参数值得单独说一下,它能按状态、按镜像、按名字前缀筛。容器多起来之后,一屏几十行,人工找起来很费眼,用它就能瞬间收敛。--format用的是 Go 模板语法,{{.Names}}、{{.Status}}、{{.Image}}、{{.Ports}}这几个字段用得最多,第一次照着抄就行。
3.2 用名字还是 ID,以及要不要先重命名
我强烈建议养成给容器取名的习惯。docker run --name web nginx就多敲这一小段,后面省心无数。名字比 ID 好记、好读、好写脚本。
docker start web如果当初没命名,也可以用 ID 前缀:
# 先查 ID docker ps -a # 假设看到 3f2a91b8c4d1 docker start 3f2a # 只要前缀能唯一区分即可名字取错了也别慌,容器停止状态下可以改名:
docker rename 老名字 新名字不过改名的成本比一开始取好名字高得多,尤其是当你的脚本、文档里已经引用了旧名字。所以讲到底,还是那句话:--name该加就加。
3.3 启动之后,用三条线确认它真的起来了
docker start web敲完,屏幕上只会打印一个web,就没了。这个输出不代表成功启动,只代表 Docker 接收了你的指令。真正的"活了没",得从三个角度确认。
第一条线看状态:
docker ps --filter "name=web"STATUS列要是显示Up 8 seconds之类的,说明主进程撑住了。如果它又变成Exited,那一秒是骗你的,得往下排查。
第二条线看日志,看它启动过程做了什么、有没有报错:
docker logs --tail 50 web # 想持续跟就跟加 -f docker logs -f web--tail 50表示只输出最后 50 行,新容器内容多的时候不至于刷屏。-f是 follow,实时跟踪,跟tail -f一个道理,调试时很好用。
第三条线看端口和细节:
# 看端口映射 docker port web # 看容器详细状态 docker inspect --format '{{.State.Status}} {{.State.RestartCount}} {{.State.OOMKilled}}' webdocker inspect里的State.RestartCount是重启次数,OOMKilled表示是不是被内存超限杀过,这俩字段排查"反复重启"和"被系统干掉"时特别有用。这三条线走完,你基本就能确定容器到底是健康、还是可疑、还是根本没好。
3.4 批量启动多个容器:一次把一排拉起来
真实场景里,你往往有不止一个容器。用docker-compose up -d自然是主流做法,但如果就是几个独立容器,命令行也能一次启动:
# 指定多个名字,空格分隔 docker start web redis mysql # 启动全部已停止的容器,把状态是 Exited 的 ID 全丢给 start docker start $(docker ps -aq --filter "status=exited")第二条命令是个非常实用的"一键全启"。它先让docker ps -aq --filter "status=exited"把停止容器的 ID 全列出来,再用$()拼成参数交给docker start。不过用之前提醒一句:别在机器上堆了几十个实验容器时无脑执行它。把一堆你早就不要的容器全拉起来,内存和端口都会打架,反而是给自己找麻烦。批量启动前先docker ps -a扫一眼,心里有数再敲。
4. 启动之后又秒退、连不上?高频原因排查链路
4.1 现象是"Up 一秒就 Exited",排查顺序别乱
docker start web回车后,docker ps短暂看到Up 1 second,再刷一下又变回Exited,这是新手最崩溃的场景之一。这时候,不要东一榔头西一棒槌瞎试,按顺序走效率最高:
- 先看退出码:
docker ps -a里Exited括号里的数字。0 说明主进程自己退了;1 说明应用报错;137 是被强杀或 OOM。 - 再看日志:
docker logs web,绝大多数错误信息都在这。 - 再查配置:配置文件路径对不对、环境变量有没有丢、依赖服务在不在线。
- 接着看资源:是不是内存超了、磁盘满了。
- 最后才考虑重建:确认是可写层或镜像本身状态坏了,才考虑
rm重新run。
这个顺序的核心是从成本最低、信息量最大的动作开始。退出码看一眼只要一秒,日志一秒就能拉,先做这两步,能省下大量瞎猜的时间。
4.2 交互式容器启动即退:Tty 与 stdin 的锅
前面 Ubuntu 的例子已经点过一次,这里再展开说透,因为这是"我明明停好了、为什么 start 又秒退"的最高频原因。
场景是这样的:你之前跑过一个docker run -it --name dev ubuntu的容器,在里面装了软件,然后exit出来了,容器变Exited。现在你想接着用,敲:
docker start dev结果它又Exited了。为什么?因为docker start dev是后台启动,没有给这个 bash 主进程分配 stdin 和 tty,bash 拿不到输入,立刻就结束。你之前能进去,是因为run时带了-it。
正确的姿势是:
docker start -a -i dev-a接回输出,-i接回输入,为了能正常交互,通常还建议配合-t分配伪终端(有些老版本要显式加)。如果只是想让它后台待着别退,可以换一种活法:
docker start dev # 它退了?那就别用 bash 当主进程 docker run -d --name dev2 ubuntu sleep infinity一句话总结这个坑:交互式容器的启动必须带上交互参数,后台启动一个 bash 容器,等于让它孤独地等待一个永远不会来的输入,它只能自己走。
4.3 端口连不上:start 不会给你补上当初没做的映射
这个坑我自己真踩过。当时我在容器里跑了个服务,用docker run -d --name app myimage起的,没加-p,容器里的服务监听 8080,我在宿主机怎么访问都连不上。我以为重启一下就好了,docker stop app又docker start app,结果还是一样。
根本原因得说清楚:端口映射是在docker create(也就是docker run那一刻)就固定下来的,属于容器的网络配置。docker start只是复用这套已经存在的配置,它不会、也无法给你追加新的-p映射。
所以:
- 当初
run时没写-p 8080:8080,那这个容器这辈子都没有这个映射,start多少次都没用。 - 当初
run时写了,那start之后映射照旧生效,你也不用重新指定。 - 宿主机端口被别的程序占了(比如本地也跑了个同端口的服务),报的错发生在
run那一刻,跟start关系不大。
那如果当初没映射现在想补怎么办?没有直接修改的办法,只能把容器"另存"出来重做:
# 把当前容器保存成新镜像 docker commit app myimage:fixed # 用新镜像重新 run,这次带上端口映射 docker run -d --name app2 -p 8080:8080 myimage:fixeddocker commit会把容器的可写层打包进新镜像,你之前改的东西不会丢。但这个方式不算优雅,长期看更该用 Dockerfile 把镜像构建流程固化下来。临时救急用它没问题。
4.4 数据"看起来没了":你多半是又 run 了一个新容器
还有一种误判特别常见:用户stop了容器,想恢复数据,顺手敲了docker run ...(可能还是同一条命令、换了个名字),结果打开一看,之前装的东西、写的文件全不见了,于是得出结论"Docker 不持久化数据"。
其实真相是:你run的是一个全新的容器,它有自己全新的可写层,旧容器里那层数据根本没带过来。旧数据还在旧容器里,你用docker start 旧容器名就能看到。
想彻底避免这种困惑,需要区分两个概念:
- 可写层:只属于某一个容器,容器删了就没了,别的容器看不到。
- 数据卷(volume)或绑定挂载(bind mount):独立于容器的存储,通过
-v挂进去,容器删了数据还在,多个容器也能共享。
判断你该用哪种,看数据的定位:临时产物、跑完就算的,扔可写层;数据库文件、上传目录、配置这种"容器重建后还得留着"的,必须用-v挂出来。我给新手的一句忠告是:凡是你希望"下次还在"的数据,就别放在容器可写层里,一开始就用卷。这条我要是早两年懂,能少折腾好几天。
5. 把"停-启"用顺手:几个值得写进肌肉记忆的设置
5.1 --restart 策略:让容器自己站起来
每次机器重启或者容器意外挂掉,都要你手动docker start,那也太累了。--restart就是解决这个的。它在docker run时指定,也可以在运行后改。
| 策略 | 行为 | 适合谁 |
|---|---|---|
no | 从不自动重启(默认) | 一次性任务、临时实验 |
on-failure | 只有非 0 退出码时才重启 | 会偶发崩溃的任务 |
on-failure:3 | 同上,最多重启 3 次 | 防止无限重启刷屏 |
always | 不管怎么退出都重启,重启 Docker 也会拉起 | 长期在线的服务 |
unless-stopped | 类似 always,但你手动 stop 之后就不再自动拉起 | 想手动控制停机时机的服务 |
后两个的差别是新手最容易搞混的,我用具体场景说:always意味着哪怕你手动docker stop了它,下次 Docker 守护进程重启(比如你重启了宿主机),它还是会自己起来。unless-stopped会记住你手动停过,不主动打扰你。我个人的偏好是本地开发环境用unless-stopped,因为我要的是"它能自己扛住意外挂掉,但我说停就真停",这个语义最贴合开发场景。
运行中的容器改策略,不需要重建:
docker update --restart=unless-stopped webdocker update也能顺手改内存、CPU 限制,属于"不改容器就能调参"的少数手段之一。
5.2 别为了启动而启动:分清什么情况该重建
新手容易把所有问题都往start上招呼,但不是所有情况都能靠start解决的。我总结了一条判断线:
- 只是容器停了,配置和镜像都没动:直接
docker start。 - 只是想让某个挂了的服务重新跑:
docker restart一条更省事,它等于 stop 加 start。 - 要更新应用代码、改端口映射、换镜像版本:别再启动旧容器了,重建才是对的。用 Compose 的话就是
docker-compose up -d,它会自动判断哪些需要重建。 - 容器文件系统坏了、状态脏了:也只能删掉重建。
判断核心是问自己一句:我想改的东西,是运行时状态,还是容器定义?状态的问题,重启能解决;定义的问题,只能靠重建。这一句话能帮你省下一堆无用的尝试。
5.3 停止也有讲究:stop 的优雅和 kill 的强硬
既然聊启动,停止也顺带说清楚,因为它们是成对的。docker stop和docker kill的区别,不只是"温柔"和"暴力":
docker stop会给容器主进程发SIGTERM,让它有机会做收尾(关连接、写盘、清临时文件)。默认等 10 秒,还没退才升级为SIGKILL。这个时间可以用-t调:
docker stop -t 30 webdocker kill直接发SIGKILL,没有商量余地,对数据库这类应用要慎用,容易留下不一致的状态。
我给的经验是:日常一律用stop,只有在容器已经卡死、stop 也停不下来的时候才动kill。尤其是跑 MySQL、Postgres 的容器,优雅关闭能大幅降低数据损坏风险。这里也顺带呼应一下数据卷那条:有了稳定卷,优雅停止的意义才更实在。
5.4 Exited 容器会越堆越多,定期收一收
一个隐形问题是:你每敲一次docker run,就多一个容器实体,哪怕它停止了也还占着磁盘的可写层。我见过不少人的机器上躺着上百个 Exited 容器,docker ps -a翻半天。定期清理是必要的:
# 删除所有已停止的容器 docker container prune # 或者更直接,把已停止容器的 ID 全删掉 docker rm $(docker ps -aq --filter "status=exited")docker container prune会先让你确认,删之前看清楚列表。这里有个坑必须提醒:如果某个容器的数据你还没挂出来、直接放在可写层里,prune一刀下去就没了。所以清理前先确认两件事——这些容器是实验用的、以及重要数据是不是都用-v挂到卷里了。别问我为什么强调这点,答案你可能已经猜到了。
另外还有一种连锁坑:删容器时没注意关联的卷,docker rm -v会连带把匿名卷也删掉。默认的docker rm不动卷,但如果你加了-v,匿名卷就跟着走。用命名卷的项目一般不受影响,但临时用-v /data挂匿名卷的场景要格外小心。
补充一句和上下文相关的判断逻辑:容器停止的原因、容器启动后能否常驻、数据能否跨容器保留,这三件事各有各的答案,别指望一条命令全解决。退出码告诉你"怎么停的",主进程类型告诉你"能不能常驻",卷的配置告诉你"数据安不安全"。
一路写下来,我自己最想做的一件事,就是让看的人别重走我那条弯路。我第一次用 Docker 的时候,为了"恢复"一个停了的容器,硬生生重新run了三遍,还把旧容器删了,最后发现数据全丢在删掉的可写层里。现在我的习惯固定成三条:给容器都起名、重要的数据一律挂卷、停止的排错先看退出码再看日志。这三条不复杂,但每一条都帮我省过真正的时间。至于start本身,其实就一条命令的事,难点从来不在命令本身,而在于你清楚它到底在启动什么、以及启动不起来的时候该往哪看。