1. 先从“环境不一致”说起:Docker 解决的核心问题
如果你是一个写过代码的开发者,大概率遇到过这样的场景:本地代码跑得好好的,一到测试服务器就各种报错,一会儿 JDK 版本不对,一会儿 Redis 没装,一会儿数据库密码没配;换一台新电脑想复现老项目,光装依赖就耗掉一个下午。这种“在我机器上明明是好的”的尴尬,背后真正的痛点就是环境不一致。Docker 就是在这样的背景下火起来的——它把应用连同它依赖的环境一起打包成一个标准单元,只要宿主机有 Docker,这个单元放在哪里都能跑,运行结果一模一样。
打个比方,以前部署一个项目就像是让人搬进毛坯房,你得自己刷墙、接水电、买家具,每个房子的装修还各不相同;而 Docker 的做法是直接把整个精装房原封不动搬过去,里面每一件家具、每一条线路都已经调试好,搬过去插上电就能住。这个“精装房”就是 Docker 镜像,真正住进去运行时的那一套流程就是容器。理解了这两个词,Docker 的大门就推开了一半。
这篇内容适合刚接触 Docker 的开发者、运维新人,以及想把自己项目打包部署的独立开发者。我会从核心概念讲起,然后是 Windows 和 Linux 两种系统的安装,再到常用命令、数据管理、Compose 编排,最后给三个实战场景和一份高频报错排查表。全程用我实际踩过的坑说话,不讲虚的。
2. Docker 的核心概念,一次讲透
2.1 镜像和容器,一张“模板与实例”的关系
镜像(Image)和容器(Container)是 Docker 里出现频率最高、也最容易混淆的两个词。我的理解是:镜像是静态的、只读的模板;容器是镜像跑起来之后形成的动态实例。用程序开发类比,镜像相当于类 Class,容器相当于 new 出来的对象 Object;用拍照类比,镜像是一张完成品照片,容器是把照片无限冲印出来的实体相框。同一个镜像可以同时启动多个容器,彼此互不干扰。
镜像是分层构建的,每一层对应 Dockerfile 里的一条指令,比如安装 Node.js、拷贝代码、设置工作目录。这种分层设计带来的直接好处是复用:多个镜像可以共享底层的基础层,比如 Ubuntu 系统层、OpenJDK 层,拉取新镜像时如果底层已经存在,就只需要下载增量层。这也是为什么第一次拉镜像往往很慢,而第二次拉一个公用基础层较多的镜像时会明显变快。
容器基于镜像创建之后,可以拥有自己的可写层:你可以往里面写文件、装软件、改配置,容器停止后这些改动默认不会丢失,但如果容器被 docker rm 删除,可写层里的数据会一起消失。所以容器里产生的关键数据,必须用后面要讲的数据卷持久化到宿主机。
2.2 Dockerfile、仓库、数据卷、网络,四块基石
Dockerfile 是构建镜像的“配方文件”,里面按顺序写清楚基础镜像、依赖安装、代码拷贝、启动命令。写好之后执行 docker build,Docker 会逐条执行指令并生成新镜像。仓库 Registry 是存放镜像的地方,Docker Hub 是最常用的公共仓库,也可以自建私有仓库或在企业内部搭一个,便于团队共享基础镜像。
数据卷 Volume 是容器与宿主机之间共享数据的机制,它独立于容器生命周期存在,即使容器被删掉,数据卷中的数据依然保留。最常见的用法是把 MySQL 的数据目录、Nginx 的日志目录、项目的配置文件挂载到宿主机对应路径上,保证容器重启或迁移后数据不丢。
网络部分最实用的就是端口映射和自定义网络。容器默认处于隔离的桥接网络中,外部访问不到容器内部端口,所以要用 -p 参数把宿主机端口映射到容器端口;多个容器之间协作时,建议创建一个自定义网络,容器之间用服务名直接互通,不依赖 IP 地址。
2.3 为什么是 Docker:隔离、资源、交付
相对于传统虚拟机,Docker 最大的优势是轻量。虚拟机需要为每个实例运行一个完整的操作系统,动辄几个 GB 内存占用;而 Docker 容器共享宿主机内核,只包含应用和运行库,启动速度是秒级,内存占用往往只有几十到几百 MB。同一台 4G 内存的服务器上,跑虚拟机可能只能撑两三个,跑 Docker 容器能跑到一二十个。
隔离性方面,每个容器有独立的文件系统、进程空间和网络栈,一个容器里的环境变量、配置文件、安装的软件不会污染宿主机,也不会影响其他容器。这带来两个实际好处:一是可以在同一台机器上跑同一个软件的多个版本而不冲突,二是拿到一个镜像就可以在一台裸机上直接复现生产环境,无需手工安装依赖。
第三个价值是交付方式的统一。以前交付一个服务,要写很长的安装文档、环境准备手册;现在交付物就是一个镜像,接收方执行 docker run 或 docker compose up 就能启动。镜像本身是可以校验、可以定版本、可以推送到仓库的,这相当于把部署流程标准化了。团队协作时,开发、测试、生产用的镜像如果来源一致,因为环境差异导致的疑难问题就能大幅减少。
3. 安装 Docker:Windows 与 Linux 双向亲测
3.1 Windows 安装 Docker Desktop,以及常见的虚拟化报错
Windows 上目前主流的方案是安装 Docker Desktop,它有两种后端:基于 WSL2,以及基于 Hyper-V。我个人更推荐 WSL2,因为它更轻量,磁盘 IO 性能也更好,而且 Windows 10 以上的系统基本都支持。安装流程大概是:先启用“适用于 Linux 的 Windows 子系统”和“虚拟机平台”两个 Windows 功能,然后在线安装 WSL2 内核,最后安装 Docker Desktop 安装包。
很多人在安装后第一次启动 Docker Desktop 时,会卡在一个经典报错上:Docker Desktop failed to start because virtualisation support wasn’t detected,翻译过来就是“检测不到虚拟化支持”。这个报错的原因大致有三个。第一是电脑 BIOS 里没开启虚拟化技术,Intel 平台叫 VT-x,AMD 平台叫 SVM,重启进 BIOS 找到对应选项打开;第二是 Windows Hyper-V 或虚拟机监控程序与 WSL2 冲突,可以在“启用或关闭 Windows 功能”里检查一下相关项;第三是 WSL2 内核没有正确安装,去微软官网下载更新即可。排查顺序建议是:先看任务管理器性能页的“虚拟化”是否显示已启用,再查 Windows 功能,最后重装 WSL2 内核。
装好之后 Windows 用户还会遇到一个高频报错:failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen。这个一般不是配置问题,纯粹是 Docker Desktop 没启动起来。先到系统托盘找到 Docker 图标启动,等鲸鱼图标不再跳动、变成静止状态,再到命令行里跑 docker version 验证。如果桌面端启动失败,先解决启动报错,API 连接问题通常会跟着消失。
3.2 Linux(Ubuntu / CentOS)下怎么装才不踩坑
Linux 安装 Docker 有两条路线:一条是直接用发行版自带的软件源安装,优点是一行命令搞定,缺点是版本不一定是最新的,比如 Ubuntu 上装的是 docker.io,版本可能落后官方好几代;另一条是使用 Docker 官方提供的软件源,装出来的 docker-ce 版本更新更全。对绝大多数新手来说,我建议直接用官方源安装,虽然多敲几行命令,但后续不容易遇到老版本镜像兼容性的问题。
Ubuntu 和 Debian 系的安装流程大致是:先执行 apt update 更新源,再安装 ca-certificates、curl、gnupg 等几个基础工具,然后添加 Docker 官方 GPG 密钥,把 docker-ce 的软件源写入 /etc/apt/sources.list.d/ 下,再次 apt update 之后 install docker-ce docker-ce-cli containerd.io。CentOS 7 的老用户需要注意,CentOS 7 自带的 yum 仓库中 Docker 版本很旧,推荐先安装 yum-utils,再用 yum-config-manager 添加 Docker 官方 CentOS 源,最后安装 docker-ce 相关包。如果公司网络访问官方源不稳定,也可以用镜像站源替换,方法是把下载源地址换成可用的镜像源域名。
装完之后先别急着跑容器,第一步是启动服务并设置开机自启:systemctl start docker,systemctl enable docker。然后跑一次 docker run hello-world,如果看到一段 “Hello from Docker!” 的输出,就说明整个 Docker 引擎已经能正常工作。这里有个容易出错的地方:很多教程会让你把用户加入 docker 组,这样以后可以不写 sudo。但 docker 组的权限本质等价于 root 权限,因为它能直接挂载宿主机目录、管理网络和进程。如果你只是个人开发机,加入组很方便;如果是在生产环境的共享机器上,务必评估好安全风险,不建议随意放开。
3.3 改动守护进程配置,别把 JSON 写坏
Docker 服务端的核心配置文件是 /etc/docker/daemon.json,很多全局行为都在这里配置,比如镜像加速、日志大小限制、存储驱动等。修改这个文件之后,需要重启 Docker 服务才能生效:systemctl restart docker。最常见的问题是用户手写 JSON 时漏了逗号或多了花括号,导致 Docker 服务直接起不来。排查方法是先执行 docker info 或直接看服务状态,再用 python3 -m json.tool /etc/docker/daemon.json 校验 JSON 格式是否正确。
我给一个通用的 daemon.json 模板,适合日常开发和测试环境:
{ "registry-mirrors": [ "https://docker.m.daocloud.io" ], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "storage-driver": "overlay2" }这里多做一步解释:max-size 和 max-file 两个参数是用来限制容器日志大小的。默认情况下 Docker 会无限收集容器 stdout 输出,一个不打印日志但长期运行的容器,可能几个月就把磁盘写满。我见过不止一次因为日志把 /var/lib/docker 占满导致服务挂掉的故障,提前在 daemon.json 里限制日志大小是成本最低的预防手段。registry-mirrors 字段是用来配置镜像加速的,不同网络环境下可用性差异很大,实际使用时以自己测试的结果为准。
4. 镜像与容器常用命令,边用边记
4.1 镜像相关命令:拉取、查看、删除、打标签
# 拉取镜像,不写 tag 默认拉 latest docker pull nginx docker pull mysql:8.0 docker pull redis:7-alpine # 查看本地镜像列表 docker images # 搜索远程镜像 docker search nginx # 删除镜像,-f 强制删除 docker rmi nginx docker rmi -f mysql:8.0 # 给镜像打标签,常用于推到自建仓库 docker tag nginx:latest myregistry.example.com/nginx:1.25拉取镜像时建议尽量写具体版本,不要用 latest。虽然 latest 用起来方便,但它指向的镜像可能在某一天突然更新,导致你本地容器行为和之前不一致,而且回滚时还得花精力找到之前的版本号。生产环境里镜像一定要钉死版本,比如 mysql:8.0.36,nginx:1.25.4,这样能显著减少不确定性。
这里再解释一下 registry-mirrors 为什么对体验影响这么大。如果你直接不配置镜像加速,从 Docker Hub 拉镜像可能非常慢甚至超时,尤其是大镜像。配置了可用的镜像源之后,内部会走加速通道,拉取速度通常能得到明显提升。碰到特别大的镜像,比如几十 GB 的 AI 推理镜像,建议分段验证:先拉基础层,再拉业务层,不要一次性干等。
4.2 容器相关命令:生命周期全流程
# 启动一个新容器,-d 后台运行,--name 起名字,-p 映射端口 docker run -d --name my-nginx -p 8080:80 nginx:1.25 # 查看正在运行的容器 docker ps # 查看所有容器,包括已停止的 docker ps -a # 停止、启动、重启容器 docker stop my-nginx docker start my-nginx docker restart my-nginx # 删除容器,删除前必须先 stop docker rm my-nginx # 强制删除所有已停止的容器 docker container prunedocker run 的参数很密集,新手容易懵。关键点拆开看:-d 表示 detached,让容器在后台运行,否则终端会直接被容器的前台进程占用;--name 给容器设一个别名,后续命令都用别名操作,比记住一串随机 ID 方便得多;-p 8080:80 是端口映射,冒号左边是宿主机端口,冒号右边是容器内部端口,外部请求宿主机 8080 端口就会被转发到容器内的 80 端口。
很多新手会困惑:我改了容器里的配置文件,为什么不生效?这是因为容器运行时,很多配置在进程启动时就已经读取了,修改文件后需要重启容器的内部进程或直接 restart 容器。另外,docker run 每次都会创建一个全新的容器,同一个镜像多次 docker run 会得到多个互相独立的容器,数据默认不共享。想要复用,要么用同一个容器名,要么使用数据卷挂载。
4.3 日志、进入容器、文件拷贝与资源清理
# 查看容器日志,-f 实时跟踪,--tail 只显示最后 N 行 docker logs -f --tail 100 my-nginx docker logs --since 2024-01-01T00:00:00 my-app # 进入正在运行的容器,常用交互式 shell docker exec -it my-nginx bash # 从宿主机拷文件到容器 docker cp ./config.json my-nginx:/etc/nginx/conf.d/ # 从容器拷文件到宿主机 docker cp my-nginx:/var/log/nginx/access.log ./access.log # 查看容器资源占用 docker stats # 清理悬空镜像、停止容器、无用网络和构建缓存 docker system prune -adocker exec 是排查容器内部问题的最常用工具。进入容器后先看进程:ps aux,再看配置文件:cat /etc/nginx/nginx.conf,修改完退出。但要注意:容器内部一般没有 vi、vim 这些编辑器,很多精简镜像只有基础命令,想在容器里改文件往往要先 apt install 或 apk add,比较麻烦。更推荐的做法是直接在宿主机改挂载目录里的文件,然后 restart 容器,这样既方便又不用依赖容器内的工具。
docker system prune 是清理磁盘的一把好手,但用的时候要小心。它默认会删除所有未被容器引用的镜像、网络、构建缓存,-a 参数还会把没有被容器使用的镜像也删掉。如果你只是临时拉了一个大镜像准备用,但先不小心 prune 了,那就得重新拉。建议定期执行 docker system df 查看当前空间占用分布,再决定清理到什么程度。
5. 数据管理与 Docker Compose 编排
5.1 数据卷和 bind mount,到底该用哪个
数据持久化有两种主流方式:数据卷 Volume 和绑定挂载 bind mount。数据卷由 Docker 管理,位置在 /var/lib/docker/volumes/ 下,你只需要给卷起个名字,不需要关心它在宿主机的具体路径;绑定挂载则是把宿主机的一个目录直接映射到容器内,比如把项目的配置文件目录映射进去,修改宿主机文件容器内立即生效。
选择上我的建议是:数据库、缓存这类需要备份和迁移的数据用数据卷,配置文件、代码这类需要频繁编辑的内容用绑定挂载。举个例子,MySQL 数据目录应该用数据卷:
docker volume create mysql_data docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -v mysql_data:/var/lib/mysql \ -p 3306:3306 \ mysql:8.0而 Nginx 的配置如果放在宿主机上,则适合用绑定挂载:
docker run -d \ --name nginx \ -p 80:80 \ -v /home/user/nginx/conf:/etc/nginx/conf.d \ -v /home/user/nginx/html:/usr/share/nginx/html \ nginx:1.25绑定挂载有个容易踩的坑:宿主机挂载目录的权限和属主如果不匹配容器内进程,容器会报权限拒绝。比如容器内以 nginx 用户运行,而宿主机目录属主是 root,就会出现读不到文件的情况。解决方法是把目录属主改成容器内的 uid,或者干脆把权限放开一些。我自己遇到过多次这种问题,定位思路是先 docker logs 看报错,再检查目录权限,最后调整属主。
5.2 端口映射、网络模式与服务名互通
默认情况下容器有自己的 IP 地址,宿主机访问容器需要端口映射。但容器每次重启后 IP 可能变化,所以多容器之间不建议直接用 IP 通信。更好的做法是把它们放到同一个自定义网络中,用容器名作为主机名互相访问。Docker 内置的 DNS 会解析容器名到对应 IP,配置一次就能长期稳定使用。
# 创建自定义网络 docker network create app-net # 两个容器都加入该网络 docker run -d --name mysql8 --network app-net -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0 docker run -d --name my-api --network app-net -p 8080:8080 my-api:latest这样 my-api 容器里连接数据库时,数据库地址直接写 mysql8,端口写 3306,不需要关心 MySQL 容器的 IP 是多少。这个机制是 Docker Compose 能实现“一键起整套服务”的基础。后面实战部分会用它来部署 Redis 主从。
5.3 Docker Compose:用一份 YAML 描述整套服务
当服务多起来之后,一条条 docker run 命令既难记又难维护。Docker Compose 的作用就是用一份 docker-compose.yml 把多个容器的配置集中管理,执行 docker compose up -d 即可一键启动所有服务。下面的示例定义了一个 Web 应用和一个数据库:
services: web: image: my-api:latest ports: - "8080:8080" depends_on: - db environment: - DB_HOST=db - DB_PORT=3306 db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: 123456 volumes: - db_data:/var/lib/mysql volumes: db_data:这里有三个细节值得注意。第一,Compose 会默认创建一个网络,所有 service 都在这个网络里,所以 web 服务里 DB_HOST 直接写 db 就能访问到数据库容器。第二,depends_on 只控制启动顺序,不保证依赖服务已经就绪,真正有用的做法是在应用里加重试机制。第三,新版 Docker Compose 已经不需要写 version 字段了,直接写 services 即可,旧版模板里的 version 字段在新版中会被忽略。
6. 三个实战场景,直接把 Docker 跑起来
6.1 实战一:部署 MySQL 8.0,数据不丢还能改时区
先用数据卷启动一个 MySQL 8.0 容器:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=MyPass123! \ -v mysql_data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0启动后用 mysql -h127.0.0.1 -uroot -pMyPass123! 验证连接。如果连不上,优先排查端口映射:docker ps 看 3306 是否显示,以及 tar -p 参数是否正确。然后检查防火墙,云服务器还要在安全组里放行 3306 端口,这是最容易被忽略的一步。
很多人在使用 MySQL 容器时发现查询出来的时间比本地时间早 8 小时,因为容器时区默认是 UTC。解决方案有两个:在启动命令里加 -e TZ=Asia/Shanghai,或者像上面示例那样把宿主机的 /etc/localtime 挂载进容器。这两个办法都有效,我习惯两种一起用,保险。另外,如果你需要自定义 MySQL 配置文件,可以新建一个目录专门放 my.cnf,然后挂载到 /etc/mysql/conf.d/ 下,容器启动时会自动读取。
6.2 实战二:Redis 主从,两个容器演示服务名互通
Redis 主从复制的核心是让从节点连上主节点,并执行 replicaof 命令。用 Docker 来做这个实验非常直观,先创建网络,然后启动主节点和从节点:
docker network create redis-net docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7-alpine \ redis-server --appendonly yes docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7-alpine \ redis-server --replicaof redis-master 6379注意这里从节点启动命令里的 redis-server --replicaof redis-master 6379,主节点地址写的是容器名 redis-master,而不是 IP,这正是自定义网络的服务名解析机制在工作。老版本 Redis 用的参数是 --slaveof,新版本已经改为 --replicaof,如果你的 Redis 是 5.0 以下版本,需要换回旧参数。
启动后验证主从是否生效:进入从节点容器,执行 redis-cli info replication,看 role 是否显示 slave,master_link_status 是否为 up。如果显示 down,最常见的原因是主节点 Redis 开启了 protected-mode,或者两个节点不在同一个自定义网络里。把主节点的 bind 配置修改一下,或者全部放在 redis-net 里重来一遍就能解决。
6.3 实战三:用 Dockerfile 打包 Java 微服务镜像
开发完一个 Spring Boot 微服务,想用 Docker 部署,最标准的做法是写一个多阶段构建 Dockerfile。多阶段的好处是构建阶段用完整的 Maven 镜像,运行阶段只保留 JRE,镜像体积能缩小一半以上:
FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]这段 Dockerfile 里有几个关键点。第一,先 COPY pom.xml 再执行 dependency:go-offline,是为了利用 Docker 的层缓存:只要 pom.xml 不变,依赖下载层就不会重新执行,后续改代码重新构建时速度会快很多。第二,构建阶段和运行阶段用不同的基础镜像,运行镜像只保留 JRE,避免把整套编译工具带进生产环境。第三,EXPOSE 只是声明容器监听 8080,真正暴露到宿主机还需要 -p 参数。
在 IDEA 中打包镜像有两种常用方式。一种是用自带的 Docker 集成功能:配置好 Docker Desktop 的连接,右键项目中的 Dockerfile 选择 Build Image,即可生成镜像。另一种是通过 Maven 插件,在 pom.xml 里配置 dockerfile-maven-plugin 或 spotify 插件,在打包时自动触发 docker build。我个人推荐直接用 IDEA 的 Docker 集成,配置简单且界面直观,适合多数场景。镜像构建成功后,执行 docker run -p 8080:8080 my-api:latest 就能把微服务跑起来。
6.4 自建轻量代码托管:Gitea 与 GitLab 的取舍
如果你需要私有代码仓库,又不想在云上买昂贵的托管服务,用 Docker 自建一个是很划算的方案。GitLab 功能全、生态好,但对内存要求很高,官方建议至少 4G 内存,我实测 2G 内存的服务器跑起来会很吃力。如果只是个人或小团队用,我强烈建议用 Gitea,它极其轻量,512M 内存都能跑得动,而且操作习惯和 GitHub 很像。
用 Compose 部署 Gitea 的模板大致如下:
services: gitea: image: gitea/gitea:latest container_name: gitea restart: always ports: - "3000:3000" - "2222:22" volumes: - gitea_data:/data environment: - USER_UID=1000 - USER_GID=1000 volumes: gitea_data:部署完成后访问 http://服务器IP:3000,按界面提示完成初始化。有两个容易忽略的点:一个是 SSH 端口,默认 22 可能和宿主机冲突,所以映射成 2222,克隆地址里也要带上这个端口;另一个是 Gitea 的配置文件存放在数据卷里,升级镜像后配置不会丢,但如果你手动改过配置,建议先备份 /data 目录再升级。
7. 高频报错与排查实录
7.1 Got permission denied:docker 组权限问题
Linux 下刚装完 Docker 就执行 docker ps,大概率会看到 “Got permission denied while trying to connect to the Docker daemon socket”。原因是当前用户不在 docker 组里,docker 客户端请求 /var/run/docker.sock 时没有权限。临时解决办法是每条命令前加 sudo,一劳永逸的办法是把用户加入 docker 组:
sudo usermod -aG docker $USER newgrp docker执行完 newgrp docker 后,当前终端会立刻生效,重新登录也会自动生效。如果还是提示没权限,可能是当时 shell 会话还保留着旧的环境,直接退出重连即可。再次强调,docker 组权限几乎等同于 root,只建议在个人开发机上使用。
7.2 镜像拉取超时或慢,怎么处理
拉取镜像超时是非常常见的问题,尤其是第一个镜像。先检查网络能不能连通 Docker Hub,确认没有代理干扰。如果你在配置了 registry-mirrors 后依然很慢,可能需要检查 daemon.json 是否生效:docker info 的输出里会显示 Registry Mirrors 列表。另外,某些镜像可能因为体积巨大,比如 TensorFlow、Hadoop 生态镜像,即使网络正常也要拉很久,判断是不是卡住可以看传输进度的速度数值。
如果觉得公共镜像源不稳定,更推荐的做法是直接把镜像从一台能访问的机器上导出,再导入内网机器。docker save 和 docker load 两个命令分别负责导出和导入:
docker save -o nginx.tar nginx:1.25 docker load -i nginx.tar这种方式在内网环境非常实用,也能避免反复重试带来的时间浪费。
7.3 Docker Desktop 虚拟化检测失败的完整排查路径
这个报错在 Windows 上很常见,我提供一条完整的排查路径。
第一步,打开任务管理器,切到“性能”选项卡,看右下角“虚拟化”是否显示“已启用”。如果显示“已禁用”,重启电脑进 BIOS,找到 Intel VT-x(Intel 平台)或 SVM Mode(AMD 平台),开启后保存重启。
第二步,确认 Windows 功能里“虚拟机平台”和“适用于 Linux 的 Windows 子系统”都处于勾选状态。尤其是升级过 Windows 系统之后,这些功能可能意外被关闭。
第三步,确认 WSL2 已安装。执行 wsl --status 查看内核版本,如果提示未安装,先执行 wsl --update 再重试。
第四步,如果以上都正常,尝试在“启用或关闭 Windows 功能”里关闭 Hyper-V 相关的隔离项,Docker Desktop 改用 WSL2 backend。我见过一些 Windows 环境因为 Hyper-V 和 WSL2 并存导致虚拟化检测失败,关掉前者后问题就消失了。
7.4 容器日志把磁盘占满,怎么提前规避
容器日志默认无限增长,这是生产环境最常见的“无声杀手”。一个长期运行的服务,stdout 日志可能以每天几百 MB 的速度增长,一周就能撑爆磁盘。预防手段有两个层面:一是在 daemon.json 里统一配置 max-size 和 max-file,二是启动单个容器时用 --log-opt 参数指定:
docker run -d --name my-app \ --log-opt max-size=10m \ --log-opt max-file=3 \ my-app:latest如果磁盘已经被日志塞满,先找到占用最大的容器,然后清空对应的日志文件。注意不能用 rm 直接删除 json 日志文件,因为容器的日志文件句柄还开着,删除后空间不会立即释放,正确做法是执行 truncate -s 0 文件,比如:
truncate -s 0 /var/lib/docker/containers/<container-id>/<container-id>-json.log执行之后磁盘空间会立刻释放,容器正常运行不受影响。
7.5 docker 服务本身起不来,怎么定位
Linux 下执行 systemctl start docker 后,如果服务启动失败,常见原因有 daemon.json 格式错误、端口被占用、iptables 规则冲突、磁盘满等。排查顺序建议是:
systemctl status docker journalctl -u docker -n 100journalctl 的输出会给出具体失败原因。我看到比较多的是 JSON 解析失败和 iptables 相关错误。如果是 JSON 格式问题,按前面讲的方法校验 daemon.json;如果是 iptables 冲突,可以先执行 systemctl stop firewalld 再启动 Docker 试试,或者把 Docker 的 iptables 相关配置微调。这块问题比较吃经验,核心原则是先看日志再动手,不要盲目重启。
7.6 其他常见问题速查表
| 问题现象 | 常见原因 | 处理方式 |
|---|---|---|
| 容器启动后立即退出 | 前台进程不存在,如缺少启动命令 | docker logs 查看容器日志,确认启动命令是否写对 |
| 端口映射不生效 | 防火墙/安全组未放行 | 检查宿主机防火墙和云安全组入方向规则 |
| 容器内无法访问外网 | 宿主机 DNS 或网络策略问题 | 修改 daemon.json 里的 dns 配置,重启 Docker |
| docker exec 进不去 | 镜像内没有 bash | 改用 sh:docker exec -it 容器名 sh |
| 容器时间不对 | 时区默认 UTC | 启动时加 -e TZ=Asia/Shanghai 或挂载 localtime |
| 数据卷权限报错 | 宿主目录属主与容器用户不一致 | 调整目录属主或权限,适配容器内 uid |
最后分享一个我自己的使用习惯:每台新服务器装完 Docker,第一件事就是改 daemon.json 配置日志限制和镜像加速,然后立刻跑一次 hello-world 确认环境正常。项目上线前,我会把所有镜像版本钉死,数据卷目录提前规划好,Compose 文件里每个服务都写上 restart: always。这些习惯看着不起眼,但长期下来避免过太多线上故障。Docker 这东西,入门不难,真正值钱的是对细节的把控和对原理的理解,希望这篇内容能帮你少走一些我走过的弯路。