说实话,我见过太多人学 Docker,第一步就倒在安装上。Docker Desktop 明明装好了,一启动就报 virtualization support not detected;好不容易跑起来了,拉个 MySQL 镜像又卡在“镜像下载慢”;容器是创建了,外部程序却连不进去;还有人直接在宿主机上执行 docker ps,被 permission denied 怼了一脸。这些坑我都踩过,所以一直想写一篇能“从头到尾走一遍”的 Docker 实操笔记——从最基础的概念讲起,到 Windows/Linux 安装,到用 MySQL 和 Redis 做实战,到 Compose 编排整套环境,再到自己写 Dockerfile 构建镜像,最后把高频报错整理成一份小抄。这篇的目标很明确:你跟着做一遍,日常开发和部署中 80% 的 Docker 场景都能自己搞定了。
1. 镜像、容器、仓库:先弄清 Docker 的三大基础概念
很多人一上来就敲 docker run,却搞不清自己到底在操作什么。这就好比你要学开车,至少得知道油门、刹车、方向盘各自是干嘛的。Docker 的核心概念其实就三个:镜像(Image)、容器(Container)、仓库(Repository)。
1.1 从“集装箱”理解 Docker 的价值
Docker 这个名字本身就在说它的设计思路——集装箱。传统运输靠散货装卸,大小不一、规格混乱,效率极低;集装箱出现后,所有货物装进统一规格的铁箱子里,吊车一吊就走,轮船、卡车、铁路全都能无缝衔接。
软件世界的问题也是一样的。你写好的程序,依赖特定的操作系统版本、特定的运行时、一堆系统库,换一台机器就“水土不服”。以前的做法是搞虚拟机——每个虚拟机里塞一个完整的操作系统,重量大、启动慢、资源浪费严重。Docker 的做法则是把应用程序和它需要的所有依赖(代码、运行时、系统工具、库、配置文件)全部打包成一个标准化的镜像,然后放到任何装了 Docker 的机器上运行。容器共享宿主机内核,不需要自带系统,所以秒级启动、占用极小。
打个比方:虚拟机是一整套毛坯房,每套都自带承重墙;容器是一间标准酒店客房,基础设施共用,拎包入住。对开发者来说,最直观的感受就是——换电脑、换服务器、给别人部署项目,再也不用“在我电脑上是好的呀”。
1.2 镜像、容器和仓库分别是什么
- 镜像(Image):一个只读的模板,相当于“安装包”或者“类”。里面包含了运行一个程序所需的全部文件和环境配置。你可以把它理解成一张光盘,内容刻好之后就不可变了。
- 容器(Container):镜像运行起来之后的实例,相当于“安装好的程序”或者“对象”。同一个镜像可以同时启动多个容器,每个容器相互隔离,可以启动、停止、删除。
- 仓库(Repository):存放镜像的地方。最常用的是 Docker Hub,相当于镜像界的应用商店,也可以理解成代码界的 GitHub。你可以从仓库拉取(pull)别人做好的镜像,也可以把自己构建的镜像推(push)上去分享或备份。
镜像还有一个重要特性——分层存储。镜像由一层层只读文件系统叠加而成,拉镜像时看到的一行行 Pulling fs layer 就是在下载这些层。基于这个特性,多个镜像可以共享底层,比如你同时装了 MySQL 5.7 和 8.0,底层的操作系统层可能是同一份,不会重复占用磁盘。
1.3 认识 Docker 的客户端和守护进程
日常用的 docker 命令是客户端工具,真正干活的叫 Docker daemon(守护进程)。客户端把命令发给守护进程,由守护进程负责拉镜像、创建容器、管理网络和卷。这就能解释两个新手常见问题:
- 执行 docker ps 提示 “Cannot connect to the Docker daemon”:说明客户端在,但守护进程没跑起来。Linux 下就是 Docker 服务没启动,Windows 下就是 Docker Desktop 没开。
- 执行 docker 命令提示 permission denied:客户端没权限访问守护进程的接口文件 /var/run/docker.sock。解决办法后面安装章节会细说。
理解了这三件套和客户端/服务端的结构,后面所有操作就有了脚手架,不会再觉得 Docker 是一堆零散命令的集合。
2. 安装避坑指南:Windows 和 Linux 下把 Docker 真正跑起来
下面的内容会覆盖 Windows 11 安装 Docker Desktop 和 Linux(以 Ubuntu 为例)安装 Docker Engine 的完整流程。这个过程是坑最多的地方,我会把高频报错直接对到解决方案上。
2.1 Linux 安装 Docker Engine:不要直接用旧版 apt 源
Linux 下装 Docker 最大的坑是用系统自带的 apt 源直接安装,版本非常老,功能不全。推荐先卸载可能存在的旧包,然后通过 Docker 官方源安装。
# 卸载可能存在的旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 添加 Docker 官方 GPG 密钥和软件源 sudo apt-get update sudo apt-get install ca-certificates curl gnupg 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 # 安装 Docker Engine、CLI 和 Compose 插件 sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完之后,让 Docker 开机自启并启动服务:
sudo systemctl enable docker --now验证是否成功:
sudo docker run hello-world能打印出 Hello from Docker! 就算通了。如果直接执行 docker run hello-world 报 permission denied,原因就是我上一节说的——普通用户没有权限访问 docker.sock。当前用户加入 docker 组即可:
sudo usermod -aG docker $USER # 刷新组权限,或重新登录 newgrp docker注意,加组之后一定要重新登录会话或执行 newgrp docker 才能真正生效。还有一点必须提醒:docker 组的权限约等于 root,能操作 Docker 的人基本能控制宿主机,所以不要把无关账号随便加进 docker 组。
2.2 Windows 安装 Docker Desktop:Virtualization 报错的完整解法
Windows 下装 Docker Desktop,最常见的失败画面是启动时弹窗报 “virtualization support not detected” 或者 “failed to start because virtualisation support wasn't detected”。这句话翻译过来是:虚拟化支持没检测到。很多人到这里就放弃了,其实问题基本出在两个地方。
第一,BIOS 里的虚拟化开关没打开。Intel 平台叫 Intel VT-x,AMD 平台叫 AMD-V。进 BIOS 找到类似 Intel Virtualization Technology 的选项,设置为 Enabled,保存重启。这一步在台式机上尤其容易踩,因为不少主板 BIOS 默认关着。
第二,Windows 的虚拟机平台和 WSL2 功能没启用。Docker Desktop 在 Windows 上依赖 WSL2 后端,需要先启用相关功能。用管理员权限打开 PowerShell,执行:
# 启用虚拟机平台和 Linux 子系统功能 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart # 重启电脑 Restart-Computer重启后安装 WSL2 内核并设为默认版本:
wsl --update wsl --set-default-version 2再安装 Docker Desktop。安装完成后打开,如果还报 “failed to start docker application container engine”,多半是 WSL 内核过旧,再执行一次 wsl --update,或者到 Docker Desktop 的设置里把 Use the WSL 2 based engine 勾选上并重启。
还有一个隐蔽问题:如果你装了 VMware Workstation 或 VirtualBox,和 Hyper-V 的虚拟化层会冲突。Docker Desktop 用 WSL2 时依赖 Windows 的虚拟化平台,VMware 16 以下版本可能直接冲突导致启动失败。要么升级 VMware 到支持 Hyper-V 共存的版本,要么卸载第三方虚拟机软件。
装好之后从 Docker Desktop 界面确认右下角状态是 Engine running,再看一眼 docker version 是否正确显示客户端与服务端版本。
2.3 镜像下载慢:配置加速器而不是干等
热词里“docker 镜像下载慢”是我被问得最多的问题之一。Docker Hub 的服务器在海外,国内网络拉取大镜像时经常超时或者只有几十 KB/s。解决办法是配置镜像加速器,而不是用代理(Docker 对代理配置比较挑剔)。
Linux 下编辑 /etc/docker/daemon.json,Windows 下在 Docker Desktop 的 Settings -> Docker Engine 里编辑同样的 JSON 配置:
{ "registry-mirrors": [ "https://docker.m.daocloud.io" ] }修改后必须重启 Docker 才能生效:
sudo systemctl daemon-reload sudo systemctl restart docker配置加速器不是魔法,公共加速源偶尔也不稳定。如果拉取还是失败,可以多试几个公共源,或者换一个拉取量更小的官方镜像 tag(比如把 redis:latest 换成 redis:7.2)。
3. 第一个容器实战:跑 MySQL 8.0 并让外部程序连上它
安装通了,前面一直铺垫,现在终于要跑第一个有实际意义的容器。选 MySQL 8.0 当实战对象,是因为它最贴近日常开发,而且踩坑概率极高——认证插件、端口映射、数据卷、容器内执行命令,这一套下来能覆盖单容器管理的所有核心操作。
3.1 docker run 参数逐项拆解
先拉镜像再启动,也可以直接用 run 命令一步到位:
docker pull mysql:8.0 docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=your_password \ -v mysql_data:/var/lib/mysql \ mysql:8.0每个参数都值得看懂,这决定了你后面会不会被坑:
-d:后台运行,不占用当前终端。没加它,关掉终端或 Ctrl+C 容器就停了。--name mysql8:给容器起名字。不加的话 Docker 会随机生成一个难记的名字,管理起来非常痛苦。-p 3306:3306:端口映射。左边是宿主机端口,右边是容器内部端口。格式是“宿主机IP:宿主机端口:容器端口”,不写 IP 表示绑定所有网卡。本地程序连接 127.0.0.1:3306,实际上就是访问容器里的 MySQL 3306。-e MYSQL_ROOT_PASSWORD=your_password:环境变量。MySQL 官方镜像通过这个变量完成 root 用户的初始密码设置。-v mysql_data:/var/lib/mysql:数据卷挂载,把容器里的 MySQL 数据目录映射到宿主机上一个叫 mysql_data 的卷里。
启动成功后先用 docker ps 看看状态。如果发现容器没起来,用 docker ps -a 能看到退出记录,再用 docker logs mysql8 看日志。MySQL 容器启动失败常见原因无非端口被占或者内存不足,端口占用时代会直接报。查看端口占用可以用:
docker logs mysql8 lsof -i :3306 # Linux/macOS netstat -ano | findstr 3306 # Windows3.2 MySQL 8.0 的外部连接问题:认证插件和 Host 权限
容器跑起来了,但很多人的下一步就卡住了——用 Navicat、DBeaver 或者老版本的 JDBC 驱动连接 127.0.0.1:3306,报错 Authentication plugin 'caching_sha2_password' cannot be loaded。
这是 MySQL 8.0 默认认证插件从 mysql_native_password 改成了 caching_sha2_password,老客户端不认识新插件。解决办法不是换数据库版本,而是进容器把 root 用户的认证方式改回旧的:
docker exec -it mysql8 mysql -uroot -p # 进入 MySQL 后执行 ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;这里还有第二个隐蔽问题:如果你只想让 root 能从外部登录,Docker 官方镜像默认创建的 root 用户 host 就是 %(允许任意主机连接),所以按上面改动就行。但如果是自己建的普通用户,创建时一定要显式指定 host:
CREATE USER 'demo'@'%' IDENTIFIED WITH mysql_native_password BY 'demo123'; GRANT ALL PRIVILEGES ON *.* TO 'demo'@'%'; FLUSH PRIVILEGES;统一用 @'%',否则你会遇到账号建了、密码也对了,但连接时一直报 Access denied 的诡异问题——那多半是用户只允许从 localhost 登录。
3.3 数据持久化:容器删了,数据必须还在
启动命令里我已经用了-v mysql_data:/var/lib/mysql,这个卷的含义必须理解透。容器是一个轻量、可随时销毁的实例,如果用默认匿名存储,容器一删除,里面所有数据就跟着没了。这对数据库来说是灾难性的。
具名卷(named volume)就是给数据一个宿主机上的永久存储位置。查看它到底存在哪:
docker volume inspect mysql_data输出里会有 Mountpoint 字段,指向宿主机上的实际目录。你随时可以把这个目录里的文件备份到别处。也正因为有这个机制,升级 MySQL 版本时可以先停旧容器,再起新容器挂载同一个卷,数据无缝衔接。
除了具名卷,还有一种挂载方式是绑定挂载(bind mount),直接指定宿主机目录映射进容器:
docker run -d --name mysql8 -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=your_password \ -v /data/mysql:/var/lib/mysql \ mysql:8.0绑定挂载的好处是文件直接落在宿主机目录里,方便查看和备份,适合有明确目录规划的生产环境。具名卷则胜在由 Docker 统一管理,路径不用自己操心,适合开发环境。开发阶段我建议一律使用具名卷,省心。
3.4 进入容器:想执行什么命令都直接来
排查问题和执行管理操作时,需要进入容器内部。MySQL 官方镜像自带客户端,所以可以直接:
docker exec -it mysql8 mysql -uroot -p # 然后输入密码进入 SQL 命令行如果镜像里没有你想要的工具,比如需要查看容器内网络情况,可以进入容器的 shell:
docker exec -it mysql8 bash注意 MySQL 镜像基于 Debian/Ubuntu,里面不一定装了 ping、vi 等工具,真要装得先 apt-get install。这暴露了一个通用规律:容器是精简环境,不是完整操作系统。能不用容器里的工具,就不要依赖,宿主机能完成的操作用宿主机做即可。
4. Docker 网络模型:从 Redis 主从架构理解容器之间如何通信
mysql 那节解决了“宿主机访问容器”的问题,这节解决“容器访问容器”的问题。热词里“docker 网络不通”出现频率极高,而且绝大多数场景都是同一个原因——容器不在同一个自定义网络里。
4.1 三种网络模式的适用场景
Docker 默认提供三种网络模式:
- bridge:默认模式。容器通过虚拟网桥与宿主机通信,外部访问需要端口映射。适合单机上的多个容器互相隔离但需要网络通信的场景。
- host:容器直接使用宿主机网络栈,不做隔离。性能最好,但端口直接暴露在宿主机上,容易冲突。仅 Linux 支持,Windows 和 Mac 的 Docker Desktop 不支持。
- none:禁用网络,相当于完全隔离。适合只跑离线任务的容器。
习惯上直接 docker run 没指定网络时,容器会连到默认的 bridge 网络。默认 bridge 有名字解析功能吗?有,但很弱——容器之间只能用 IP 访问,而且容器重建后 IP 会变。这就是“网络不通”的一大来源。我们需要的是一种“不用记住 IP、用名字互访”的方案,这就是自定义 bridge 网络。
4.2 自定义网络:容器间用服务名互访
创建网络并让容器加入,一条命令开一台主从 Redis:
# 创建自定义 bridge 网络 docker network create redis-net # 启动 Redis 主节点 docker run -d --name redis-master --network redis-net -p 6379:6379 redis:7 # 启动 Redis 从节点(注意没有映射端口,外部不需要直接访问它) docker run -d --name redis-slave --network redis-net redis:7 \ redis-server --replicaof redis-master 6379核心魔法就在--network redis-net。在这个网络里,Docker 内置 DNS 会把容器名解析为对应 IP。从节点配置里的 redis-master 并不是宿主机主机名,而是主节点容器名。只要在同一个自定义网络里,这个名字就能被解析,网络就通了。
验证主从是否建立成功:
docker exec -it redis-slave redis-cli info replication输出里如果显示 role:slave 和 master_link_status:up,说明主从就绪。如果换成默认 bridge 网络,从节点根本解析不了 redis-master,会一直报 “Can't connect to Redis server”。这也是“docker 网络不通”最常见的真相。
4.3 容器访问宿主机服务:host.docker.internal 这个彩蛋
实际开发里还有一种需求:容器里的程序要连宿主机上的某个服务,比如 MySQL 装在宿主机而非容器里。容器和宿主机之间什么地址是通的?
Linux 下最简单的方式是用--network=host直接共享宿主机网络栈,容器内访问 localhost 就是访问宿主机。但 Docker Desktop(Windows/Mac)不支持 host 模式,这时可以用 Docker 内置的host.docker.internal域名,它专门解析到宿主机的 IP。
docker run -d --name myapp --add-host=host.docker.internal:host-gateway myapp:latest在 Linux 上想彻底模拟 Docker Desktop 的体验,加这个参数即可。容器里配置数据库连接地址时用jdbc:mysql://host.docker.internal:3306/demo,从此不再纠结宿主机 IP 到底是 192.168.x.x 还是 172.17.x.x。
4.4 容器网络排错三板斧
遇到容器间访问失败,我通常按三步走,效率很高:
第一,确认两个容器在同一个自定义网络:docker network inspect redis-net,看 Containers 字段列出的容器是否都在。
第二,进入容器内部测试连通性。比如 redis-slave 里 ping redis-master 不通,问题大概率是 DNS 解析或网络隔离,而不是业务层面的问题。
第三,查看进程监听端口:docker exec 容器名 netstat -tlnp,确认目标进程确实监听在容器内的正确端口上。很多“连不上”最后发现是服务本身没起来或配置监听地址不对。
5. Docker Compose:用一份 YAML 管理整套环境
单个容器用 docker run,两个容器手动加网络也能凑合。但到了“应用 + MySQL + Redis + 队列”这种典型组合,一条条命令敲下去就是灾难。Docker Compose 就是干这个的:用一份 YAML 声明所有服务、网络、卷,然后docker compose up -d一键拉起整套环境。
5.1 Compose 文件的结构和语义
以一个典型的后端项目为例,你有一个 Spring Boot(或任意 Web 服务)应用,依赖 MySQL 和 Redis。docker-compose.yml 长这样:
services: mysql: image: mysql:8.0 container_name: app-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: demo ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 redis: image: redis:7 container_name: app-redis ports: - "6379:6379" app: build: . container_name: app-server depends_on: mysql: condition: service_healthy redis: condition: service_started ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: dev volumes: mysql_data: networks: default: name: app-net这里有几个值得说明的设计点:
services下每个服务等同于一个容器定义。image可以直接用现成镜像,build则指定从当前目录的 Dockerfile 构建。- 同一份 Compose 文件里的服务,默认自动加入同一个网络(上面的 networks 配置把网络命名为 app-net)。在应用代码里,配置数据库地址直接写
jdbc:mysql://mysql:3306/demo,Redis 地址写redis://redis:6379,这里的 mysql、redis 就是服务名,由 Compose 负责解析成容器 IP。 depends_on控制启动顺序。我特意加了 healthcheck,因为 depends_on 只保证先启动,不保证“可用”。不加健康检查,应用启动时 MySQL 还在初始化,就会连库失败。这是 Compose 使用中很经典的一个坑。volumes顶层声明了具名卷 mysql_data 供 mysql 服务使用,数据和容器生命周期解耦。
5.2 常用命令和更新流程
# 启动所有服务(后台) docker compose up -d # 查看状态 docker compose ps # 查看日志(-f 持续跟踪) docker compose logs -f app # 进入某个服务容器 docker compose exec app bash # 停止并删除所有服务(保留数据卷) docker compose down # 停止并删除所有服务,同时删除声明的卷(数据没了,慎用) docker compose down -v # 校验 YAML 配置 docker compose config日常更新服务的标准流程是这样:改完代码重新 build,然后 up -d。因为配置没变,Compose 只会重建受影响的容器,其他服务保持不动。这比手动停掉再启动要安全得多。
docker compose build app docker compose up -d5.3 迁移和备份的便利性
Compose 的价值在换环境时体现得淋漓尽致。公司新员工配开发环境,以前要写一份好几页的 Wiki 安装文档,现在丢一个 docker-compose.yml 过去,让他们自己docker compose up -d,十分钟搞定。我自己的小项目也是这种模式,代码仓库里放一份 Compose 文件,本地开发、测试服务器、朋友要跑一套环境,同一份配置通吃。
要注意的是,新版 Docker Compose 已经全面使用docker compose子命令(带空格),旧版的docker-compose独立命令已经逐步淘汰。如果你用的是新版 Docker Engine,已经包含 compose 插件,直接用新命令即可。
6. 构建自己的镜像:Dockerfile 入门与层缓存优化
用完别人的镜像,迟早要自己做镜像——打包自己的应用、定制运行环境、给项目做交付。Dockerfile 就是构建镜像的“菜谱”,一条指令对应镜像中的一层。
6.1 一个最小可用的 Node.js 镜像
假设有一个 Node.js 应用,根目录下有 app.js 和 package.json。最直观的 Dockerfile 这么写:
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["node", "app.js"]逐条解读一下:
FROM:指定基础镜像。node:20-alpine 是精简版,体积比 node:20 小一半以上,适合最终部署。WORKDIR:切换工作目录,后续命令都在这个目录下执行。COPY package*.json ./:只先把依赖清单拷贝进镜像。RUN npm install:在镜像里安装依赖。COPY . .:把项目代码拷贝进去。EXPOSE:声明容器对外端口,纯文档性质。CMD:容器启动时执行的命令。
构建命令:
docker build -t myapp:v1.0 .注意最后的点表示构建上下文是当前目录。Docker 会把这个目录打包发送给守护进程作为上下文,所以不需要的文件最好别往里放。
6.2 层的原理和理解缓存才是关键
很多人写完 Dockerfile 发现每次改代码都要重新 npm install 一遍,慢得要命。理解了镜像分层就懂了——每条 COPY 和 RUN 产生一个新层,Docker 构建时如果发现某一层没有变化,会直接复用缓存。
所以上面这个 Dockerfile 的顺序是刻意设计的:package.json 在代码前面拷贝,npm install 在前面执行。这样你只改 app.js 时,package.json 没变,RUN npm install 这层会命中缓存,整个镜像构建只需要拷贝代码这一层的时间。反过来如果把COPY . .放在前面,每次代码变更 npm install 都会全部重跑。
你会发现很多老手写 Dockerfile 特别讲究指令顺序,不是洁癖,是实打实的构建速度优化。
6.3 多阶段构建:镜像瘦身的重要手段
以 Java/Go 这类编译型语言为例,构建需要完整 JDK,但运行只需要一个小型运行时。多阶段构建让一个 Dockerfile 里出现多个 FROM,最后一个 FROM 是最终镜像:
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 CMD ["java", "-jar", "app.jar"]最终镜像只包含 JRE 和一个 jar 包,体积从带全套 Maven 和源码的几百 MB 缩到一百多 MB。这一套思路同样适用于所有编译型语言。
6.4 常用工具链的镜像打包思路
热词里提到 “IDEA 打包 docker 镜像” 和 “php 使用 docker 打包镜像”。思路其实都是同一个:写好 Dockerfile,然后交给任何可以调 docker build 的工具去执行。IDEA 里装好 Docker 插件并配置连接本机 Docker 后,可以直接右键 Dockerfile 选择 Build Image,本质就是图形化调 docker build。
PHP 项目打包时需要注意基础镜像选择。官方 php 镜像分 cli 和 fpm 版本,项目需要哪些扩展就用 docker-php-ext-install 安装,第三方扩展有时需要编译。比如:
FROM php:8.2-fpm RUN docker-php-ext-install pdo_mysql COPY . /var/www/html这类镜像无法做到开箱即用是因为 PHP 扩展生态太丰富,官方不可能全预装。理解 Dockerfile 的指令逻辑之后,这类定制完全在自己掌控中。
7. 高频报错排查手册:一张表解决 90% 的启动和连接问题
最后这部分是我的实战小抄。做 Docker 这些年,日常被问到最多的报错就集中在下面这十来条。整理成表格,遇到问题直接对表操作。
7.1 常见报错自查表
| 报错信息(关键词) | 常见原因 | 处理思路 |
|---|---|---|
| permission denied while trying to connect to the Docker daemon | 当前用户不在 docker 组 | sudo usermod -aG docker $USER,重新登录 |
| Cannot connect to the Docker daemon at unix:///var/run/docker.sock | Docker 服务没启动 | Linux 执行 systemctl start docker;Windows 打开 Docker Desktop |
| virtualization support wasn't detected | BIOS 虚拟化没开 / WSL2 功能未启用 | BIOS 开启 VT-x/AMD-V;启用虚拟机平台和 WSL |
| failed to start docker application container engine | WSL 内核过旧 / 资源不足 | wsl --update;给 Docker Desktop 分配更多内存 |
| driver failed programming external connectivity on network | 宿主机端口被占用 | docker ps 看现有端口占用,或改 -p 宿主机端口 |
| name already in use | 容器名冲突 | docker rm -f 旧容器,或换 --name |
| pull access denied / manifest unknown | 镜像名或 tag 写错 | docker search 确认正确的仓库名和 tag |
| no space left on device | 磁盘空间不足 | docker system prune -a 清理无用镜像缓存 |
| Error response from daemon: conflict | 端口或资源冲突 | docker ps -a、docker inspect 详细排查 |
| Authentication plugin 'caching_sha2_password' cannot be loaded | MySQL 8 默认认证插件和旧客户端不兼容 | 在容器内 ALTER USER 改认证插件 |
7.2 排查思路:不要瞎试,按链路走
遇到报错,第一反应不是去搜报错全文,而是做好三件事:看状态、看日志、看配置。
看状态:docker ps -a确认容器是 Exited 还是 Up。Exited (0) 表示正常退出,查应用日志;Exited (1) 通常表示启动时有非零退出码,多半是初始化脚本或命令出错。
看日志:docker logs --tail 100 容器名。这是最直接的线索来源,比任何猜测都靠谱。启动失败看启动日志,运行异常看运行日志。
看配置:docker inspect 容器名会打印容器的完整元数据,环境变量、挂载卷、网络、端口映射全在里面。遇到“明明配置了为什么没生效”的诡异问题,先 inspect 一遍,很多答案直接就能看出来。
7.3 日常运维必须养成的习惯
容器跑久了,磁盘被吃掉是必然的。Docker 会积累镜像、容器、卷、构建缓存,每一样都可能占大量空间。先用docker system df看看空间被谁占了,再执行清理:
# 清理停止的容器、悬空镜像、无用网络 docker system prune # 更彻底:连未使用的镜像和构建缓存一起删 docker system prune -a警示一下:docker system prune -a会把没在使用的镜像全部删掉,下次启动需要重新拉取。还有卷并不会被上面的命令自动清理,如果确认卷数据不要了,用docker volume prune,但这操作对数据库是毁灭性的——执行前一定想清楚。
还有两个实用习惯:一是启动命令加--restart unless-stopped,让机器重启后容器自动拉起;二是给容器设置资源约束,避免某个容器吃掉整个宿主机的内存:
docker run -d --name myapp --restart unless-stopped --memory 1g --cpus 1.0 myapp:latest7.4 容器日志无限增长的处理
容器日志默认存到 /var/lib/docker/containers/{id}/*-json.log,不设上限的话,一个高频日志的服务几周就能写出几个 GB。建议在 daemon.json 里统一配置日志轮转:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }改完重启 Docker 才生效,而且只对之后创建的容器生效。已经跑着的容器需要重建(docker rm -f + 重新 run)才能应用新策略。手动截断当前日志的办法是:
truncate -s 0 /var/lib/docker/containers/{容器ID}/*-json.log容器还开着也能用这个方法清空文件,不需要重启,比删文件安全——删文件可能会导致容器内文件描述符失效。
我自己的体会是,Docker 的报错看着吓人,但只要养成了“状态—日志—配置”这个排查链路,绝大多数问题都能在三五分钟内定位。热词里那些几十次被搜索的报错,推到最后都是几个基础概念没搞清楚:daemon 没起、权限不够、容器不在同一网络、端口冲突、数据没卷挂载。把这套从安装到编排的流程完整走一遍,再回来看这些错误,它们就不再是“玄学”,而是一道道有标准答案的送分题了。真心建议你把手上常用的 MySQL、Redis 都顺手容器化一遍,踩过一轮坑,比看十遍教程都管用。