你说你是Docker新手,拉个镜像慢到怀疑人生,docker run 参数抄了又抄还是记不住,好不容易把容器跑起来,第二天发现数据全没了,瞬间想砸电脑。别急,这些问题我全踩过,今天这篇就把Docker容器最核心的操作逻辑一次讲透,没有废话,全是实操。
这个标题看着可能有点密集——8个核心操作、6大高频坑、3个核心逻辑,但核心就一件事:让你从"只会跟着教程敲命令"变成"遇到问题能自己判断怎么处理"。我接触Docker这么多年,带过不少新人,发现最容易出问题的往往不是命令本身,而是对容器本质的理解偏差——不知道它是个什么东西,出问题的时候自然不知道往哪个方向排查。所以我会先讲透底层逻辑,再带你过一遍必须会的操作,最后把高频坑逐个拆开,保证你看完能少走一大半弯路。
1. 三个核心逻辑,先搞懂容器到底是个啥
1.1 容器不是虚拟机,它就是一个进程
很多新手第一次接触Docker,脑子里默认把它当成了轻量级虚拟机。这个误解非常要命,因为一旦用"虚拟机"的思维去理解容器,后面遇到的所有问题都会觉得莫名其妙。
举一个最简单的例子:你用docker run -d nginx启动了一个容器,然后执行docker ps看不到它。如果你把容器理解成虚拟机,你的第一反应是"机器没开起来";但如果你理解成进程,你的第一反应是"进程挂了",然后再去docker logs查看问题出在哪。这个思路的转变,决定了你能不能快速定位问题。
实际上的原理是这样的:Docker容器共享宿主机内核,通过Linux内核的namespace做资源隔离,通过cgroup做资源限制。容器里的进程在宿主机上就是一个个普通进程,只是被"圈"在了一个隔离环境里罢了。
注意:这个认知直接决定你的排查思路,容器应用挂了第一件事就是看日志,不是去"重启系统"。
1.2 镜像是一层一层的,文件系统变更靠"写时复制"
Docker镜像为什么能复用?为什么拉一个几百MB的镜像有时只需要几十秒?核心在于镜像的分层存储机制。
Docker镜像是由多个只读层堆叠而成的,每一层对应Dockerfile里的一条指令。你拉取一个镜像时,Docker会检查本地已有的层,只下载缺失的层,这就是为什么如果你的本机已经有其他镜像共享了一些基础层,拉新镜像会快很多。
当一个容器运行后,Docker会在镜像层之上挂载一个可写层。容器中对文件系统的任何修改,包括新增、删除、修改文件,都发生在这个可写层。用"写时复制"的机制,即使多个容器共享同一个镜像或底层只读层,每个容器内部的修改也是相互隔离的。
这就是为什么容器删除后数据会丢——因为数据写在可写层里,容器删了,可写层也就没了。理解了这个原理,你就会明白为什么官方一直强调:数据必须挂载到数据卷或宿主机目录里。
1.3 容器是"一次性"的,可写层不是保险箱
顺着上面的逻辑再往下走:容器的生命周期应该是短暂的、可替换的。你要做的是随时可以删掉一个容器,再用同一个镜像重新创建一个,而业务数据丝毫不受影响。
我之前见过一个小伙子在容器里部署了一个MySQL,用了一段时间之后数据越来越多,某天服务器重启后容器没自动启动,他一着急,直接去容器内部检查数据,结果发现数据还在,就没放在心上。过了几天他手动清理镜像的时候,顺手把那个容器给删了,等他发现数据库连不上的时候,数据已经彻底没了。
这个场景太典型了。你得把容器理解成一次性的沙滩城堡,而数据卷是你放在岸边的保险柜。城堡被海浪冲走了没关系,保险柜里的东西不能丢。
2. 八个核心操作,从零开始跑通容器
2.1 拉取镜像:先搞明白你下载的是啥
镜像是什么?你可以理解为应用运行所需的"全部材料打包"。nginx镜像里包含了nginx程序、配置文件、依赖库等;MySQL镜像里包含了MySQL程序、初始化脚本等。
拉取命令很简单:
docker pull nginx默认会从Docker Hub拉取最新版本(latest),也可以指定版本和仓库地址:
docker pull mysql:8.0 docker pull registry.cn-hangzhou.aliyuncs.com/library/mysql:8.0这里多提一句,国内拉取Docker Hub镜像经常很慢,甚至超时失败。解决方案有两种:一是配置镜像加速器(在Docker Desktop的设置里可以配置registry-mirrors),二是在拉取命令里直接指定国内镜像源地址。配置好之后实测下载速度能提升好几倍。
2.2 运行容器:docker run 是全部操作的入口
docker run是最常用的命令,但新手往往被它的参数搞晕。我拆解一下最常用的组合:
docker run -d -p 8080:80 --name web-server nginx-d:后台运行,不加这个参数,你的终端就会被nginx的前台日志刷屏,Ctrl+C一按容器就停了。-p 8080:80:端口映射,格式是"宿主机端口:容器内端口"。这里把宿主机的8080端口映射到容器的80端口,这样你访问http://localhost:8080就能访问到容器内部的nginx。--name:给容器起个名字,不加的话Docker会随机生成一个类似"angry_gates"的名字。
启动MySQL时一般还要加环境变量和挂载数据卷,这就是比较完整的形态:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e MYSQL_DATABASE=mydb \ -v mysql-data:/var/lib/mysql \ mysql:8.0-e是设置环境变量,MySQL镜像需要通过MYSQL_ROOT_PASSWORD指定root密码,不然容器会直接启动失败。-v是挂载数据卷,我们后面专门说。新手最怕看到又长又乱的命令,我的建议是把常用参数写成一个小抄贴在手边,用几次就熟练了。
2.3 查看容器状态:docker ps 的两种用法
刚跑完容器,第一件事就是确认它有没有正常运行:
docker ps这个命令只显示正在运行的容器。如果容器启动后因为各种原因退出了,docker ps列表里看不到它,你可能会以为容器不存在。这时候要用:
docker ps -a-a参数显示所有容器(包括已退出的)。每个容器会有一个唯一的CONTAINER ID,后续很多操作都可以直接使用这个ID的前几位来定位容器,比如docker logs abc123。
2.4 查看日志:排查问题的第一把钥匙
容器跑起来了但访问不了,或者没几秒就退出了,第一个排查手段永远是看日志:
docker logs web-server如果日志太多,可以只看最后100行并且持续跟踪:
docker logs --tail 100 -f web-server--tail是只看末尾多少行,-f是持续输出新产生的日志,类似tail -f的效果。排查问题的时候这招最实用。很多新手容器一启动就退出,不查日志就盲目删了重跑,这完全是碰运气。八成的情况,日志里已经写清楚了失败原因——比如端口被占用、环境变量缺失、配置文件写错等等。
2.5 进入容器:在容器内部执行命令
需要进入容器内部调试的时候,用这个:
docker exec -it web-server bash-it是两个参数:-i保持标准输入打开,-t分配一个伪终端。不这样写,交互式操作没法玩。如果容器里没有bash(比如一些精简版alpine镜像),可以试试sh:
docker exec -it web-server sh如果你只是想执行单条命令,不用进入容器:
docker exec web-server ls /usr/share/nginx/html这个在排查问题时非常常用,不用反复进出容器。
2.6 停止、启动、删除容器:生命周期管理
容器用完了想停下来,或者修改了配置想重启,常用命令如下:
docker stop web-server # 停止容器,会发出SIGTERM信号给它留出优雅退出的时间 docker start web-server # 启动一个已停止的容器 docker restart web-server # 重启容器 docker rm web-server # 删除容器(需要先停止)如果要删除所有已停止的容器,记住这个:
docker container prune它会一次性清除所有处于停止状态的容器,释放命名和存储资源。如果明确要强制删除正在运行的容器,加-f参数。
2.7 端口映射与数据卷:容器外部访问入口和数据存放点
端口映射在2.2里已经提过,这里强调一个点:端口映射是"单向"的,你可以在启动容器时通过-p参数指定映射,但容器运行中不能追加新的映射。如果想改端口,只能把容器删了重新run。
数据卷是Docker里最容易被忽视、后果最严重的一环。数据卷就是挂载到容器里的一个独立存储空间,它可以在宿主机上,也可以由Docker管理,目的是让数据独立于容器生命周期。
docker run -d --name mysql8 \ -v mysql-data:/var/lib/mysql \ mysql:8.0上面这个mysql-data由Docker管理,数据存在宿主机的/var/lib/docker/volumes/目录下。另一种方式是直接挂载宿主机目录:
-v /opt/mysql/data:/var/lib/mysql这种方式更直观,你可以直接在宿主机上看到和备份数据。具体用哪种,看你的运维习惯,我建议生产环境直接用宿主机目录的方式,方便备份和迁移。
2.8 清理镜像和缓存:给磁盘瘦身
长期使用Docker,磁盘空间会被镜像、容器、数据卷、构建缓存占满。我清理的思路是,先删容器、再删镜像,最后清没用的数据卷:
docker rm $(docker ps -aq) # 删除所有容器 docker rmi <镜像ID或名称> # 删除指定镜像 docker image prune -a # 删除所有没有被容器使用的镜像 docker volume prune # 删除未被任何容器挂载的数据卷 docker system df # 查看磁盘占用情况docker system df会列出镜像、容器、数据卷、构建缓存各自占了多少空间,非常直观。
3. 六大高频坑,每一个都是新手踩出来的
3.1 镜像拉取失败或超时
这个问题在国内几乎每个人都碰到过。官方镜像源在国外,网络连接不稳定或速度极慢,导致拉取超时、中断,甚至出现manifest unknown之类的错误。
解决办法第一选择是配置镜像加速器。打开 Docker Desktop 的 Settings → Docker Engine,在配置里加上registry-mirrors地址,然后重启Docker。Linux环境下修改(或新建)/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://hub-mirror.c.163.com" ] }然后重启Docker服务:
sudo systemctl daemon-reload sudo systemctl restart docker配置好之后拉镜像速度是肉眼可见的提升。如果还是慢,就直接从国内镜像仓库拉取,比如阿里云容器镜像服务,docker pull registry.cn-hangzhou.aliyuncs.com/...。
3.2 容器一启动就退出(秒退)
这是新手最常遇到的问题,没有之一。docker ps -a看到容器显示EXITED,第一反应往往是"再跑一次",结果依然秒退。
绝大多数的原因是:镜像里定义的执行命令运行失败。常见场景有:
- 指定了错误的环境变量(比如MySQL的密码没设)
- 依赖的端口已被占用
- 配置文件写错,应用启动时直接报错退出
终极解法就三个字:看日志。
docker logs <容器ID或名称>日志会明确告诉你失败的原因。比如MySQL缺少MYSQL_ROOT_PASSWORD,yesql配置的端口被占用了等等。看完日志再动手,效果立竿见影。另外,有些容器是"前后台不分"的,如果镜像里的进程本身是后台守护进程(比如某些脚本里写了daemon off),容器会认为主进程已经退出了,所以也秒退。这个问题常见于直接用系统包管理器装nginx之类的软件然后手动启动的镜像。
3.3 端口被占用了怎么办
启动容器报错Bind for 0.0.0.0:8080 failed: port is already allocated,这是端口冲突。可能的情况是宿主机上有其他进程占了这个端口,也可能是另一个容器已经映射了这个端口。
排查方法:
lsof -i :8080 # 或者 netstat -tunlp | grep 8080看看是谁占用了8080端口,然后改成其他端口重新启动容器。注意,改端口可不是改条命令那么简单,你得先删掉原来的容器:
docker rm -f web-server docker run -d -p 8081:80 --name web-server nginx3.4 容器删除后数据全没了
这个问题在前面已经反复强调过了,根因就是没挂载数据卷。运行MySQL、Redis、MongoDB这类有状态服务时,数据目录一定要挂载到宿主机:
- MySQL数据目录:
/var/lib/mysql - Redis持久化文件:
/data - MongoDB数据目录:
/data/db - PostgreSQL数据目录:
/var/lib/postgresql/data
如果你用命名数据卷,即使容器删了,只要卷还在,数据就还在。你完全可以放心重建容器,重新挂载同一个卷,数据立马回来。我用一句话总结:操作数据库容器之前,先检查启动命令里有没有-v,没有就先别干。
3.5 容器时区不是北京时间
容器默认时区通常是UTC,所以日志时间比北京时间慢8小时。排查日志的时候,时间对不上很误导人。
最简单的解决方式是启动时挂载宿主机的时区配置:
docker run -d \ --name web-server \ -e TZ=Asia/Shanghai \ nginx很多官方镜像支持TZ环境变量,启动时指定一下就行。如果镜像不支持或者你已经启动了,就进容器改:
docker exec -it web-server bash ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime改完重启容器生效。不过每次重建容器都要重新设置,所以我更推荐在docker run时直接加-e TZ=Asia/Shanghai,或者挂载时区文件:
-v /etc/timezone:/etc/timezone:ro3.6 容器里用root跑服务,安全隐患很大
默认情况下,容器里的进程是以root身份运行的,这非常危险。一旦容器被攻破,攻击者获得的就是宿主机上的root权限。即使有namespace隔离,风险依然很大。
解决办法是启动容器时指定运行用户:
docker run -d --user 1000:1000 nginx或者用-u参数指定用户名。更规范的做法是:如果你自己写Dockerfile,在构建镜像时就创建一个普通用户,并用USER指令切换过去。
需要注意一个前提,容器里的用户必须对挂载的目录有写权限。如果数据卷目录是root创建的,普通用户进不去,应用就会启动失败,这个在调试时最容易迷惑人。
4. 进阶但不难的补充:网络、Compose与日常排障
4.1 容器网络:别默认把端口暴露到公网
-p 8080:80会让端口直接暴露在宿主机的所有网卡上,包括公网。如果你只是本机调试,不想让外部访问,可以绑定回环地址:
-p 127.0.0.1:8080:80多个容器之间通信,推荐创建自定义网络:
docker network create my-net docker run -d --network my-net --name mysql8 mysql:8.0 docker run -d --network my-net --name web-server nginx使用自定义网络后,容器之间可以用容器名直接访问,比如web容器里连接MySQL,直接写mysql8:3306就行,IP地址不用记。
4.2 docker compose:多个容器一条命令跑起来
如果项目依赖了MySQL、Redis、应用本身等多个容器,再用docker run一个个启动就太痛苦了。docker compose可以把所有服务定义在docker-compose.yml文件里,一条命令全部拉起:
version: '3' services: mysql: image: mysql:8.0 container_name: mysql8 environment: MYSQL_ROOT_PASSWORD: rootpassword ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql app: build: . container_name: web-app depends_on: - mysql ports: - "8080:80" volumes: mysql-data:然后运行:
docker compose up -d这个文件把镜像、端口、数据卷、环境变量都声明清楚了,下次服务器迁移或者换机器部署,拷贝文件直接跑,比手动翻历史命令找docker run靠谱一万倍。我强烈建议,凡是超过一个容器的场景,直接用compose。
4.3 常见排查命令速查
把最有用的排查命令整理成一张表,建议收藏:
| 场景 | 命令 | 说明 |
|---|---|---|
| 查看运行中容器 | docker ps | 列出所有运行中的容器 |
| 查看所有容器 | docker ps -a | 包含已退出容器 |
| 查看容器日志 | docker logs -f <容器> | 跟踪日志输出 |
| 进入容器 | docker exec -it <容器> bash | 容器内执行交互命令 |
| 查看镜像 | docker images | 列出本地镜像 |
| 查看端口占用 | lsof -i :8080 | 检查端口占用情况 |
| 查看资源占用 | docker stats | 实时看容器CPU、内存 |
| 查看容器信息 | docker inspect <容器> | 包括IP、挂载卷、环境变量等 |
docker inspect是排查问题的神器,比如确认容器实际的挂载路径对不对、IP是多少、环境变量有没有生效,全在这里面。
5. 实操总结与个人体会
如果说这么多内容里只能留下一个观念给你,我最想强调的是:把容器当成一只"一次性手套",而不是一台"小电脑"。它随手可取、随时可换,你需要保护的是"手套"之外的数据和配置。任何需要长期保留的东西,都必须挂载到宿主机目录或数据卷里;任何需要改的配置,都应该写进docker run参数或者compose文件里,而不是手动进入容器"改完就这样用"。
我自己早期因为没挂载数据卷把MySQL数据弄丢过一次,从那以后定了个死规矩:跑有状态服务容器,先想清楚数据落在哪里;跑测试环境容器,一律标注清理日期,过时就容器和镜像一起清掉。
如果你看完想动手试试,我建议的路线是:先拉一个nginx,跑起来,修改默认页面,然后把容器删了再重建一次,体会一下"增减容器对配置和数据无感"的爽快。接下来可以试着跑一个Redis,设置密码、挂载数据卷,再用compose把Redis和一个小应用一起拉起来。走完这三四个实验,你基本就具备日常使用Docker的底气了。