做Docker这几年,我最常被问到的不是“这个命令什么意思”,而是“我照教程敲了,为什么跑不起来”。其实绝大多数问题都出在同一个地方:对Docker命令背后的对象模型不够熟。镜像、容器、网络、存储卷,这四类对象各有各的生命周期,命令再多也就是对这四类对象的增删改查。这篇就把我日常用得上的Docker命令按场景重新捋一遍,从装好Docker到生产环境排障,每条命令都会说清楚它解决什么问题、什么时候用、坑在哪。新手可以按顺序读,老手建议直接跳到第6节的排障速查表。
1. 先搞明白:Docker命令从来不是背出来的,而是一套对象操作逻辑
接触过虚拟机的人上手Docker,通常会觉得很顺手,因为思路很像:装系统、装软件、打快照、克隆。但Docker真正好用的点在于它把“环境”本身拆成了可移动、可复制的对象。你拉一个镜像,等于下载了一份完整的运行环境模板;你启动一个容器,等于基于这个模板开了一个实例;你把某个目录挂进容器,等于让实例里的文件跟宿主机实时同步。理解了这个模型,后面所有命令都只是“在哪个对象上做什么动作”的选择题。
1.1 从“装个软件”到“跑一个环境”,Docker到底改变了什么
以前部署一个新项目,流程大概是:买服务器、装系统、装数据库、装运行时、配环境变量、调防火墙、试运行、再调、再试。这一套下来,小半天就没了,而且每一步都可能因为系统版本、依赖冲突、端口占用而卡住。换成Docker之后,部署这个动作变成了两句话:拉镜像,起容器。
我举个具体的例子。你需要在服务器上跑一个MySQL 8.0,传统装法要处理yum源、初始化密码、配置字符集、开放端口,一不小心还能把系统自带的MariaDB冲突给踩出来。Docker的装法是这样:
docker pull mysql:8.0 docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=你的密码 \ -v /data/mysql:/var/lib/mysql \ mysql:8.0两条命令做完,数据库就起来了,数据落在宿主机的/data/mysql目录里,密码通过环境变量注入。换一台机器,同样的两条命令,一模一样的环境。这就是“环境即代码”的直观感受。Docker命令的价值不在于“能敲出什么”,而在于你使用命令时是否清楚这一条命令在改变哪个对象、影响哪些范围。
1.2 Docker命令的全景地图:镜像、容器、网络、存储
我自己总结过一个粗线条的划分:镜像命令以docker image开头,容器命令以docker container开头,网络命令以docker network开头,存储命令以docker volume开头。新版本官方鼓励使用这种“对象+动作”的完整写法,但为了敲起来快,很多简写也保留着,比如docker ps其实就是docker container ls,docker run本质上是docker container run的简写。
初学者常犯的一个毛病是把命令记混,比如docker rm和docker rmi,一个删容器,一个删镜像,拼写只差一个字母。我见过不止一次有人想删镜像,结果把正在跑的容器给删了,虽然通过rmi删镜像时它会提示容器还在运行,但如果你先手动rm了容器,接着又去rmi,一旦镜像被多个容器引用,Docker会拒绝删除并提示冲突。这些细节光靠背命令是记不住的,必须在实际使用中踩过一次才有感觉。
所以这篇我不会做成“字典式命令罗列”,而是按照一条真实的使用路径来讲:装环境、拉镜像、跑容器、配网络、挂存储、上生产、排故障。每一步涉及哪些命令,我会把参数讲透,顺带说清楚哪些命令我实际生产中很少碰,哪些是一定要刻进肌肉记忆的。
2. 环境准备:安装、守护进程与基础姿势
在敲任何Docker命令之前,你得先确保Docker本体是健康的。很多人拿Linux服务器练手,装完Docker就直接docker run,结果报一堆莫名其妙的错,最后发现是Docker服务压根没启动。这个问题比你想的常见得多。
2.1 不同系统上的安装方式与注意点
Linux上最省事的安装方式是用官方脚本,但生产环境我建议走发行版自带的包管理,这样后续升级和依赖管理都更可控。Ubuntu和Debian系的安装可以这样处理:
# 更新索引并安装依赖 sudo apt update sudo apt install -y ca-certificates curl gnupg # 添加Docker官方GPG密钥和源 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-pluginCentOS系则是用yum(或新版系统的dnf)添加源后安装docker-ce。这里有个很容易被忽略的点:装完Docker之后,默认只有root用户能和Docker守护进程通信,普通用户直接敲docker命令会报“permission denied”。解决办法是把用户加进docker组:
sudo usermod -aG docker $USER # 退出终端重新登录,组权限才会生效你可能会在网上看到别人建议改/var/run/docker.sock权限来绕过去,我不建议那么做,那等于把Docker的管理权限对所有人敞开了,存在被人借权限做危险操作的可能,比如挂载宿主机根目录到容器里面改你的系统文件。
Windows和macOS上基本就是安装Docker Desktop,一个图形界面跑完所有事。但Windows上有个前提条件容易栽跟头:CPU的虚拟化功能得开着,Hyper-V或者WSL2要处于可用状态。不少人卡在安装后的下一步,双击Docker Desktop图标,状态栏提示“virtualization support not detected”,这就是虚拟化没开。
2.2 安装后必做的三个检查命令
装完Docker,别急着拉镜像,先确认三件事:Docker服务是不是活着、命令行能不能连通守护进程、当前版本号是多少。
# 查看Docker服务状态(systemd环境下) systemctl status docker # 或者直接看docker info,这个命令同时会给出版本、存储驱动、镜像数、容器数等一大堆信息 docker info # 单独确认版本 docker version我给新人讲这三个命令时通常会强调:docker version不像别的软件那样只打印一个版本号,它分两段输出,一段是Client客户端版本,一段是Server服务端版本。如果Server那段是空白或者报错Unavailable,说明你的命令行根本没连上Docker守护进程,后面的命令大概率全是“Cannot connect to the Docker daemon”。服务端没起来就systemctl start docker,套接字权限不对就从用户组和socket文件两个方向排查。
还有个小习惯:把当前用户加进docker组之后,重新登录终端,先敲一句docker info看看有没有继续报权限错。这一步确认干净了,后面所有命令才能顺畅执行。
2.3 启动失败的常见原因与处理路线
接手过的机器里,Docker Desktop启动失败最常出现的报错有几种。一种是刚才说的“virtualization support not detected”,多半是BIOS里Intel VT-x或者AMD-V没开,进BIOS找Intel Virtualization Technology或者SVM Mode这类选项打开就行。另一种是提示WSL2相关错误,比如“WSL 2 installation is incomplete”,这种一般要去启用Windows的“适用于Linux的Windows子系统”和“虚拟机平台”两个功能,然后重启加载WSL2内核。
再有一种报错很磨人:“failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen...”。npipe是Windows上的命名管道,出现这个报错一般意味着Docker Desktop的后端引擎还处于启动中,或者已经崩溃。我自己的处理顺序是先右键托盘图标看引擎是否Running,不行就Quit Docker Desktop后重新打开,再不行去命令行执行docker context ls看看当前上下文,绝大多数情况下切换回default上下文或者重启引擎能解决。
Linux服务器上则常见另外几种启动失败,比如镜像存储目录磁盘满了、daemon.json配置有语法问题、selinux或防火墙拦截了容器流量。排查统一用journalctl -u docker看守护进程日志,能直接定位到具体原因。
3. 镜像操作命令:从拉取到瘦身
镜像就是容器的模板。你对模板操作得熟练,容器才能真正跑得干净、跑得稳定。拉镜像谁都会,真正拉开差距的是对镜像细节的把控:版本标签、多架构、层结构和体积。
3.1 拉取、查看、打标签与删除镜像
拉镜像最常用的是docker pull,后面跟镜像名加版本标签。比如:
# 拉取最新版 docker pull nginx # 实际上等于拉了nginx:latest # 拉取指定版本 docker pull mysql:8.0 # 拉取指定平台的镜像,这在Mac或者ARM服务器上非常有用 docker pull --platform linux/amd64 mysql:8.0别小看latest这个标签,它是很多事故的根源。latest会随官方仓库更新而漂移,今天拉的和下周拉的可能不是同一个版本。凡是线上跑着的服务,镜像标签必须钉死到具体版本号,甚至再严谨一点,钉到镜像摘要Digest——就是镜像ID那一长串sha256值,比如mysql@sha256:xxxx,漏掉一个字节都不影响唯一指向。
查看本地镜像和删除镜像的常用命令:
# 列出本地镜像 docker images # 只看镜像ID,适合写脚本 docker images -q # 给镜像打标签,本质是给同一个镜像加一个引用 docker tag nginx:latest myapp/web:v1 # 删除镜像 docker rmi nginx:latest删除镜像这里有个连环坑:docker rmi删除的是“标签”,不是删除镜像内容,所以当你用同一个镜像打过好几个tag,删掉其中一个标签,本地镜像库里可能还剩着其他标签对应的同一份镜像层。如果多个镜像共用了父层,只有所有引用都被删干净,那些共用层才会真正被清除。我见过有人在服务器上docker rmi之后发现磁盘空间几乎没有释放,就是这个原因。
3.2 镜像导出:save与load,离线部署的黄金搭档
生产环境里很多服务器是不能直接访问公网镜像仓库的。这时候把镜像从能上网的机器导出,再拷进内网机器导入,就是绕不开的操作。
# 把镜像保存成tar包 docker save -o nginx.tar nginx:1.26 # 从tar包加载镜像 docker load -i nginx.tar我习惯在打包的时候打个压缩,毕竟上G的镜像当裸tar拷来拷去很浪费带宽:
docker save nginx:1.26 | gzip > nginx-1.26.tar.gz # 加载时直接解压 gunzip -c nginx-1.26.tar.gz | docker load这里有个最容易被坑到的地方:docker save产生的tar包,后续在另一台机器上load回来,镜像名和标签是跟着走的,但如果源机器上镜像名写得不规范、带了一堆临时tag,load出来的镜像也会是那副乱糟糟的样子。所以做离线包之前先docker tag把目标命好,再save。不然到达目标机器上,docker images看到一堆没有规律的名字,想run的时候还得猜。
3.3 不要随便commit镜像,构建镜像的正确姿势
docker commit可以把一个运行中的容器状态保存为新镜像。网上有些教程会让你在容器里手工装了一堆软件后commit一下,把这个容器变成“万能镜像”,我不建议大家在生产上这么干。
为什么?因为commit生成的新镜像只有一部分状态,你手工在容器里改动的过程没有沉淀成可重复执行的步骤文档,别人接手的时候完全不知道这个镜像里装了什么东西、改过什么文件。镜像变得像一个黑盒,出问题只能靠拆解调试,没法重放。
正确的构建姿势是Dockerfile加docker build:
docker build -t myapp:v1 .Dockerfile把“从哪个基础镜像开始”“装什么依赖”“复制哪些文件”“启动时跑什么命令”都明文写死在文件里,整个构建过程可重放、可审计、可code review。生产环境里Dockerfile就跟源码一样重要,丢了它等于丢了镜像的“灵魂”,只剩下一个快照壳子。
4. 容器核心命令:run起来只是开始
镜像是模板,容器才是真正干活的实例。docker run是所有人最早接触的命令,但大多数人只用了其中三四个参数。我会把run的每个常用参数当一组来拆,然后讲清楚容器起来之后怎么管理和交互。
4.1 docker run的黄金参数矩阵
一条典型的docker run,可能是长这个样子的:
docker run -d \ --name app-server \ -p 8080:80 \ -v /data/app:/app/data \ -e ENV=production \ --restart unless-stopped \ --network mynet \ --cpus 1.5 \ --memory 1g \ nginx:1.26一个一个拆开看。
-d是后台运行,不加的话容器会在前台占用你的终端,日志刷屏刷到天荒地老,Ctrl+C直接就停掉容器了。实际部署服务几乎都会加-d。
--name给容器起名字,好记也好用,后续日志、查看、删除都用这个名字,比容器ID好记太多了。
-p把宿主机的端口映射到容器的端口,格式是“宿主机端口:容器端口”。前面8080是宿主机对外提供的端口,后面80是容器里nginx监听的端口。生产上端口规划是个大活,有时候为了省事直接-p 80:80,但一旦多服务同机部署,端口冲突就冒出来了,所以规划时最好显式区分。
-v挂载数据卷,左边是宿主机目录,右边是容器目录。这样容器可以被删掉重建,但数据还留在宿主机上,这就是“容器无状态、数据有状态”的基本盘。
-e注入环境变量,MySQL的root密码、连接串、各种开关配置都靠这里传。很多人忽略的一点是,环境变量里放密码这类敏感信息,生产环境更推荐用docker secret或者外部密钥管理方案,直接写进命令会出现在命令行历史里,有一定风险。自己实验环境无所谓,生产环境还是小心为妙。
--restart设置的是容器退出后的策略,后面细讲,它几乎决定了你的服务能不能“半夜挂了第二天早上仍在”。--network指定网络,--cpus和--memory是资源限制,这两项在生产上不能省,不然一个失控容器能把整台机器吃干净。
还有两个容易混淆但实战中会交替用到的是-i和-t。单独一个-i表示交互式,保持标准输入打开,配合-t(伪终端)就是经典的-it组合,用来进入容器执行命令。
4.2 容器日常管理:ps、logs、exec、cp、port
容器启动之后,日常打交道最多的就是下面这几个命令。
docker ps用于查看运行中的容器,加-a看所有容器(包括已停止的)。它输出的信息里“STATUS”列很关键,Up表示在跑,Exited表示退出了,后面跟的“(0)”表示退出码,非0值得注意,比如137表示被SIGKILL杀掉了,一般是OOM或者手动kill,125/126/127这类则可能是启动参数或者执行入口有问题。
docker logs取容器日志:
# 实时追踪日志 docker logs -f web # 只看最后100行 docker logs --tail 100 web # 只看某时间点之后的 docker logs --since 2025-01-01T00:00:00 web排查问题第一步永远是看日志,别急着重启容器,先把现场保住。docker logs还有个大坑:如果你没有配置Docker日志驱动和轮转策略,一个高并发服务跑几天,日志文件能把磁盘写满。这个问题我会在后面的生产章节里详细说。
docker exec进入容器:
# 进入一个运行中的容器,打开交互式shell docker exec -it web /bin/bash # 不在容器内进shell,只执行一条命令 docker exec web ls /etc/nginx很多初次接触Docker的人会把docker exec和docker run搞混。docker run是“新建并启动一个容器”,docker exec是“在已经运行的容器里执行命令”。排障的时候,你想看容器内部的文件、环境变量、进程状态,exec就是你的入口。需要注意容器镜像里不一定有bash,Alpine这种精简镜像通常只有sh,所以失败时可以换成/bin/sh试试。
docker cp负责容器与宿主机之间拷贝文件:
# 从容器拷贝到宿主机 docker cp web:/etc/nginx/nginx.conf ./nginx.conf # 从宿主机拷贝到容器 docker cp ./myconfig.conf web:/etc/nginx/conf.d/我常用它来临时修正容器里的配置,尤其是手头没有Dockerfile又想快速验证某个配置时。但改完容器里的文件后,记住这个改动不会持久保留,除非你commit下来,容器一重建,改动就丢了。所以cp进容器只适合临时调试,正经做法还是把文件挂载进容器。
docker port用来查询端口映射:
docker port web这个命令输出信息虽然简单,但当你忘了当初-p映射了什么端口的时候,它是唯一能快速答上来的查法。docker inspect还可以拿到更多底层信息,比如容器IP、挂载列表、环境变量,配合--format参数可以只提取指定字段:
docker inspect -f '{{.NetworkSettings.IPAddress}}' web4.3 实战:一条命令跑通MySQL 8.0和Redis主从
结合项目中遇到的问题,我拿两个最常见的中间件实例来讲清楚实战闭环。
跑MySQL 8.0的典型命令:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Root@123456 \ -e MYSQL_DATABASE=appdb \ -e TZ=Asia/Shanghai \ -v /data/mysql8:/var/lib/mysql \ --restart unless-stopped \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci最后跟的那两段不是Docker参数,而是传给mysql服务的启动参数,Docker会把它拼到容器入口命令后面,这样比进容器改my.cnf要优雅得多。数据目录-v挂到宿主机,容器随便删,数据安然无恙。初始化数据库的SQL脚本也可以挂载进/docker-entrypoint-initdb.d目录,首次容器启动时会自动执行,注意是只在数据目录为空的情况下执行。
Redis主从稍微绕一点,但道理一样。先起主节点:
docker network create redis-net docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ -v /data/redis-master:/data \ redis:7 redis-server --appendonly yes再起从节点,从节点上的replicaof参数指向主节点容器名,因为同一个Docker网络下,容器名会被DNS自动解析成对应IP,所以不需要查IP,直接写容器名就可以:
docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ -v /data/redis-slave:/data \ redis:7 redis-server --replicaof redis-master 6379这里最能体现自定义网络的好处:容器间通信不用管IP地址变化,容器重建之后名字不变、网络不变,DNS解析自动跟随新IP,比维护一堆静态IP舒服太多。官方还把shard-group-based redis cluster加进来了,但最通用的主从方案还是这套逻辑。
5. 网络与存储:生产容器稳定运行的两块基石
容器跑起来了,数据能不能持久保存、容器之间能不能互相通信,这两个问题会一直陪着你从开发到上线。很多人觉得docker run能跑起来就是成功,其实网络和存储才决定着你这套架构能不能经得住运维。
5.1 bridge、host、none:三种网络模式的适用场景
Docker默认的网络是bridge模式,容器通过一个虚拟网桥跟宿主机通信,对外通过端口映射暴露服务。这种模式好处是容器隔离性好,互不干扰,缺点是每次启动容器,内部IP都可能变,容器名之间的DNS解析只在自定义网络里才生效。
另一个常用模式是host模式,容器直接使用宿主机的网络栈,不需要做端口映射,容器里监听的端口就等同于宿主机的端口。好处是性能好、没有NAT一层,坏处是失去了网络隔离,端口冲突要靠人工管理。我一般只在对网络性能极其敏感的场景才用它,比如某些压测工具、日志采集组件。
none模式最冷门,容器完全独立于网络,只有回环接口,适合本地跑一些不需要联网的批处理任务。还有macvlan模式,可以让容器直接获得局域网IP,看起来就像一台独立的物理机器,适合做IoT网关、NAS这类场景。
默认的bridge网络有个很大的限制:容器用IP通信时,每次重建容器IP就变,脚本里写死IP就完蛋了。所以凡是需要互相调用的服务,强烈建议建一个自定义网络:
docker network create myapp-net创建之后,同一网络里的容器互相ping或者访问服务时,直接写容器名就行:
# 容器A访问B的80端口 docker exec app-a curl http://app-b:80这个特性在生产里帮了大忙。我之前排查过一个微服务调用偶尔超时的问题,最后发现是某个服务容器重建后IP变化,另一个服务里硬编码了旧IP,白查了半天。换成自定义网络加容器名之后,再没出过这个问题。
5.2 端口映射与容器互通,“docker网络不通”排查实录
很多人见“docker网络不通”这几个字当场懵掉,其实先想清楚“哪到哪不通”再说。不通的可能有很多层:是宿主机访问容器里服务不通,还是容器访问外网不通,还是容器之间不通,这三类问题的排查入口完全不同。
宿主机访问容器服务不通,优先查端口映射:
docker port web # 0.0.0.0:8080 -> 80/tcp然后看防火墙。Linux服务器上常见的事故就是docker -p 8080:80跑起来了,防火墙挡在宿主机外,外部机器访问8080超时,但curl localhost:8080是通的。这种情况要放行宿主机端口而不是容器端口,因为外部流量到这里直接撞上宿主机的防火墙。云服务器还要记得看安全组规则,很多新手部署后访问不了,问题都出在安全策略上。
容器之间不通,先检查它们是不是在同一个网络:
docker inspect web | grep -i network如果在同一个自定义网络里却还是不通,那多半是容器内的服务监听地址或者端口不对。比如有的服务默认只监听127.0.0.1,容器IP是172.x.x.x,自然连不上,这种就要去改服务配置。
容器访问外网不通,最常见的原因是宿主机的IP转发没有打开。Linux下临时开启:
sysctl net.ipv4.ip_forward=1 # 永久开启就写进 /etc/sysctl.conf如果宿主机有环境变量配置了代理,部分容器需要走代理访问外网,也要注意代理设置是否生效。不过这些属于企业网络环境特例,我在自己团队里通常建议尽量让容器走直连或者通过反向代理统一出网,省得每个容器都要配一套代理参数。
5.3 数据持久化:volume和bind mount怎么选
容器是临时的,数据必须放到宿主机上。Docker提供两种主要持久化方式:命名卷(docker volume)和绑定挂载(bind mount)。
命名卷的创建方式:
docker volume create mydata docker run -d -v mydata:/app/data myapp也可以不预先create,docker run时直接-v我随便起个名字:/容器目录,Docker会自动创建。命名卷的优点是不用手工管理宿主机目录路径,Docker自己把数据存放在它的工作目录下,适合给数据库这类“只知道挂到容器路径,不关心宿主机路径”的场景。
绑定挂载则是把你指定的宿主机目录直接挂进去:
docker run -d -v /home/user/app:/app myapp绑定挂载的优势是你直接能看着宿主机文件,修改也很直观,改完容器内立刻同步生效,非常适合开发调试和配置文件管理。我之前调试nginx配置就喜欢直接-v把nginx.conf挂进去,改完宿主机文件再在容器里reload。
数据卷也会踩坑。最常见的一个是:你在容器里写了数据,挂载目录权限不对,容器内进程无法写入宿主机目录,直接报Permission denied。这个和宿主机目录的属主有关,运行容器的用户是root还好,如果容器里是普通用户(比如很多官方镜像默认uid 1000),那宿主机目录可能需要chown一下。另一个坑是:把宿主机目录bind mount到了容器里已有数据的路径上,容器的原目录会被挂载层“遮住”,目录里原有的东西在容器里就看不到了。挂载前先看清容器镜像的WORKDIR和VOLUME声明。
6. 生产环境高频命令:重启策略、日志清理与资源管控
开发环境里容器挂了就重启一下,没什么心理负担。但生产环境不行,半夜两点容器挂了,没人守着的话服务就一直瘫着。好在这里有一些办法让容器“自己站起来”,还有一些资源管控手段防止一颗老鼠屎坏了一锅汤。
6.1 容器崩溃自愈:restart策略与docker update
docker run时加了一个--restart参数,这个参数正是自愈能力的关键。它有几种取值:
- no:默认值,容器退出后不自动重启。
- on-failure:3:容器以非0退出码退出时自动重启,最多重试3次。
- always:只要容器退出就自动重启,包括守护进程重启之后。
- unless-stopped:除非手动docker stop,否则总会自动重启。
生产上我最喜欢用的是unless-stopped。它的好处非常实际:如果你手滑docker stop了一个容器,它会尊重你的意愿保持停止,而always策略在容器被stop之后,Docker服务一旦重启,它又会自己拉起来,反而让你觉得“明明停了怎么又跑了”。
如果容器已经创建了但当初没加--restart,不需要删除重建,一个命令就能改:
docker update --restart unless-stopped app-serverdocker update还可以改资源限制。
6.2 日志与资源限制:避免磁盘写爆和OOM
开发环境瓶颈不常出现,生产环境跑三个多月,最典型的伤害来自两个地方:日志写满磁盘、内存失控导致OOM。
默认情况下Docker会把容器的stdout和stderr收集成json-file日志文件,放在宿主机的/var/lib/docker/containers/目录下。没有任何旧日志清理机制,跑上几周,日志文件很容易膨胀到几个G甚至几十个G。解决办法是在daemon.json里配置日志轮转:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }改完重启Docker服务对存量容器不生效,存量容器需要重建。所以我一般在一台新机器装完Docker之后先改好daemon.json,再开始起容器。已有的容器如果不想重建,可以临时用docker logs --tail手动把日志截断,或者直接删掉对应的日志文件再通知容器重新打开fd,比较麻烦,我更建议容器定期重建时把日志策略一起带上。
资源限制上,docker run的几个参数就够了:
docker run -d \ --cpus 1.5 \ --memory 2g \ --memory-swap 2g \ --pids-limit 512 \ myapp--cpus限制CPU核数,--memory限制内存上限,如果只设--memory不设--memory-swap,容器可以使用两倍于--memory的swap,有时候并不符合预期,我通常会显式把两者设为一致,让容器不可用swap。--pids-limit是限制容器内最大进程数,防住某些脚本疯狂fork把宿主机进程表打满。
容器OOM之后会怎样?容器会直接被杀掉,docker ps看到的状态是Exited (137)。这种OOM不一定每次都在docker logs里留下明显的错误记录,所以要养成看退出码的习惯。
6.3 批量清理与Compose快速编排
一台机器上容器跑多了,镜像、悬空镜像、无用卷、停止的容器会堆积得很厉害。手工一个个删又怕删错,我一般用几条prune命令:
# 清理停止的容器 docker container prune # 清理悬空镜像(没有标签也没有容器引用的镜像) docker image prune # 清理所有未使用的镜像 docker image prune -a # 清理所有未使用的卷(高危,务必确认) docker volume prune # 一键清理所有未使用资源 docker system prune -a --volumesprune命令带有“打扫”的性质,建议在低峰期执行。docker system prune -a --volumes会把所有没有被运行中容器使用的镜像和卷都清掉,如果构建缓存里存着老版本镜像想做回滚用,也会一并清理,所以这套重拳要慎用。
至于多服务编排,绕不开Docker Compose。它不需要额外命令体系,只要写一个compose.yml,然后用docker compose命令整体管理:
docker compose up -d docker compose down docker compose ps docker compose logs -fCompose的好处是服务间的依赖关系、网络、存储卷、资源限制都写在一个文件里,整个环境的启停由一条命令完成。比如上面提到的MySQL和Redis主从,如果写成Compose文件,新环境直接docker compose up -d全部按配置跑起来,不比一条条docker run香得多。
7. Docker命令排障速查表:那些让人血压飙升的场景
多年代运维下来,我发现绝大多数Docker问题都有清晰的规律可循,排查步骤也是套路化的。下面我把本地项目、Linux服务器和Windows环境里见过的高频报错整理成一个速查表,按“症状-可能原因-排查命令-解决方案”的方式列出来,适配日常运维和新人学习。
| 症状 | 可能原因 | 排查命令 | 解决思路 |
|---|---|---|---|
| Docker命令报Cannot connect to the Docker daemon | Docker服务未启动/守护进程崩溃 | systemctl status docker | systemctl start docker,看journalctl -u docker |
| Windows上Docker Desktop启动提示virtualization support not detected | BIOS虚拟化没开/WSL2未启用 | 系统信息看虚拟化状态 | BIOS打开VT-x/AMD-V,启用WSL2并安装内核 |
| Windows上提示failed to connect to the docker api at npipe | Docker Desktop引擎未就绪 | docker context ls | 重启Docker Desktop/切换default上下文/重置WSL |
| docker run直接退出,docker logs没输出 | 启动命令/入口脚本执行失败 | docker inspect查看Entrypoint | 手动docker exec进入容器调试线索,检查挂载目录权限 |
| 宿主机能访问容器,外部机器访问不了 | 防火墙/安全组拦截 | telnet 宿主机IP 端口或nc -vz | 放行宿主机对应端口,检查云安全组规则 |
| 容器之间无法用容器名互相访问 | 不在同一自定义网络 | docker network inspect <net名> | 创建自定义网络,让所有容器加入同一个网络 |
| docker pull一直超时 | 网络到镜像仓库不通 | docker info看registry配置 | 配置可用的镜像加速地址,或通过代理下载再用save/load导入 |
| 删除镜像时报image is being used by container | 镜像被容器引用 | docker ps -a | 先删容器再删镜像,或强制删除docker rmi -f |
| 容器日志文件巨大,磁盘满 | 日志轮转未配置 | du -sh /var/lib/docker/containers | 配置log-opts max-size并重建容器 |
| 容器被杀,退出码137 | 内存超限被OOM Kill | dmesg | grep -i oom查看内核日志 | 加大--memory,或优化应用内存占用 |
| 宿主机目录挂载进容器后权限报错 | 目录属主和容器进程UID不一致 | ls -n宿主机目录 | chown调整宿主机目录属主 |
| docker save的tar包load回来没有原来的名字 | save前tag不规范 | docker images | save前显式docker tag好最终名字 |
| Pull拉取的镜像没有预期的版本 | tag写错/仓库registry写错 | docker manifest inspect <镜像:tag> | 检查镜像名/标签/私有仓库前缀 |
7.1 两个一定不能省的基础动作:docker inspect和docker events
排障时除了看日志,我给新人的建议是多用docker inspect,它把容器、镜像、网络、存储卷的配置以JSON形式完整dump出来,是定位问题的放大镜。比如想确认容器内部环境变量是否生效,docker inspect -f '{{.Config.Env}}'容器名一条命令搞定。想确认挂载的是不是宿主机指定目录,看Mounts字段,挂载类型、源路径、目标路径都清清楚楚。容器异常退出时看State.Restarting和State.ExitCode,重启计数和退出码一目了然。
还有一个常被忽略的命令docker events,它实时输出Docker守护进程收到的事件流。写脚本做自动化、观察容器重启规律时特别好用,比如:
docker events --filter container=web --filter event=die容器一挂,这条命令当场就能给你推送一个die事件,再配合一些监控工具,能让运维系统立刻感知故障。
7.2 容器内调试三板斧:logs、exec、cp
遇到容器正常但应用报错,我习惯按顺序做三件事。
先看日志,docker logs往往直接给出错误堆栈或业务异常信息。如果项目日志是写文件而不是stdout,那就得用第二板斧,docker exec进入容器内部看业务日志文件,命令前面加--user指定用户能避免误碰容器里的权限限制。如果容器里连工具都没有(精简镜像没有vi、没有curl、没有ping),又不想老去装依赖,就用第三板斧:docker cp把宿主机上的静态排查工具或调试脚本拷进容器执行。比如宿主机上编译好的curl、数据库客户端,拷进去就能用,比在容器里边装依赖边调试快得多。
这三个工具配合起来,基本能覆盖容器内“服务没起来、起来了报错、能跑但行为异常”三类问题。
7.3 我的排障习惯:三个命令先摸清现状
接手的机器出问题时,我从来不急着docker rm -f或者docker compose down,脑子会因为慌乱而短路。首先执行三个命令摸清现状:
docker ps -a docker images df -h第一句看容器状态,哪些在跑、哪些退出、退出码是多少;第二句看本地镜像体积,确认磁盘占用大头在哪里;第三句确认磁盘有没有被写满。磁盘满是最容易被忽略但导致大量“Connection refused/无法创建文件/容器写日志失败”现象的根因,一上来先看它,能省掉后面一大半无效操作。
这三个命令执行完再决定删哪个容器、重建哪个镜像、清理哪个卷,基本不会误伤。
最后再分享一个我自己踩过的坑
写这种全场景命令梳理的文章,本质上是在帮读者形成“命令围绕对象转”的思维方式。但光记住命令还不够,更重要的是有一套自己的排障动作。我现在处理任何Docker问题,固定流程一定是“先看现状,再看日志,最后动刀”。现状看的是docker ps -a、docker images、df -h,日志看的是docker logs和docker inspect,动刀之前再罗列受影响容器清单。这套流程听着笨,但它能拦住绝大多数“手比脑子快”引发的次生事故。
另外,建议新入手的同事把常用的命令整理成自己的速查卡,不要依赖记忆或者幸运。我在团队里会要求提交部署文档时必须写明白镜像版本和启动参数,这不是形式主义,而是因为Docker的命令执行成本很低,但产生的影响范围很广,一条-v写错,可能就把宿主机根目录挂进容器了。有个小检查办法:凡是docker run命令里涉及-v的参数,先从右往左看一遍,确认右边的容器内路径是预期的,再确认左边的宿主机路径存在、可控、不是/、/etc这种系统级目录。这个习惯帮我挡掉过好几次足以毁掉环境的误操作。