1. 镜像管理:一切的起点
不管你是刚接触Docker的新手,还是已经在开发环境里折腾了一段时间的“半熟手”,我相信大部分人对Docker的第一印象都来自镜像。很多教程上来就让你跑一个docker run hello-world,但其实日常开发里打交道最多的,还是镜像的拉取、查看、删除和导出这类基础操作。我见过不少人把docker images和docker ps搞混,也见过有人docker rmi和docker rm分不清,结果把镜像删了一堆,容器还在跑,留下一堆<none>的悬空镜像,越搞越乱。
1.1 拉取与推送:你必须先搞懂仓库、标签和镜像的关系
先说最基本的docker pull。它的完整语法是docker pull [选项] 名字[:标签],这里的“标签”就是tag,默认是latest。为什么我一直强调要把标签写完整?因为latest不是一种保证,它只是一个名字。很多软件的作者会把最新的稳定版打上latest,但也有的项目会把最新的预发布版本也标记成latest,你拉下来跑起来才发现行为不对。所以生产环境或者要复现问题的时候,务必指定具体的版本号,比如docker pull mysql:8.0.32。
镜像名有时候还会带域名前缀,比如docker pull registry.cn-hangzhou.aliyuncs.com/library/nginx:1.24,这是从特定镜像仓库服务拉取。默认情况下Docker会去Docker Hub拉,但国内网络环境下拉Docker Hub经常慢到让人怀疑人生,这时候配置镜像加速器就成了刚需。具体操作是编辑Docker守护进程的daemon.json文件,加上registry-mirrors字段,然后重启Docker服务。这里我要提醒一句:加速器不是万能的,有时候拉取大镜像依然会超时,好的习惯是尽量拉精简版镜像,比如alpine系列,体积只有几十兆,部署和调试都快得多。
和pull对应的是docker push,这个命令一般配合docker tag使用。为什么要tag?因为推送的镜像名必须包含仓库地址,比如你要推到自己公司的私有仓库,就得先把本地镜像重新打一个带仓库地址的标签,再执行docker push。我见过很多人卡在这一步,觉得docker tag和docker cp一样是“复制”操作,其实tag只是给镜像增加了一个引用名称,并不会复制一份实体数据。同一份镜像可以有多个标签,删除其中一个标签不代表镜像被删了,只有当最后一个标签被删掉,镜像才真正变成待回收状态。
1.2 镜像的清理与元数据:rmi、prune、history和inspect
镜像删不掉,是新手最容易遇到的困境之一。docker rmi 镜像ID报错image is being used by stopped container,很多人就懵了。这背后其实是一个很合理的保护机制:容器创建时可能基于某个镜像写入了数据,如果你把镜像删了,容器的“底层底座”就没了,后续排查问题会很麻烦。解决方案也很直接:要么先删容器再删镜像,要么用docker rmi -f强制删除。但我并不推荐动不动就加-f,尤其是在有数据卷的情况下,强制删除可能会让你丢失线索,最好是先把关联的容器清理掉。
随着镜像越拉越多,本地磁盘会被大量无用的中间层镜像占满。这时候docker image prune是真正的救星。docker image prune -a会把所有没有被运行中容器使用的镜像都清理掉,注意是“所有”,不只是悬空镜像,所以执行之前先看一眼列表,或者用docker images确认一下哪些镜像你还留着有用。我还习惯搭配docker system df查看当前磁盘占用分布,这个命令能很直观地告诉你镜像、容器、数据卷、构建缓存分别吃了多少空间。
docker history 镜像名是我调镜像问题时很喜欢用的命令。它能展示镜像的每一层是怎么构建出来的,每层的大小是多少。有时候你发现一个镜像异常大,用history一查,就能看到是不是某条RUN命令把源码、编译中间产物都留在了镜像里。配合docker inspect可以看镜像的配置信息,比如环境变量、暴露的端口、入口点。我建议不要只是复制inspect的输出,而是养成用docker inspect -f '{{.Config.Env}}'这种格式化输出的习惯,只提取你关心的字段,看着方便,写脚本也顺手。
这里把镜像相关命令整理成一张速查表,方便你贴在终端旁边:
| 命令 | 作用 | 常用场景 |
|---|---|---|
docker pull nginx:1.24 | 拉取指定标签镜像 | 部署前获取镜像 |
docker images | 列出本地所有镜像 | 检查镜像是否已存在 |
docker tag 旧名 新名 | 给镜像增加标签 | 推送到私有仓库前打标 |
docker push 仓库地址/镜像名:标签 | 推送镜像到仓库 | 发布自建镜像 |
docker rmi 镜像名 | 删除镜像 | 清理无用镜像 |
docker image prune -a | 清理未使用镜像 | 磁盘空间告急时 |
docker history 镜像名 | 查看镜像构建历史 | 排查镜像体积过大 |
docker inspect -f '{{.Config.Env}}' 镜像名 | 查看镜像配置 | 确认环境变量等 |
docker save -o 文件名.tar 镜像名 | 导出镜像为tar文件 | 内网离线传输 |
docker load -i 文件名.tar | 导入tar文件为镜像 | 离线环境安装镜像 |
docker save和docker load这对命令,在离线环境或者跨机器迁移镜像时特别好用。比如生产环境不能访问外网,你就在本机把需要的镜像save成tar包,拷贝过去再load。这里有个小坑:save出来的tar包是包含所有历史层的,所以文件往往比镜像本身看起来“占空间”要大,这是正常的。如果有人只想要一层文件系统,那就得用export/import,但那个会丢失历史层信息,我不建议日常使用,除非你真的只需要运行时的文件系统。
2. 容器生命周期:从创建到销毁的完整闭环
镜像只是模板,真正跑起来的是容器。很多人理解容器时,脑子里总想着“容器是不是一个小虚拟机”,这么想也不是不行,但容易留下一个误区:以为容器是持久存在的。实际上,容器更像是一个进程,只是这个进程拥有独立的文件系统、网络和进程空间。搞清楚这个定位,你就能理解为什么容器动不动就“没了”,也不再会纠结“容器里改的文件怎么重启就丢了”这类问题。
2.1 docker run:一篇文章吃透最核心的参数
创建容器没法绕开docker run。哪怕你后面用Docker Compose,Compose底层也是调用同样的逻辑。run的参数极多,但日常高频使用的就那么几个,我一个个说。
-d表示后台运行(detach),不加的话容器会在前台运行,直接霸占你的终端,Ctrl+C还会把容器停掉。调试阶段建议不加-d,这样日志直接打在终端上,看得清楚;等确认没问题了再改成-d。--name是给容器起名字,如果不起,Docker会随机分配一个focused_wing之类的名字,做运维的人看到这种名字血压就会上来。-p是端口映射,格式是宿主机端口:容器端口,比如-p 8080:80表示把宿主机的8080端口映射到容器的80端口。这里有个坑:如果你不写-p,光靠容器内部的端口,外面是访问不到的。Docker默认的网络隔离机制就是这么设计的。
-v是数据卷挂载,格式是宿主机目录:容器目录,比如-v /data/mysql:/var/lib/mysql。这一步的作用是把容器里的数据目录映射到宿主机上,这样即使容器被删除、重建,数据也不会丢。我见过太多人因为图省事不挂数据卷,一rm容器,数据库就“失忆”了。--restart=always是设置重启策略,表示Docker守护进程启动时自动拉起这个容器,或者容器异常退出时自动重启。部署到服务器上,这个参数几乎是必加的。
还有两个容易被忽略但很实用的参数:--rm和--network。--rm表示容器退出时自动删除,非常适合跑一次性任务,比如临时跑一个脚本、做一次数据库迁移,跑完即焚,不会在系统里留下一堆死掉的容器。--network用于指定网络模式,默认是bridge桥接网络,但如果你想容器直接共用宿主机网络栈,就用--network host。要注意,host模式下-p参数是失效的,因为容器根本不会拥有独立的网络命名空间,端口直接就是宿主机的端口。很多人第一次用host网络发现端口映射没生效,就是这个原因。
2.2 日常运维三件套:start、stop、restart与rm
容器跑起来之后,日常操作无非就是启、停、重启、删除。docker stop 容器名和docker start 容器名我都经常用,但这里有一个细节值得注意:stop是发送SIGTERM信号,给进程一个优雅退出的机会,默认等待10秒后再发SIGKILL强制杀掉。如果你知道这个容器有比较长的收尾工作,可以用-t参数延长超时时间,比如docker stop -t 30 容器名。相反,如果你想把stop和start合并成一步,直接用docker restart也行,它内部就是先stop再start。
docker rm用于删除容器。删除之前如果容器还在运行,需要先stop,或者直接docker rm -f强制删除。rm -f本质上是先发SIGKILL再删除,所以不推荐在容器有重要状态时使用。还有个命令是docker rename 旧名 新名,这个很简单,但很多人不知道,容器名字起错了不用删除重建,直接改名就行。
暂停和恢复的命令docker pause和docker unpause也有用,它们的机制和stop不同:pause是使用cgroups冻结进程,进程的内存状态还在,只是不再被调度执行。什么时候用得上?比如你想对一个容器做文件系统快照,或者临时让某个服务“冻结”一下,但不想让它彻底退出,就可以用pause。不过日常使用频率确实不高,知道有这回事就行。
2.3 容器运行状态查看与进入容器:exec和attach怎么选
遇到问题要排查,就必须进容器里看。常用的进入容器的方式有两种:docker attach和docker exec。很多人刚开始学的时候会把它们搞混,其实区别非常明显:attach是把你当前的终端连接到容器的主进程上,如果你退出(比如按Ctrl+C),意味着你向容器主进程发送了中断信号,容器可能会跟着退出。而exec是在容器里再启动一个新的进程,比如docker exec -it 容器名 /bin/bash,这个bash进程是独立的,你在这个进程里退出,完全不影响容器主进程。
所以我强烈建议,日常调试一律用exec,千万别用attach去“看容器日志”。有人觉得attach能看到实时输出很方便,但只要你一退出,容器就停了,这个代价太大了。exec的-i和-t参数我也解释一下:-i保持标准输入打开,-t分配一个伪终端。简单说,交互式操作(比如敲命令、看输出、输入密码)需要同时加-i -t,也就是我们常说的-it。如果你只是想在容器里执行一条命令并拿到结果,比如docker exec 容器名 cat /etc/hosts,那不需要-it,直接执行就行。
另外,docker cp是我经常忽略但到关键时刻特别管用的命令。它能在容器和宿主机之间复制文件。比如容器里的应用生成了一个日志文件或导出文件,你想拿出来分析,又不想进容器里搞什么重定向,直接用docker cp 容器名:/app/data.log ./data.log就完事了。反向也一样,比如你想把一个本地的压缩包拷进容器里解压,docker cp 本地文件 容器名:/目标路径即可。这个命令不要求容器处于运行状态,停止的容器照样能拷,非常实用。
3. 看日志、看状态、看资源:调试容器必备的“三看”
容器跑起来不等于一切正常。应用启动失败、端口没监听、内存持续上涨……这些都要靠日志和资源命令来判断。我见过很多人在容器出现问题时慌慌张张把容器删了重建,结果问题复现时没有任何线索。正确做法是先保留现场,看日志、看进程、看资源,定位到原因再动手。
3.1 docker ps的完整用法:别忽略了-a参数
docker ps是查看运行中容器列表的命令,但真正完整的查看方式是docker ps -a,它会列出包括已退出状态在内的所有容器。为什么要看退出的容器?因为你可能需要找回之前的容器ID查看它的日志、检查它的配置,或者基于它重新起一个容器。很多人在容器退出后找不到原来的容器了,其实容器还在,只是状态变成了Exited。
docker ps的输出里包含一列STATUS,它能告诉你容器已经运行了多久、是Up还是Exited、退出状态码是什么。状态码很有价值:如果你看到一个容器反复退出,状态码是1,说明是程序自身的错误;状态码是137,一般是被OOM Kill或者强制杀掉了;143则是被SIGTERM终止。别小看这个细节,排查问题的时候能省不少时间。
docker ps还有过滤功能。docker ps -a --filter "status=exited"可以只看已退出的容器;--filter "name=mysql"可以根据名字模糊搜索。容器数量多的时候,用--last 5只看最近创建的5个,比直接ps刷屏要舒服得多。
3.2 docker logs:查看日志的正确姿势与常见误区
日志是排查问题的第一手资料。docker logs 容器名会把容器内主进程的标准输出和标准错误流都打出来。有个常见的误区:你启动容器时用了-d,然后发现容器好像没起来,这时候第一反应应该是去看日志,而不是急着看进程列表。docker logs经常能直接告诉你答案,比如“port is already allocated”或者“database connection refused”。
docker logs的几个参数也是高频使用的。-f用于实时跟踪日志输出,类似于tail -f,适合观察启动过程;--tail 100表示只看最后100行,日志量特别大的时候,别直接一上来就全量打印,先把最后几十行看明白再说。--since和--until可以按时间范围过滤,比如docker logs --since 30m 容器名只看最近30分钟的日志。不过这里有个限制:只有容器使用标准输出写日志,这些命令才有效。如果应用直接把日志写到了容器内的某个文件里,docker logs是看不见的,你得用exec进去看文件,或者把日志文件挂载到宿主机目录里,我建议从设计上就做好这个规划,别等系统跑起来了再来想着怎么收日志。
3.3 docker top和docker stats:容器里到底发生了什么
当你需要知道容器里现在有哪些进程在跑,用docker top 容器名。它的输出和Linux的top命令类似,可以看到进程的用户、PID、CPU使用率、启动命令等。排查容器内进程异常、僵尸进程、或者确认某个服务是否还在运行,这个命令很直接。
docker stats则是查看所有运行中容器资源占用的命令,输出包括CPU、内存、网络I/O、磁盘I/O等指标。我不建议长时间挂着看,因为它会持续刷新,比较消耗性能。我一般用docker stats --no-stream让它只输出一次快照,然后分析重点。当你遇到宿主机负载突然飙高,用这个命令扫一眼就能定位是哪个容器在“吃”资源。如果需要更细粒度的监控,那就是Prometheus那套体系了,但日常开发机上,stats已经足够。
3.4 容器健康检查与系统信息:inspect、events、system df
docker inspect是查看容器和镜像详细配置的瑞士军刀。它的输出是一个巨大的JSON,看起来压力很大,但只要会用-f格式化输出,就能精确拿到你要的东西。比如查看容器的IP地址:docker inspect -f '{{.NetworkSettings.IPAddress}}' 容器名;查看容器挂载的数据卷:docker inspect -f '{{json .Mounts}}' 容器名。掌握这些常用模板,比你纯靠肉眼在JSON里找要高效得多。
docker events是一个常被忽略的命令,它以事件流的形式输出Docker守护进程接收到的所有事件,比如容器创建、启动、停止、删除、镜像拉取等。我在调试自动化脚本、排查“容器为什么被拉起了”这类问题时非常依赖它。举个例子:你发现某个容器总是在半夜被重启,docker events配合时间戳就能看是不是定时任务或者外部脚本在操控Docker API。
最后再提一下docker system df,前面也提到过,它会显示Docker在镜像、容器、数据卷、构建缓存四个维度上占用的磁盘空间,并统计可回收的量。配合docker system prune使用,prune会把停止的容器、未使用的网络、悬空镜像和构建缓存一次性清理干净。但这里我要特别提醒:docker system prune -a --volumes是“核弹级”操作,它会把所有未使用的数据卷也删掉。数据卷里装的很可能是有用的数据,如果你只是想让磁盘干净一点,建议先手动确认哪些数据卷要保留,再用这个命令。我吃过一次亏,清理完之后才发现开发数据库的历史数据全没了,从那以后我再也不在prune后面加--volumes。
4. 网络与数据卷:让容器和外界正常协作
容器之间、容器与宿主机之间、容器与外部网络之间,这几种通信关系是搞Docker绕不开的内容。很多人遇到“容器访问不了外网”“容器之间互相 ping 不通”“容器重启后IP变了导致连接失败”这类问题,根本原因往往出在网络配置上。数据卷则是另一个让人头大的点:容器删除、升级、迁移之后,数据能不能保得住,全靠一开始挂载是否设计正确。
4.1 容器网络模式:bridge、host、none怎么选
Docker默认提供几种网络驱动。bridge是默认的,每个容器会拥有一个虚拟网卡,通过一个叫docker0的网桥与宿主机通信。这种模式下,容器和外界通信是需要NAT的,而容器对外提供服务则需要-p做端口映射。host模式则直接让容器使用宿主机的网络栈,没有独立的IP,性能开销极小,适合对网络性能要求高的场景。但代价是隔离性变差,你没法用-p做端口控制,容器监听什么端口,宿主机就监听什么端口。none模式表示容器没有网络接口,一般只用于极特殊的离线任务,平时基本不用。
除了这三种,还有一种自定义网络(custom bridge network)是实际开发中最推荐的模式。用docker network create mynet创建一个网络,然后启动容器时用--network mynet接入。自定义网络有一个内置DNS功能,容器之间可以直接用容器名互相访问,不需要查IP地址。这个特性在做微服务联调时尤其重要:你不需要记录服务的IP,直接用服务名就能请求到,比如A容器访问B容器,直接用http://B容器名:8080,即使B容器重启导致IP变了,连接也不会断。
4.2 数据卷与绑定挂载:我该把数据放哪里
数据卷分两类:一种是Docker管理的卷(volume),另一种是绑定挂载(bind mount)。用-v挂载宿主机目录的方式就是bind mount,它的优点是你知道数据具体在哪,可以随时到宿主机路径下查看或备份。而volume则是把数据交给Docker管理,存放位置在/var/lib/docker/volumes/下面,你想直接进去翻目录比较麻烦,但它的好处是跨宿主机迁移更方便,卷可以通过docker volume子命令独立管理。
这里有一个常见的性能和数据安全细节:如果你用bind mount挂了一个空目录到容器的非空目录,容器里原有目录的内容会被“遮住”。举个例子,你挂载/data到Nginx容器的/usr/share/nginx/html,如果/data是空的,网页目录就是空的,404没商量。很多人在换Nginx、换页面文件时莫名其妙出现404,排查半天发现是挂载目录覆盖了镜像里的默认文件。解决方案很简单:先把镜像默认目录里的文件复制到宿主机挂载目录,再挂载进去。
对于数据库这类需要持久化存储的容器,我有几个建议:第一,务必为数据库容器挂载数据卷,防止容器重建后数据丢失;第二,备份时优先使用docker run --rm配合数据卷挂载的方式做逻辑备份,而不是盲目地cp整个数据目录;第三,除非你真的知道自己在做什么,否则不要用root用户直接操作/var/lib/docker/volumes下面的原始卷文件,很容易因为权限问题搞坏数据。
4.3 容器间通信与端口映射的那些坑
端口映射是新手最容易踩坑的地方。-p 8080:80的做法是把宿主机8080端口映射到容器80端口。但你有没有遇到过“8080端口明明没被占用,Docker启动却报端口冲突”的情况?这往往是因为Docker默认的端口绑定地址是0.0.0.0,也就是所有网卡接口都监听。如果你的宿主机有多个IP,或者同时有内网和公网IP,你可能只想让内网访问某个服务,但0.0.0.0会把服务暴露在全部接口上,包括公网。这时候可以指定绑定地址,比如-p 127.0.0.1:8080:80,就只允许本机访问。
另外,容器启动后IP地址是会变的。如果你手动指定了--network bridge,容器每次重启,IP都可能变。所以绝对不要在代码里写死容器的IP地址。如果两个服务需要通信,优先用自定义网络加服务名。如果外部系统需要访问容器内的服务,就用端口映射,别依赖容器IP。
5. Docker Compose:把“一串命令”变成“一份配置”
随着你接触的项目越来越复杂,你会发现单条docker run命令会变得又臭又长:端口要映射好几个,环境变量要设十几个,数据卷要挂几处,依赖关系还要理清楚。拿这个命令去部署、去交接,效率极低。这就要用到Docker Compose了。它本质上是一个“命令编排工具”,把你原本要手敲的docker run参数写进一个docker-compose.yml文件里,用一个docker compose up完成所有容器的创建和启动。
5.1 从一条命令到一份compose文件
Compose文件的语法其实很好理解,一个服务对应一个容器,服务名就是容器之间的通信名。例如你要跑一个Redis和依赖它的应用,写出来的compose文件结构大致是这样的:
services: redis: image: redis:7.0 container_name: my-redis ports: - "6379:6379" volumes: - redis-data:/data restart: always app: build: . depends_on: - redis environment: - REDIS_HOST=redis - REDIS_PORT=6379 ports: - "8080:8080" volumes: redis-data:这里redis这个名字,在app服务里可以直接作为主机名使用,比如REDIS_HOST=redis。因为Compose默认会为整个项目创建一个自定义网络,所有服务都在这个网络里,并且以服务名做DNS解析。depends_on表示服务启动顺序,但这里要强调:depends_on只保证“先启动依赖服务”,不保证依赖服务“已经就绪”。比如数据库容器启动了,但MySQL可能还需要几秒才能接受连接,这时候你的app连接数据库时依然会被拒绝。解决办法是应用层面做重试,或者用健康检查来控制真正的就绪状态。
5.2 Compose常用命令:up和down是主角,但不是全部
docker compose up -d是启动所有服务的命令,-d表示后台运行。如果你修改了compose文件,再执行一次up -d,Compose会自动识别配置变化并重建对应容器,这是非常方便的开发体验。docker compose down则是停止并删除所有容器,连同默认创建的网络也会清理掉。注意,down不会删除数据卷(除非你加了-v),所以数据一般还是安全的。
查看服务状态用docker compose ps,查看日志用docker compose logs -f。进某个特定容器用docker compose exec 服务名 /bin/bash。我经常遇到有人用docker compose logs查看所有服务的日志,然后发现日志量大到根本看不清。实际上docker compose logs后面可以跟服务名,比如docker compose logs app只看app服务的日志,这个筛选能力很实用。
docker compose config是一个好用的校验命令,它会把compose文件解析成最终的配置并输出,如果你写了语法错误、缩进错误,它都会报出来。启动失败时别急着到处找原因,先跑一下docker compose config,很多时候问题就出在YAML格式上。还有一个命令是docker compose pull,只拉取镜像而不启动服务,适合部署前预先下载镜像,避免启动时等太久。
5.3 compose项目组织与命名的讲究
Compose项目默认以所在目录名命名,比如你在/home/user/demo下执行docker compose up,那么创建的网络名就是demo_default,容器名如果你没显式指定,也会带上demo-前缀。如果你同时管理多个项目,建议在compose文件顶部用name: myproject指定一个清晰的项目名,避免后面排查时搞不清哪个容器属于哪个项目。
还有一个很多人没注意的点:Compose文件里不仅可以用image指定镜像,还可以用build指定Dockerfile路径,让Compose在启动前先构建镜像。这句话听起来很平常,但我见过不少新手项目,Dockerfile改了,Compose启动后容器还是旧版本,原因就是没用docker compose build重新构建,直接up用了上次构建的缓存镜像。所以记住:改了Dockerfile,先docker compose build再up -d,不要跳过构建步骤。
6. 开发机上最常遇见的5个坑和排查方法
我在文章里讲到不少具体的命令用法,也知道命令用得再熟练,遇到诡异的环境问题照样会让你卡住半天。接下来这些坑,都是我本人以及周围同事在开发机和服务器上真实经历过的,每一条都有具体的报错信息和排查思路,建议收藏。
6.1 Docker Desktop报错:virtualization support not detected
这个报错基本只出现在Windows上,原因是CPU虚拟化没有开启。virtualization support not detected就是说Docker Desktop检测不到你电脑的虚拟化能力,启动自然就失败了。排查步骤也很简单:打开任务管理器,点击“性能”标签页,看看“虚拟化”一栏是不是“已启用”。如果是“已禁用”,你需要进主板的BIOS/UEFI设置里打开Intel VT-x或AMD-V。不同品牌的主板设置位置不一样,但一般都叫“Virtualization Technology”或“SVM Mode”。改完重启电脑,再启动Docker Desktop一般就好了。
还有一个后续报错也常遇到:failed to connect to the docker api at npipe:////./pipe/dockerdesktop-linux。这个报错的意思是Docker Desktop后台没有真正跑起来,或者客户端连接不到它的API。常见原因是Docker Desktop服务启动失败,或者是用了WSL 2但内核没有更新。这时候先去Windows服务列表里找到com.docker.service,确认它是不是运行状态;如果不行,就打开PowerShell执行wsl --update更新一下WSL内核,然后重启Docker Desktop。这类问题九成以上可以通过“开启虚拟化+更新WSL”两个步骤解决。
6.2 镜像下载慢、拉取超时怎么办
镜像下载慢在国内开发机上是个老话题了。除了配置镜像加速器,还有一个思路是:如果你能访问一台有完整镜像的服务器,可以在服务器上docker save导出镜像为tar文件,再拷贝到本机docker load。这个方法虽然笨,但非常稳定,适合那些加速器也救不了的大镜像。如果是在Dockerfile构建过程中下载基础镜像卡住,可以考虑把基础镜像换成国内有缓存或更小体积的版本,比如把ubuntu:22.04换成ubuntu:22.04的slim版本,或者使用alpine。
6.3 容器时间是UTC,和本地时间对不上
容器内部默认使用UTC时区,你看到的日志时间会比北京时间晚8个小时。这个问题的表现是“日志时间对不上”,排查问题时很容易产生误导。解决方案有两个:一是在启动容器时通过环境变量设置时区,比如-e TZ=Asia/Shanghai;二是在挂载宿主机时区文件,比如-v /etc/localtime:/etc/localtime:ro。使用Compose时,直接在环境变量里加TZ: Asia/Shanghai即可。我建议从一开始就统一好时区规范,不然日志混在一起,排查问题就像破案一样费劲。
6.4 端口占用冲突:address already in use
启动容器时遇到port is already allocated或address already in use,说明端口被占用了。先查一下是谁占用的:本机服务用lsof -i:端口号或者netstat -tlnp | grep 端口号;Docker容器占用的,用docker ps看端口映射列表。找到占用方之后,要么停掉它,要么换个宿主机的映射端口。如果你使用的是Compose并遇到端口冲突,我建议不要把宿主机端口配成8080:80这种固定值,可以考虑让Docker随机分配宿主机端口,比如"8080"只写容器端口,宿主机端口是随机的,用docker compose ps查看实际分配端口。这种方式在本地开发时挺实用。
6.5 Docker命令权限不足:permission denied
在没有配置好的Linux环境上装完Docker,直接执行docker ps可能会报permission denied while trying to connect to the Docker daemon socket。这是因为当前用户不在docker用户组里。解决方法是把当前用户加入docker组:
sudo usermod -aG docker $USER执行后注销重新登录,让组权限生效。如果没有sudo权限,那就没办法了,只能每次用sudo docker。不过这里要提醒一句:把用户加入docker组等同于授予该用户较高的系统权限,因为Docker守护进程是root权限运行的,能操作Docker往往也能间接控制系统文件,所以只建议在你完全信任的开发机上这么配置,生产环境要谨慎管理docker组用户。
排查问题时,我的经验是:先看报错原文提示,再定位是命令语法问题、环境问题还是应用本身的问题。Docker的报错信息其实写得比较清晰,只要你养成“先看报错、再动操作”的习惯,很多坑都能少踩。比如docker run报Unable to find image,说明本地没有这个镜像,正在去拉取,不是错误;报exec: "bash": executable file not found,说明镜像里没有bash,换个sh就好。
最后再分享一个小技巧:终端里把docker ps -a和docker images设置成别名,比如alias dpa='docker ps -a'、alias dim='docker images'。平时敲命令快很多,也不容易手滑输错。等你把这些高频命令都练熟了,再回头看那些动不动就要你“一行命令部署环境”的教程,你就不只是会执行,而是能真正理解它为什么能跑起来,出问题也知道从哪里下手。这大概就是学习Docker最有价值的部分——你不是在背命令,而是在掌握一种管理应用运行环境的方法。