这段时间我把开发环境从 Docker Desktop 迁到了 WSL2 Ubuntu 里的原生 Docker Engine,迁移完第一感受是:命令行反馈明显快了一截,而且 docker 命令的行为和我在 Linux 服务器上完全一致,不用再担心“本地能跑、线上跑不起来”的差异问题。整个过程从 WSL2 环境准备到 Docker Engine 装好能正常拉镜像跑容器,差不多花了一个下午,中间踩了虚拟化未启用、GPG 密钥失效、镜像拉取超时这几个典型坑。这篇文章就把这套完整流程拆开讲清楚,每个操作背后的原因也一并说透,适合想在 Windows 下获得接近真实 Linux 服务器体验、又不想被 Docker Desktop 拖慢节奏的开发者参考。
1. 先在“运行环境”上做决策:为什么是 WSL2 + 原生 Docker Engine
很多 Windows 用户的第一反应是装 Docker Desktop,这个东西确实省事,装上之后托盘图标点一下就能用。但如果你在 Linux 服务器上部署过应用,很快会感觉到 Docker Desktop 那一层封装带来的隔阂:docker 命令的实际执行环境、网络模型、文件挂载行为,都和裸 Linux 上装的 Docker Engine 存在细微差别。另一个现实问题是资源占用,Docker Desktop 在 Windows 上会额外跑一个完整的 Linux 虚拟机,哪怕你已经在 WSL2 里配置了后端,它仍然需要维持自己的管理进程和图形界面,对于内存只有 16GB 的笔记本来说,压力不小。
1.1 Docker Desktop 与 WSL2 原生引擎的本质差异
Docker Desktop 在 Windows 上的工作方式是:先启动一个 Hyper-V 虚拟机,再在这个虚拟机里跑一个精简版 Linux,Docker Engine 实际上跑在这个精简系统里。你 Windows 侧的 docker CLI 通过命名管道或 TCP 连接到虚拟机内的 daemon。这套方案的最大好处是屏蔽了底层差异,装完就能用,但代价也在这里——中间隔了一层,很多网络和存储行为要经过虚拟化转换,性能有损耗,排查问题时也不够直观。
WSL2 本身也是一个轻量虚拟机,但它的内核启动速度和内存占用优化比传统 Hyper-V 虚拟机好得多。直接在 WSL2 的 Ubuntu 发行版里安装原生 Docker Engine,意味着 docker CLI 和 dockerd 都运行在同一个 Linux 内核之上,没有额外的管理层。我在实际使用中明显感觉到docker build时的文件监听和缓存命中率更稳定,尤其在使用 bind mount 做本地开发时,改动文件后的热更新延迟比原来低了不少。
1.2 一份方案对比表格与我的选择逻辑
| 对比维度 | Docker Desktop(WSL2 后端) | WSL2 原生 Docker Engine |
|---|---|---|
| 安装复杂度 | 低,图形界面引导 | 中,全部命令行操作 |
| 资源占用 | 较高,存在管理进程与 GUI | 较低,仅 dockerd 与容器进程 |
| 与 Linux 服务器行为一致性 | 较一致,但有大版本跟随周期 | 完全一致,可直接复用生产配置 |
| Docker Compose / Buildx | 自带并自动更新 | 通过组件包安装,版本可控 |
| 命令行体验 | 依赖 Windows 侧 CLI 转发 | 直接使用 Linux 侧 CLI,反馈更原生 |
| 适合场景 | 快速上手、需要图形管理界面 | 追求性能、贴近生产环境、脚本化部署 |
我的选择逻辑其实很简单:既然平时部署目标就是 Linux 服务器,那本地开发环境越接近目标环境越好。而且 WSL2 原生方式的 daemon.json、容器网络、存储驱动都能直接沿用生产环境的配置,省去了一层“本地没问题、上线就出幺蛾子”的中间环节。如果你只是偶尔跑一两个容器做实验,Docker Desktop 确实更方便;但如果你想长期在 Windows 下做容器化开发,我建议直接上原生 Engine。
2. WSL2 前置检查:虚拟化、版本与发行版这三道门槛
在装 Docker 之前,得先把 WSL2 本身跑起来。这里最容易卡住的就是启动时提示“WSL2 无法启动,因为此计算机上未启用虚拟化”,这个报错我在两台机器上遇到过,原因和处理方式不太一样,值得单独说一下。
2.1 未启用虚拟化的报错与排查路径
这个报错出现时,第一反应应该是进 BIOS 确认虚拟化是否开启。Intel 平台对应 Intel VT-x,AMD 平台对应 SVM Mode,不同主板叫法不一样,但基本都在 Advanced / CPU Configuration 这类菜单下。开启并保存重启后,虚拟化就位了。
还有一类情况是 BIOS 里开了虚拟化,但 Windows 侧缺少必要功能组件。需要确保“虚拟机平台”(Virtual Machine Platform)和“适用于 Linux 的 Windows 子系统”这两个可选功能都被启用。在管理员 PowerShell 里执行以下命令可以快速确认和启用:
# 检查 WSL 功能状态 Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform如果某个功能显示为 Disabled,就用管理员权限开启:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行结束后必须重启系统。重启后在 PowerShell 里执行wsl --status,如果看到默认版本是 2,说明基础环境就位了。如果仍然提示虚拟化问题,可以再检查一下是否有旧版本 Hyper-V 与第三方虚拟机软件冲突,比如部分版本 VMware Workstation 与 WSL2 的虚拟化层互斥,需要升级到支持嵌套虚拟化的版本。
2.2 用 wsl --install 快速建立 Ubuntu 环境
从较新的 Windows 10 版本开始,安装 WSL2 已经不需要手动下载安装包,直接执行:
wsl --install这个命令默认会安装 WSL2 内核和 Ubuntu 最新 LTS 发行版。如果你像我一样想指定 Ubuntu 22.04,可以先查看可用发行版列表:
wsl --list --online然后安装指定版本:
wsl --install -d Ubuntu-22.04这里有个实际经验:如果你之前已经停用或删除过某个发行版,重新安装可能会遇到分发目录残留的问题。稳妥的做法是先执行wsl --shutdown,然后在设置里删除旧的 Ubuntu 虚拟磁盘,或者用wsl --unregister Ubuntu-22.04清理干净再装。我自己遇到过注册表里残留导致新安装的 Ubuntu 启动后密码不一致的情况,折腾了一阵子。所以安装前如果系统里有过旧版 WSL,建议先彻底清理。
2.3 确认 WSL2 版本与 Ubuntu 发行版信息
装好后进入 Ubuntu 终端,先做两个确认:
# 确认 WSL 内核版本 uname -r # 确认 Ubuntu 版本 cat /etc/os-release如果uname -r输出的是以microsoft-standard-WSL2结尾的内核版本,说明当前确实运行在 WSL2 上。接下来更新软件源与基础包:
sudo apt update sudo apt upgrade -y这一步很重要。Docker Engine 的安装依赖ca-certificates、curl等基础工具,而 WSL2 的全新发行版往往只包含最小基础软件包。先把系统更新到最新,可以避免后续安装过程中出现依赖版本过旧导致的诡异问题。
3. 安装 Docker Engine:从 GPG 密钥到四个组件的完整命令
Docker Engine 在 Ubuntu 上的官方安装方式是通过 apt 仓库,而不是直接下载 deb 包。这样做的最大好处是后续可以用apt upgrade统一管理 Docker 组件升级,不用每次手动去拉二进制文件。这里我把安装过程拆成三步,每一步都有明确的执行目标和验证手段。
3.1 apt 仓库安装与 GPG 密钥的建立
首先安装必要的基础依赖:
sudo apt update sudo apt install -y ca-certificates curl然后创建 keyrings 目录并下载 Docker 官方的 GPG 密钥:
sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc这里的install -m 0755 -d是创建一个权限为 755 的目录,用于存放 GPG 密钥。指定这一权限的目的是让 apt 在验证仓库签名时能够正常读取密钥文件,同时又不会给普通用户不必要的写权限。chmod a+r则是确保所有用户都能读取密钥内容,属于 apt 官方文档明确要求的权限设置。
接下来添加 Docker 仓库。这里有一个容易踩的坑:不能直接写死 Ubuntu 版本代号,而应该动态读取当前系统的版本代号,因为不同 Ubuntu 版本的仓库路径不同:
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null$(dpkg --print-architecture)会输出当前 CPU 架构,比如 amd64 或 arm64,确保仓库地址与架构匹配。$(. /etc/os-release && echo "$VERSION_CODENAME")会读取系统版本代号,22.04 对应 jammy,24.04 对应 noble。这样即使在将来升级了 Ubuntu 版本,这条配置也能自动适配(前提是 Docker 官方仓库已支持新版本)。
3.2 docker-ce、containerd、buildx 与 compose 插件各自的职责
添加完仓库后执行:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin很多人第一次看到这么多包名会疑惑:一个 Docker 装这么多东西干什么?其实每个包都有明确分工:
docker-ce:Docker 社区版核心守护进程(dockerd),负责管理容器生命周期、镜像、网络和存储。docker-ce-cli:命令行客户端工具,就是你在终端里敲的 docker 命令。containerd.io:容器运行时,负责真正启动和停止容器进程。dockerd 把容器操作下发给 containerd 执行。没有它,docker 命令只会报错“Cannot connect to the Docker daemon”。docker-buildx-plugin:Buildx 插件,提供多平台镜像构建能力,比如在 x86 机器上构建 ARM 镜像。docker-compose-plugin:Compose 插件,让你可以直接用docker compose up启动多容器应用,不需要额外安装 python-docker-compose。
安装完成后先不要急着用,还需要确认 daemon 能否正常启动。在 WSL2 环境下,默认情况下 dockerd 不会自动运行,所以先手动启动并设为开机自启:
sudo service docker start sudo systemctl enable docker如果在较新版本且开启了 systemd 的环境里,systemctl enable docker会正常生效。如果提示 System has not been booted with systemd as init system (PID 1),说明当前还没开启 systemd,这篇文章后面专门有一节讲怎么处理。
3.3 首次验证:hello-world 背后的完整链路
安装配置完成后,跑一个最小的验证:
sudo docker run hello-world正常情况会输出一段介绍文字,说明 Docker 工作正常。这一步验证的不只是“能跑起来”,而是整条链路:CLI 发出请求 -> dockerd 接收 -> containerd 拉取镜像 -> 创建容器 -> 运行并输出日志 -> 退出。如果到这里一切顺利,说明最困难的部分已经完成了。
如果输出的是Cannot connect to the Docker daemon,先检查 dockerd 是否在运行:
sudo service docker status如果服务是停止状态,启动服务前先查看一下日志,找到失败原因:
sudo dockerd --debug我遇到过一次 dockerd 无法启动的情况,日志提示网络桥接创建失败。这是因为 WSL2 的网络模型与物理机不同,docker0 网桥的创建依赖 iptables 正常工作。排查后确认是 Ubuntu 24.04 里默认的 nftables 与 Docker 的 iptables 规则冲突,临时方案是在/etc/docker/daemon.json里显式指定 iptables=false,但更好的做法是检查iptables -L能否正常输出,并确保iptables-nft包已安装。
4. 两项收尾配置:镜像加速与免 sudo
Docker Engine 装好并能跑通 hello-world 后,接下来这两项配置是我认为“不做会很难受”的事情:一个是镜像加速,一个是把当前用户加入 docker 用户组。
4.1 daemon.json 中配置 registry mirror
在部分网络环境下,直接从 Docker Hub 拉取镜像经常出现超时或下载中断,尤其是比较大的镜像,比如 node、python、postgres 这类常用镜像,每个动辄几百 MB,下载中断一次就得从头再来。解决办法是在/etc/docker/daemon.json里配置 registry mirror(镜像加速地址)。
先创建配置文件:
sudo mkdir -p /etc/docker sudo nano /etc/docker/daemon.json写入如下内容:
{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com" ] }需要注意,公共镜像加速地址的可用性会随时间变化,有的已经停止服务,有的需要注册账号获取专属地址。比较可靠的做法是去云服务商的控制台申请一个专属加速地址,格式类似https://xxxx.mirror.aliyuncs.com,拿到后填入即可。我这里列出的两个地址仅供验证思路,实际使用时建议以各家官网最新公告为准。
配置完成后重启 Docker:
sudo systemctl daemon-reload sudo systemctl restart docker用以下命令验证加速是否生效:
docker info | grep -A 5 "Registry Mirrors"如果输出里列出了你配置的地址,说明已经生效。实测下来配置加速后拉取同样的 postgres:16 镜像,时间从十几分钟降到几分钟,差距很直观。
4.2 将当前用户加入 docker 用户组
每次敲 docker 命令都要加 sudo,这不仅是手累的问题,还会导致一些意想不到的权限边界。比如在使用 Dockerfile 时,某些 COPY 指令对宿主机文件的读取权限会受 sudo 影响,容易踩坑。
把当前用户加入 docker 用户组:
sudo usermod -aG docker $USER执行后必须注销当前终端会话或重启 WSL,让用户组变更生效:
exit wsl --shutdown重新进入 WSL 后,直接执行docker ps,不再需要 sudo 就说明配置成功了。
关于这个操作有一个安全提醒:docker 用户组的权限等价于 root,因为容器可以挂载宿主机目录并执行任意命令。所以只建议在单机开发环境里这么做,如果是多人共享服务器,需要审慎评估。
4.3 验证配置是否生效
重启 WSL 后,我建议一次性执行这几个命令,确认整体状态:
docker version docker info docker run --rm hello-worlddocker version会输出 Client 和 Server 两部分信息。如果只有 Client 没有 Server,说明 daemon 没起来。docker info会显示存储驱动、内核版本、镜像加速地址等关键信息。hello-world 再跑一遍,确认用户组配置没有破坏原有功能。
5. 让 Docker 服务开机自启:WSL2 的 systemd 与 Windows 侧联动
WSL2 早期的发行版没有 systemd,服务管理全靠 init.d 脚本,Docker 每次开机都需要手动service docker start,很烦人。好在新的 WSL2 内核已经支持 systemd,只需要在 /etc/wsl.conf 里打开开关。
5.1 在 wsl.conf 中开启 systemd
编辑 /etc/wsl.conf:
sudo nano /etc/wsl.conf写入:
[boot] systemd=true保存后在 Windows PowerShell 里执行:
wsl --shutdown重新进入 WSL 后,执行:
systemctl is-system-running如果输出running,说明 systemd 已正常接管。此时 Docker 服务会由 systemd 管理,开机自动启动:
systemctl enable docker systemctl start docker开启 systemd 之后有一个连带好处:之前很多依赖 systemd 的服务管理命令都可以正常使用了,比如systemctl status ssh,对于在 WSL 里跑开发服务的人来说非常方便。
5.2 从 Windows 侧直接调用 docker 命令
如果你已经习惯在 Windows PowerShell 里操作,不想每次切到 WSL 终端,可以安装 Windows 版的 Docker CLI。安装后通过环境变量把 DOCKER_HOST 指向 WSL2 里的 daemon,这样 PowerShell 里的 docker 命令和 WSL 里的 docker 命令操作的是同一个 daemon。
先确保 WSL2 里的 Docker daemon 监听 TCP 端口。在 /etc/docker/daemon.json 里增加 hosts 配置:
{ "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com" ], "hosts": [ "unix:///var/run/docker.sock", "tcp://0.0.0.0:2375" ] }重启 Docker 后在 Windows PowerShell 里设置环境变量:
$env:DOCKER_HOST = "tcp://localhost:2375" docker ps如果能看到 WSL2 里的容器列表,说明联通成功。可以把这行环境变量写入 PowerShell 的 profile,省得每次设置。
这里要强调一下安全性:2375 端口默认没有任何身份验证,监听 0.0.0.0 意味着局域网内其他机器也能访问。只建议在纯本机开发环境这么干,如果需要更安全的远程访问,应该配置 TLS 证书认证,或者干脆使用 SSH 转发。
5.3 在 Windows Terminal 中固化 WSL 默认终端
我个人的使用习惯是:把 WSL 设为 Windows Terminal 的默认配置文件,这样每次打开终端直接进入 Ubuntu,无需切换。然后在 Ubuntu 的~/.bashrc里加几个常用别名:
alias d='docker' alias dc='docker compose' alias dps='docker ps' alias dpsa='docker ps -a' alias dlog='docker logs -f --tail 100'这些小东西用惯了之后,工作效率提升明显。我还在~/.bashrc里加了一段自动判断 docker 是否在运行的逻辑,如果docker info失败就自动启动服务,省得每次开机都要手动敲。
6. 踩坑实录:安装与使用过程中的六个典型问题
任何环境搭建过程中都会遇到各种报错,把我在 WSL2 上安装和使用 Docker Engine 过程中遇到过的典型问题整理出来,按排查思路写清楚,希望你能少走弯路。
6.1 安装 docker 的 GPG 密钥无效或下载失败
执行apt update时如果提示The following signatures couldn't be verified because the public key is not available,多半是 GPG 密钥没有正确下载。常见原因有两个:一是下载时网络不通导致文件内容为空,二是文件名或路径与 source list 里的 signed-by 不一致。
排查方式很直接:
ls -l /etc/apt/keyrings/docker.asc file /etc/apt/keyrings/docker.asc如果文件大小是 0 或者内容不是 ASCII 文本,删除后重新下载。如果网络下载失败,可以考虑换个时间段重试,或者使用代理镜像源。注意不要把 source list 里的 signed-by 路径写错,我之前因为把路径写成了相对路径导致 apt 找不到密钥,报错信息完全牛头不对马嘴。
6.2 apt update 时签名报错与 Ubuntu 版本的坑
添加 Docker 仓库后,apt update报错The repository ... does not have a Release file或者 404 错误,大概率是仓库地址里的版本代号写错了。我在升级系统时遇到过这样的问题:系统从 22.04 升级到 24.04 后,旧的 Docker 仓库源还是 jammy,但系统实际是 noble,apt 找不到对应的包索引。
解决办法是重新生成 source list 中的版本代号:
sudo rm /etc/apt/sources.list.d/docker.list # 用前文的方式重新添加仓库,确保 $(. /etc/os-release && echo "$VERSION_CODENAME") 输出的是当前版本所以在添加仓库时务必使用动态获取版本代号的写法,而不是把 jammy 或 noble 写死。
6.3 WSL2 内存占用过高导致构建卡死
WSL2 默认会使用宿主机最多 50% 的内存,对于 16GB 内存的机器来说就是 8GB。执行大型镜像构建或同时跑多个容器时,内存经常被打满,构建过程直接 OOM。我一开始以为是 Docker 的问题,配置了 swap 也没用,后来才发现是 WSL2 的全局内存限制。
解决办法是在 Windows 用户目录下创建.wslconfig文件:
[wsl2] memory=8GB processors=4 swap=4GB保存后执行wsl --shutdown再重新进入。这里有个小细节:swap配置只在 WSL2 内核里生效,如果你在 daemon.json 里也配置了 Docker 层面的 swap,需要注意两层限制取最小值。实测配置.wslconfig后,构建过程稳定很多,不会再因为内存不足而中断。
6.4 containerd 与 docker-ce 版本不匹配
如果执行docker run时提示failed to create containerd task: OCI runtime create failed,很大概率是 containerd 和 docker-ce 版本之间存在不兼容。这个坑在添加 Docker 官方仓库时比较容易遇到,因为apt install docker-ce只安装当前仓库里最新的 docker-ce,但 containerd.io 可能是旧版本缓存在本地。
解决办法很简单:显式升级全部 Docker 相关组件到统一版本:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin如果依然报错,尝试清除缓存后重新安装:
sudo apt autoremove sudo rm -rf /var/lib/docker sudo apt install --reinstall docker-ce containerd.io注意最后一步会删除所有本地镜像和容器数据,操作前确认没有需要保留的数据。
6.5 容器端口在 Windows 侧无法访问
在 WSL2 里启动了一个端口为 8080 的容器,Windows 浏览器却访问不到。这个问题的根源在于 WSL2 的 NAT 网络模式:从 Windows 访问 WSL2 内的服务通常是通过 localhost 转发,不一定总是立即生效。
我遇到的情况是 WSL2 IP 变了,导致旧会话里的连接目标失效。解决思路:
# 在 WSL 里查看当前 IP hostname -I # 在 Windows PowerShell 里查看是否能连上 Test-NetConnection -ComputerName <WSL_IP> -Port 8080如果 IP 通了,把浏览器地址改成 WSL IP 访问。如果 localhost 能通而 IP 不能通,检查 Docker 容器的端口映射参数,确认没有遗漏-p 8080:8080。另外,Windows 防火墙有时候会拦截对 WSL 虚拟网卡的访问,需要检查入站规则。如果希望固定 IP,可以在 .wslconfig 里配置networkingMode=mirrored,让 WSL2 直接共享宿主机的网络接口,这样 localhost 转发更稳定。
6.6 WSL2 文件挂载性能与代码目录选择
把项目代码放在 Windows 文件系统里,然后通过/mnt/c/...路径挂载进容器,开发时保存代码会有明显的延迟,大型项目尤其明显。这是 WSL2 跨文件系统读写(9P 协议)的固有开销,不是 Docker 能解决的。
我的经验是:把项目代码放在 WSL2 内部文件系统,也就是~/projects下,然后用 Windows 侧的资源管理器通过\\wsl$\Ubuntu-22.04\home\<user>\projects路径访问。这样 Docker build 和容器挂载都在 Linux 文件系统内完成,性能提升非常明显。实测在同一个项目上,放在 WSL 内部后的 Docker build 时间比放在 /mnt/c 下快了三倍以上。
如果你的项目仓库已经 clone 在 Windows 侧,可以用git clone重新克隆一份到 WSL 内,或者用cp -r复制过去,之后 git 操作在 WSL 内进行。数据一致性方面,WSL2 的虚拟磁盘安全性足以应对日常开发,不用担心文件丢失。
最后分享一个我这段时间用得最顺手的组合:VS Code 的 WSL 扩展 + Windows Terminal + WSL2 原生 Docker Engine。在 WSL 里打开 VS Code 会自动进入远程开发模式,终端直接敲 docker 命令,容器管理、代码编辑、调试全部在同一个窗口完成,这套组合目前是我的 Windows 开发主力方案。如果你也在 Windows 下做容器化开发,希望这篇文章能帮你顺利搭好环境,少踩几个坑。