Docker-Desktop-WSL2实战指南-Windows从安装配置到项目容器化

Docker Desktop + WSL2 实战指南:Windows 从安装配置到项目容器化

专栏:工具|适用方向:Windows 开发、Python Web、可复现项目环境

官方资料核对日期:2026-10-01。本文使用 Docker Desktop 的 WSL2 后端、Linux 容器和 docker compose 插件命令。安装要求、许可政策和界面会随版本变化,涉及这些内容时请同时检查文末官方链接。Windows 容器不在本文实践范围内。

目录

  • [Docker Desktop + WSL2 实战指南:Windows 从安装配置到项目容器化](#Docker Desktop + WSL2 实战指南:Windows 从安装配置到项目容器化)
    • [Windows 怎么使用 Docker?先区分执行终端](#Windows 怎么使用 Docker?先区分执行终端)
    • [1. Docker 到底解决什么问题?容器和镜像有什么区别?](#1. Docker 到底解决什么问题?容器和镜像有什么区别?)
    • [2. Docker 和 Docker Desktop 有什么区别?Docker 和 WSL2 是什么关系?](#2. Docker 和 Docker Desktop 有什么区别?Docker 和 WSL2 是什么关系?)
      • [2.1 各组件分别负责什么](#2.1 各组件分别负责什么)
      • [2.2 一个 `docker run` 经历了什么](#2.2 一个 docker run 经历了什么)
    • [3. 安装前如何检查 Windows 和 WSL2?](#3. 安装前如何检查 Windows 和 WSL2?)
      • [3.1 先看系统要求,再安装](#3.1 先看系统要求,再安装)
      • [3.2 没有 WSL:安装;版本旧:更新](#3.2 没有 WSL:安装;版本旧:更新)
      • [3.3 默认版本与已有发行版转换不同](#3.3 默认版本与已有发行版转换不同)
    • [4. Docker Desktop 怎么安装和配置?](#4. Docker Desktop 怎么安装和配置?)
      • [4.1 下载、安装、首次启动](#4.1 下载、安装、首次启动)
      • [4.2 检查后端与 WSL Integration](#4.2 检查后端与 WSL Integration)
    • [5. 怎样验证 Docker 真正安装成功?](#5. 怎样验证 Docker 真正安装成功?)
    • [6. 第一个有意义的容器:用 nginx 走完生命周期](#6. 第一个有意义的容器:用 nginx 走完生命周期)
    • [7. Docker 镜像怎么管理?latest 是最新版本吗?](#7. Docker 镜像怎么管理?latest 是最新版本吗?)
    • [8. Dockerfile 怎么写?先容器化一个最小 Web 项目](#8. Dockerfile 怎么写?先容器化一个最小 Web 项目)
    • [9. docker build 做了什么?最后一个点是什么意思?](#9. docker build 做了什么?最后一个点是什么意思?)
    • [10. .dockerignore 怎么写?它和 .gitignore 有什么不同?](#10. .dockerignore 怎么写?它和 .gitignore 有什么不同?)
    • [11. Docker 数据怎么持久化?容器重启后数据就会没吗?](#11. Docker 数据怎么持久化?容器重启后数据就会没吗?)
      • [11.1 用一个小实验理解 named volume](#11.1 用一个小实验理解 named volume)
      • [11.2 Bind mount:由你决定宿主路径](#11.2 Bind mount:由你决定宿主路径)
    • [12. Windows + WSL2 的项目文件应该放在哪里?](#12. Windows + WSL2 的项目文件应该放在哪里?)
      • [12.1 三种路径不要混用](#12.1 三种路径不要混用)
      • [12.2 大量小文件时,跨文件系统可能成为瓶颈](#12.2 大量小文件时,跨文件系统可能成为瓶颈)
      • [12.3 换行符、执行权限、文件名也会影响运行](#12.3 换行符、执行权限、文件名也会影响运行)
    • [13. Docker 端口映射怎么理解?](#13. Docker 端口映射怎么理解?)
    • [14. Docker 网络基础:两个容器怎样通信?](#14. Docker 网络基础:两个容器怎样通信?)
    • [15. Docker Compose 怎么用?为什么不继续堆 docker run?](#15. Docker Compose 怎么用?为什么不继续堆 docker run?)
      • [depends_on 与健康检查的边界](#depends_on 与健康检查的边界)
    • [16. 完整实践:Flask + Redis 项目容器化](#16. 完整实践:Flask + Redis 项目容器化)
      • [16.1 项目目录](#16.1 项目目录)
      • [16.2 完整应用代码](#16.2 完整应用代码)
      • [16.3 依赖文件](#16.3 依赖文件)
      • [16.4 Dockerfile:服务进程、日志与非 root 用户](#16.4 Dockerfile:服务进程、日志与非 root 用户)
      • [16.5 完整 .dockerignore](#16.5 完整 .dockerignore)
      • [16.6 完整 Compose 文件](#16.6 完整 Compose 文件)
      • [16.7 配置检查、构建与启动](#16.7 配置检查、构建与启动)
      • [16.8 HTTP 与数据链路验收](#16.8 HTTP 与数据链路验收)
      • [16.9 依赖中断与恢复验收](#16.9 依赖中断与恢复验收)
      • [16.10 删除并重建:验证数据持久化](#16.10 删除并重建:验证数据持久化)
      • [16.11 停止与清理:保留数据和删除数据分开](#16.11 停止与清理:保留数据和删除数据分开)
      • [16.12 如何迁移到 Python、C/C++、Web 或 AI 项目](#16.12 如何迁移到 Python、C/C++、Web 或 AI 项目)
    • [17. Docker Desktop 占用内存太高怎么办?](#17. Docker Desktop 占用内存太高怎么办?)
      • [17.1 先区分容器、WSL 进程和缓存](#17.1 先区分容器、WSL 进程和缓存)
      • [17.2 .wslconfig 管的是 WSL2,不是某个容器](#17.2 .wslconfig 管的是 WSL2,不是某个容器)
      • [17.3 自动内存回收和 Resource Saver 有版本差异](#17.3 自动内存回收和 Resource Saver 有版本差异)
    • [18. Docker 镜像下载慢或失败,应该怎样排查?](#18. Docker 镜像下载慢或失败,应该怎样排查?)
      • [18.1 第一步:确认 Engine 和目标地址](#18.1 第一步:确认 Engine 和目标地址)
      • [18.2 第二步:检查 Windows DNS 和 HTTPS](#18.2 第二步:检查 Windows DNS 和 HTTPS)
      • [18.3 第三步:确认代理配置在哪一层](#18.3 第三步:确认代理配置在哪一层)
      • [18.4 第四步:认证、Registry 与镜像加速服务](#18.4 第四步:认证、Registry 与镜像加速服务)
    • [19. Docker Desktop 启动不了怎么办?常见故障排查表](#19. Docker Desktop 启动不了怎么办?常见故障排查表)
      • [19.1 CLI 连接错 Engine 时,重装往往无效](#19.1 CLI 连接错 Engine 时,重装往往无效)
      • [19.2 Starting 状态:先保留诊断,再考虑重置](#19.2 Starting 状态:先保留诊断,再考虑重置)
      • [19.3 磁盘清理要按对象进行](#19.3 磁盘清理要按对象进行)
    • [20. Docker 常用命令速查:只保留开发高频操作](#20. Docker 常用命令速查:只保留开发高频操作)
    • [21. Docker 不适合解决什么问题?](#21. Docker 不适合解决什么问题?)
    • [22. 从安装到项目:形成可以复用的工作流](#22. 从安装到项目:形成可以复用的工作流)
    • 官方资料与更新入口
    • 配图来源与署名

"在我的电脑上可以运行,为什么换一台电脑就报错?"

代码没变,环境却换了:先别急着改业务逻辑。

这个问题往往不是代码突然变了,而是 Python 版本、系统库、环境变量、数据库和依赖组合变了。Docker 的价值,是把项目需要的用户空间环境、依赖和启动方式变成可以构建、分发、检查的交付物。

本文从尚未配置 Docker 的 Windows 电脑出发,最终完成一个 Flask + Redis 持久化计数器。过程中会分别回答:Docker Desktop 怎么安装、Docker 与 WSL2 是什么关系、Dockerfile 怎么写、端口如何转发、数据如何保存,以及启动或下载失败时如何定位。

Windows 怎么使用 Docker?先区分执行终端

全文有两类终端:

  • Windows PowerShell :检查 Windows、安装和管理 WSL、检查 Windows 端口、编辑 .wslconfig。
  • WSL 的 Bash:保存项目、操作 Linux 路径、执行项目构建和 Compose 实践。

不涉及路径和 Shell 特性的 docker 命令,两边通常都能运行,前提是它们连接到预期的 Docker Engine。带 $(pwd) 的挂载命令只在本文注明的 Bash 中执行。PowerShell 使用 curl.exe,避免旧版 PowerShell 将 curl 解释成其他命令别名。

文章配套项目分为 01-basic-web 和 02-flask-redis,分别对应单容器入门和完整实践。正文也完整给出必需文件,读者可以直接按目录创建。

仓库配套入口:源码与运行说明、代码包、验证记录。

验证范围说明 :配套应用进行了 Linux 环境下的 HTTP、真实 Redis 读写、依赖中断恢复和 AOF 重启恢复验证;本地验证使用 Redis 6.2.14,项目镜像配置为 redis:7.4,两者不能视为同一环境。Compose 文件通过官方规范的 Schema 检查。验证环境没有 Docker Engine 和 Windows/WSL,因此本文不把这些检查描述为 Windows 安装实测或容器端验收。第 16 节提供了读者可执行的完整容器验收流程。

1. Docker 到底解决什么问题?容器和镜像有什么区别?

假设一个 Python Web 项目需要 Python 3.12、Flask、Redis,以及系统中的某些动态库。只发给别人一个 app.py,并不能交付运行条件;只发 requirements.txt,也不能固定 Python 解释器和操作系统依赖。

Docker 可以把这些条件写进构建流程,并生成可运行的镜像。但数据库内容、密钥、运行时配置、CPU 架构和宿主机内核仍需要单独考虑,"用了 Docker"不等于所有环境差异自动消失。

四个概念要分清:

对象 技术含义 在开发流程中的作用
Image,镜像 包含文件系统层、启动配置等内容的只读模板 分发应用及其用户空间环境;镜像本身不是运行中的程序
Container,容器 从镜像创建的运行实例,有进程、网络配置和可写层 实际执行程序;同一镜像可以创建多个容器
Dockerfile 描述如何构建镜像的指令文件 将基础环境、安装依赖、复制代码和默认启动方式写成可重复执行的流程
Registry,镜像仓库服务 保存和分发镜像的服务,如 Docker Hub 或组织私有 Registry 让其他电脑或服务器获取构建结果

镜像文件系统是只读的;容器在其上增加自己的可写层。因此,一个容器修改 /app/config.json,不会直接改掉原镜像,也不会自动同步到另一个容器。
#mermaid-svg-FwMkGNF3xNRvVXBT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FwMkGNF3xNRvVXBT .error-icon{fill:#552222;}#mermaid-svg-FwMkGNF3xNRvVXBT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FwMkGNF3xNRvVXBT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FwMkGNF3xNRvVXBT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FwMkGNF3xNRvVXBT .marker.cross{stroke:#333333;}#mermaid-svg-FwMkGNF3xNRvVXBT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FwMkGNF3xNRvVXBT p{margin:0;}#mermaid-svg-FwMkGNF3xNRvVXBT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT .cluster-label text{fill:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT .cluster-label span{color:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT .cluster-label span p{background-color:transparent;}#mermaid-svg-FwMkGNF3xNRvVXBT .label text,#mermaid-svg-FwMkGNF3xNRvVXBT span{fill:#333;color:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT .node rect,#mermaid-svg-FwMkGNF3xNRvVXBT .node circle,#mermaid-svg-FwMkGNF3xNRvVXBT .node ellipse,#mermaid-svg-FwMkGNF3xNRvVXBT .node polygon,#mermaid-svg-FwMkGNF3xNRvVXBT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FwMkGNF3xNRvVXBT .rough-node .label text,#mermaid-svg-FwMkGNF3xNRvVXBT .node .label text,#mermaid-svg-FwMkGNF3xNRvVXBT .image-shape .label,#mermaid-svg-FwMkGNF3xNRvVXBT .icon-shape .label{text-anchor:middle;}#mermaid-svg-FwMkGNF3xNRvVXBT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FwMkGNF3xNRvVXBT .rough-node .label,#mermaid-svg-FwMkGNF3xNRvVXBT .node .label,#mermaid-svg-FwMkGNF3xNRvVXBT .image-shape .label,#mermaid-svg-FwMkGNF3xNRvVXBT .icon-shape .label{text-align:center;}#mermaid-svg-FwMkGNF3xNRvVXBT .node.clickable{cursor:pointer;}#mermaid-svg-FwMkGNF3xNRvVXBT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FwMkGNF3xNRvVXBT .arrowheadPath{fill:#333333;}#mermaid-svg-FwMkGNF3xNRvVXBT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FwMkGNF3xNRvVXBT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FwMkGNF3xNRvVXBT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FwMkGNF3xNRvVXBT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FwMkGNF3xNRvVXBT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FwMkGNF3xNRvVXBT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FwMkGNF3xNRvVXBT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FwMkGNF3xNRvVXBT .cluster text{fill:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT .cluster span{color:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FwMkGNF3xNRvVXBT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FwMkGNF3xNRvVXBT rect.text{fill:none;stroke-width:0;}#mermaid-svg-FwMkGNF3xNRvVXBT .icon-shape,#mermaid-svg-FwMkGNF3xNRvVXBT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FwMkGNF3xNRvVXBT .icon-shape p,#mermaid-svg-FwMkGNF3xNRvVXBT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FwMkGNF3xNRvVXBT .icon-shape .label rect,#mermaid-svg-FwMkGNF3xNRvVXBT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FwMkGNF3xNRvVXBT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FwMkGNF3xNRvVXBT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FwMkGNF3xNRvVXBT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} docker build
docker run
push 与 pull
Dockerfile 与构建上下文
Image
Container
Registry

构建与运行是两个阶段:RUN pip install ... 在构建时安装依赖;CMD ... 设置容器启动时默认执行的程序。把两个阶段混在一起,是 Dockerfile 难以维护的常见原因。

2. Docker 和 Docker Desktop 有什么区别?Docker 和 WSL2 是什么关系?

2.1 各组件分别负责什么

Docker CLI 是你输入的 docker 命令。它是客户端,负责解析参数并向 Engine API 发出请求。

Docker Engine 负责镜像、容器、网络和存储的管理。Linux 上常见的后台进程是 dockerd,它再与容器运行时协作,创建并管理容器进程。

Docker Desktop 是桌面开发产品,集成 CLI、Compose、后台 Engine、图形界面,以及 Windows 与 Linux 运行环境之间的文件和网络连接。安装 Desktop 与只安装 CLI,得到的能力不同。

WSL2 使用真实 Linux 内核和受管理的轻量虚拟机,让 Windows 可以运行 Linux 环境。WSL1 的实现方式不同,不是本文 Linux 容器后端需要的 WSL2 环境。

Linux 容器 运行 Linux 用户空间程序,依赖 Linux 内核提供进程隔离、资源限制和系统调用。Windows 内核不能直接执行所有 Linux 系统调用,所以 Docker Desktop 需要 Linux 后端来运行这类容器。

来源:Docker 官方概览。这是通用架构图;本文的 Docker Host 位于 Desktop 管理的 WSL2 Linux 后端。图片仅由 WebP 转为 PNG,内容未改。

2.2 一个 docker run 经历了什么

#mermaid-svg-KtHOxN1uiNbULpYD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KtHOxN1uiNbULpYD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KtHOxN1uiNbULpYD .error-icon{fill:#552222;}#mermaid-svg-KtHOxN1uiNbULpYD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KtHOxN1uiNbULpYD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KtHOxN1uiNbULpYD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KtHOxN1uiNbULpYD .marker.cross{stroke:#333333;}#mermaid-svg-KtHOxN1uiNbULpYD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KtHOxN1uiNbULpYD p{margin:0;}#mermaid-svg-KtHOxN1uiNbULpYD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KtHOxN1uiNbULpYD .cluster-label text{fill:#333;}#mermaid-svg-KtHOxN1uiNbULpYD .cluster-label span{color:#333;}#mermaid-svg-KtHOxN1uiNbULpYD .cluster-label span p{background-color:transparent;}#mermaid-svg-KtHOxN1uiNbULpYD .label text,#mermaid-svg-KtHOxN1uiNbULpYD span{fill:#333;color:#333;}#mermaid-svg-KtHOxN1uiNbULpYD .node rect,#mermaid-svg-KtHOxN1uiNbULpYD .node circle,#mermaid-svg-KtHOxN1uiNbULpYD .node ellipse,#mermaid-svg-KtHOxN1uiNbULpYD .node polygon,#mermaid-svg-KtHOxN1uiNbULpYD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KtHOxN1uiNbULpYD .rough-node .label text,#mermaid-svg-KtHOxN1uiNbULpYD .node .label text,#mermaid-svg-KtHOxN1uiNbULpYD .image-shape .label,#mermaid-svg-KtHOxN1uiNbULpYD .icon-shape .label{text-anchor:middle;}#mermaid-svg-KtHOxN1uiNbULpYD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KtHOxN1uiNbULpYD .rough-node .label,#mermaid-svg-KtHOxN1uiNbULpYD .node .label,#mermaid-svg-KtHOxN1uiNbULpYD .image-shape .label,#mermaid-svg-KtHOxN1uiNbULpYD .icon-shape .label{text-align:center;}#mermaid-svg-KtHOxN1uiNbULpYD .node.clickable{cursor:pointer;}#mermaid-svg-KtHOxN1uiNbULpYD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KtHOxN1uiNbULpYD .arrowheadPath{fill:#333333;}#mermaid-svg-KtHOxN1uiNbULpYD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KtHOxN1uiNbULpYD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KtHOxN1uiNbULpYD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KtHOxN1uiNbULpYD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KtHOxN1uiNbULpYD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KtHOxN1uiNbULpYD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KtHOxN1uiNbULpYD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KtHOxN1uiNbULpYD .cluster text{fill:#333;}#mermaid-svg-KtHOxN1uiNbULpYD .cluster span{color:#333;}#mermaid-svg-KtHOxN1uiNbULpYD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KtHOxN1uiNbULpYD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KtHOxN1uiNbULpYD rect.text{fill:none;stroke-width:0;}#mermaid-svg-KtHOxN1uiNbULpYD .icon-shape,#mermaid-svg-KtHOxN1uiNbULpYD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KtHOxN1uiNbULpYD .icon-shape p,#mermaid-svg-KtHOxN1uiNbULpYD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KtHOxN1uiNbULpYD .icon-shape .label rect,#mermaid-svg-KtHOxN1uiNbULpYD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KtHOxN1uiNbULpYD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KtHOxN1uiNbULpYD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KtHOxN1uiNbULpYD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WSL2 Linux 后端
Windows PowerShell 中的 Docker CLI
Docker Desktop 管理的 Engine API
WSL 发行版中的 Docker CLI
容器运行时
Linux 容器进程

CLI 请求 Engine 创建容器;Engine 检查本地是否有镜像,必要时拉取镜像,然后准备文件系统、网络、挂载和启动配置,再通过运行时启动进程。-d 只是让 CLI 不持续附着在输出上,容器里的主进程仍必须保持运行。

这里的关键不是把 WSL2 当作 Engine 后面的另一个网络节点,而是理解:Engine 和 Linux 容器进程运行在 Desktop 管理的 Linux 后端中。

Docker Desktop 使用自己的 docker-desktop 环境。你安装的 Ubuntu 是工作环境,可通过 WSL Integration 使用同一个 Desktop Engine;它不意味着还要在 Ubuntu 里另装一套 dockerd。

先安装 Ubuntu 不是 Docker Desktop 的必要条件。 如果只在 PowerShell 中使用 Docker,可以不安装个人 Linux 发行版。但本文后续把项目放在 WSL 的 Linux 文件系统中,建议配置一个 Ubuntu 等发行版,获得一致的 Linux 工具和路径。

如果你之前已经在 WSL 内独立安装 Docker Engine,应先确认正在连接哪个 Engine、旧数据存放在哪里,再决定如何迁移。不要为了消除冲突直接删除旧镜像和数据。

参考:Docker Desktop WSL2 后端、Docker daemon。

3. 安装前如何检查 Windows 和 WSL2?

3.1 先看系统要求,再安装

在 Windows PowerShell 执行:

powershell 复制代码
winver
wsl --status
wsl --version
wsl -l -v
命令 检查内容 如何解读
winver 打开 Windows 版本与构建号窗口 用系统版本、版本类型和构建号对照官方要求
wsl --status 默认发行版、默认 WSL 类型等总体配置 默认版本是 2,不代表所有已有发行版都是 WSL2
wsl --version WSL 软件、内核等组件版本 这里的 WSL 软件版本,与发行版列表中的 VERSION 2 不是一个概念
wsl -l -v 列出发行版、运行状态、WSL1/WSL2 类型 关注你要使用的 Ubuntu 等发行版是否为 2

如果 wsl --version 不被识别,可能正在使用较旧的 WSL 实现;应按微软安装与更新文档处理,而不是认定"能打开 Ubuntu 就满足要求"。

截至本文核对日期,Docker Windows 安装文档列出的 WSL2 要求包括 WSL 2.1.5 或更高版本 、64 位且支持 SLAT 的处理器、启用硬件虚拟化、至少 8GB 系统内存,以及启用 Windows 的 Server 服务(LanmanServer,不是指安装 Windows Server 系统)并设置为自动启动。实际下载前仍需检查官方最新要求。安装要求

需要检查该服务时,在 PowerShell 执行:

powershell 复制代码
Get-Service LanmanServer | Select-Object Name, Status, StartType

它只读取服务名称、运行状态和启动类型。受组织策略管理或服务未启用时,应根据官方要求和本机权限处理,不要直接批量修改所有服务。

任务管理器的"性能 → CPU"可以帮助查看虚拟化是否已启用。如果未启用,需要根据设备厂商说明在 BIOS/UEFI 中开启;菜单名称可能是 Intel VT-x、AMD-V 或其他名称。不要直接套用另一台电脑的 BIOS 路径。

来源:Docker Docs 的 Windows 虚拟化检查素材,原图未修改。图中是较早的 Windows 界面,只需关注 Virtualization / Enabled;图中 CPU 和占用数值不是本文测试数据,也不用于判断 Windows 11 硬件资格。

Windows 10 用户要特别区分技术最低构建号和支持生命周期。 官方安装页面可能仍列出 Windows 10 22H2 等最低条件,同时注明只支持仍在微软服务周期内的系统。普通 Windows 10 Home/Pro 已于 2025-10-14 结束支持;LTSC、ESU 等情况还需分别确认,不能仅凭构建号推断 Docker 支持资格。对于新配置的开发机,优先使用仍受支持的 Windows 11 版本。微软 Windows 10 生命周期

本文针对 Linux 容器。家庭版不能由此推导为支持 Windows 容器;不同架构和版本的安装要求也不同。ARM 设备应选择官方对应安装包,并检查架构支持状态。

3.2 没有 WSL:安装;版本旧:更新

在管理员 PowerShell 中,尚未安装 WSL 时执行:

powershell 复制代码
wsl --install

它启用所需组件并安装 WSL,默认情况下还会安装默认 Linux 发行版。按提示重启后,完成发行版首次初始化和 Linux 用户创建。企业受管设备、较旧 Windows 或离线环境可能需要官方文档中的其他步骤。

来源:Docker Docs 的 WSL2 Windows 功能示意图,原图未修改。红圈显示组件名称,不要求照抄其他复选框;本节优先使用微软的 WSL 安装命令,手动步骤以当前官方文档为准。

已经安装 WSL,但组件版本不满足要求时执行:

powershell 复制代码
wsl --update

它更新 WSL 组件;如果命令失败,应检查 Windows 更新渠道、组织策略和网络,不要反复重装 Docker 来解决 WSL 本身的问题。

3.3 默认版本与已有发行版转换不同

powershell 复制代码
wsl --set-default-version 2

它使之后安装的发行版默认使用 WSL2,不会自动把已有 WSL1 发行版转换为 WSL2。

如果 wsl -l -v 显示目标发行版是 1,先备份重要文件,再执行:

powershell 复制代码
wsl --set-version Ubuntu 2

Ubuntu 必须替换为列表中的准确名称。该命令转换现有发行版;转换时间和所需磁盘空间取决于已有数据,不能随意中断。

参考:微软 WSL 安装、WSL 基本命令。

4. Docker Desktop 怎么安装和配置?

4.1 下载、安装、首次启动

  1. 从 Docker 官方 Windows 安装页面 下载适合 CPU 架构的安装包。
  2. 阅读当前 Docker Desktop 订阅协议。截至核对日期,免费范围包括个人、教育、非商业开源项目,以及同时满足员工少于 250 人、年收入少于 1000 万美元的小型企业。大型组织的专业使用、政府实体和超出免费范围的商业使用需要付费订阅;实际使用仍应按协议核对,不能把 Engine 的开源许可直接套到 Desktop。订阅与许可说明
  3. 按安装器提示选择安装方式。如果出现后端选项,选择 WSL2。只有一个可用后端时,安装器可能自动选择。
  4. 如安装器或 Windows 要求重启,先重启。
  5. 从开始菜单打开 Docker Desktop,完成首次提示,等待 Engine 启动。

安装是否需要管理员权限取决于安装方式与组件,不能笼统说"所有版本都必须管理员安装"。当前安装器提供按用户和所有用户等方式;以实际安装器为准。

此阶段不需要给所有人都添加 docker-users 组,也不需要开放不加密的远程 Docker API。是否涉及特权功能,应按官方安装模式说明判断。

4.2 检查后端与 WSL Integration

在 Docker Desktop 的 Settings 中检查:

  • General :确认使用 WSL2 后端;某些版本会显示 Use the WSL 2 based engine,默认启用时也可能不显示同样的开关。
  • Resources → WSL Integration :启用你实际使用的 WSL2 发行版,例如 Ubuntu。保存并应用后,在该发行版终端验证 docker version。
  • 容器模式:本文需要 Linux containers。处在 Windows containers 模式时,WSL Integration 可能不可见。
  • Proxies:网络必须经代理时在这里配置,路径和选项可能随版本变化。

启用 Integration 的目的,是让 Ubuntu 中的 CLI 方便地连接 Desktop 管理的 Engine。它不是在 Ubuntu 中安装第二套 Engine 的操作。

刚开始不要随意修改 Docker Engine JSON、实验性选项、磁盘映像路径、WSL 网络模式,也不要主动设置远程 TCP daemon 地址。先验证默认工作链路,再针对具体问题调整配置。

不同安装历史下,wsl -l -v 中出现的 Desktop 内部发行版可能不同。尤其不要把是否存在 docker-desktop-data 当作唯一成功标准;官方发布说明已记录不再需要该辅助发行版的 WSL2 配置方式。Desktop 发布说明

参考:WSL 集成、Desktop 设置。

5. 怎样验证 Docker 真正安装成功?

按以下顺序执行,不要只看版本字符串:

bash 复制代码
docker --version
docker version
docker info
docker run --rm hello-world
docker compose version
命令 验证对象 失败时优先看什么
docker --version 当前终端能否找到 Docker CLI PATH、终端是否重新打开、安装是否完成
docker version Client 与 Server 两端版本和连接情况 只有 Client、Server 报错时,检查 Desktop、context 和连接环境变量
docker info Engine 的实际信息 容器模式、存储驱动、资源、代理等;OSType 应与 Linux 实践相符
docker run --rm hello-world 镜像获取、容器创建和进程运行 本地没有镜像时还会检查 Registry 下载链路;--rm 在退出后删除测试容器
docker compose version Compose 插件是否可用及实际版本 本文使用带空格的 docker compose,不要求旧的 docker-compose 独立程序

hello-world 输出完成后退出是正常行为,它不是常驻服务。镜像通常还会留在本地,--rm 删除的是容器。

如果 PowerShell 成功、Ubuntu 失败,优先检查目标发行版的 WSL Integration。若两边都无法得到 Server 信息,先解决 Engine 启动或连接问题,再继续教程。

6. 第一个有意义的容器:用 nginx 走完生命周期

执行:

bash 复制代码
docker run -d -p 127.0.0.1:8080:80 --name my-nginx nginx:stable

逐项拆解:

参数 作用
docker run 从指定镜像创建并启动一个新容器
-d 后台运行,CLI 返回容器 ID
-p 127.0.0.1:8080:80 将 Windows 本机回环地址的 8080 端口转发到容器 80 端口
--name my-nginx 设置容器名称,后续可用名称代替长 ID
nginx:stable 使用 nginx 的 stable 标签;本地没有时按默认拉取策略获取

浏览器打开 http://localhost:8080,应看到 nginx 默认页面。这里使用回环地址,适合本机练习;若写成 -p 8080:80,通常会发布到所有宿主接口,可能允许其他机器访问,取决于网络与防火墙。

stable 是会更新的标签,不是固定不变的镜像内容。本文用它降低入门时维护版本号的负担,正式项目的固定策略见第 7 节。

查看容器与日志:

bash 复制代码
docker ps
docker ps -a
docker logs --tail 50 my-nginx
docker logs -f my-nginx

docker ps 列出运行中的容器;-a 加上已退出容器。--tail 50 只读取最近 50 行日志;-f 持续跟随日志,按 Ctrl+C 结束跟随,不会停止后台 nginx。浏览器请求后,通常能在日志中看到访问记录。

随后完成停止、再次启动和删除:

bash 复制代码
docker stop my-nginx
docker start my-nginx
docker stop my-nginx
docker rm my-nginx

stop 请求主进程终止,超过宽限时间可能强制结束;start 启动同一个已停止容器,因此沿用原来的端口、挂载和可写层;第二次 stop 为删除做准备;rm 删除容器及其可写层,镜像仍保留。

如果名称已经存在,先用 docker ps -a 确认它是什么,再决定启动还是删除。不要为了重复运行命令直接清理所有容器。

修改端口、挂载等创建参数通常需要重新创建容器,docker start 不会重新读取另一套 docker run 参数。

7. Docker 镜像怎么管理?latest 是最新版本吗?

bash 复制代码
docker images
docker pull python:3.12-slim
docker rmi hello-world:latest

docker images 查看本地镜像;pull 下载或更新指定标签;rmi 删除本地镜像引用,可能因容器引用而被阻止。前面的 hello-world 使用 --rm,测试结束后通常可以删除其镜像。

字段 含义
Repository 镜像仓库名称,例如 python、myteam/api
Tag 仓库中的标签,例如 3.12-slim、v1
Image ID 本地镜像对象标识,输出通常展示短形式
Digest 内容寻址的标识,可用于固定具体镜像内容

latest 只是一个约定俗成的标签。省略标签通常会使用它,但 Registry 不会据此保证它是时间上最新、最稳定或适合你项目的版本。

python:3.12-slim 比 python:latest 限定得更多,但同一标签仍可能更新基础系统或 Python 补丁版本。要更严格复现,应记录或固定 digest,同时锁定应用依赖和构建平台;日后更新也应有意识地重新验证。

可以检查本地镜像的仓库 digest:

bash 复制代码
docker image inspect python:3.12-slim --format '{{json .RepoDigests}}'

image inspect 查看镜像元数据;--format 只输出 RepoDigests。从远程拉取的镜像一般有相应 digest,本地刚构建而未推送的镜像可能没有仓库 digest。

不要把 Image ID 和 Registry 的 manifest digest 混为一谈,也不要在教程中填写一个未经核实的 digest。镜像名后接 @sha256:... 时应使用实际取得的值。

8. Dockerfile 怎么写?先容器化一个最小 Web 项目

本节的目标是单独验证"代码 → 镜像 → HTTP 服务",暂不连接 Redis。项目放在 WSL Linux 文件系统中:

bash 复制代码
mkdir -p ~/projects/docker-demo
cd ~/projects/docker-demo

mkdir -p 创建项目及需要的父目录;cd 切换到该项目,后续构建上下文以这里为起点。若使用配套压缩包,可以进入 01-basic-web 而不另建目录。

目录结构:

text 复制代码
docker-demo/
├── app.py
├── requirements.txt
├── Dockerfile
└── .dockerignore

app.py:

python 复制代码
from flask import Flask, jsonify

app = Flask(__name__)


@app.get("/")
def index():
    return jsonify(message="Hello from a Linux container", project="docker-demo")


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=False)

requirements.txt:

text 复制代码
Flask==3.1.2

这里使用一个明确的示例版本,不声称它是最新版本。仅固定直接依赖还不是完整的依赖锁定,长期项目应进一步锁定传递依赖并维护升级流程。

Dockerfile:

dockerfile 复制代码
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["python", "app.py"]

逐行理解:

指令 本例作用 为什么这样写
FROM python:3.12-slim 以包含 Python 3.12 的精简 Linux 镜像为基础 不依赖 Windows 上是否安装了 Python;slim 较精简,但缺少某些编译工具
WORKDIR /app 设置镜像内工作目录,不存在时创建 后续相对路径和默认启动目录有明确基准
COPY requirements.txt . 把构建上下文中的依赖文件复制到 /app 先复制依赖描述,有利于复用依赖安装缓存
RUN pip install --no-cache-dir -r requirements.txt 构建时安装依赖 -r 读取清单;--no-cache-dir 避免把 pip 下载缓存保存在该层中
COPY . . 把未被忽略的上下文内容复制到 /app 安装依赖后再复制代码,日常改代码通常无需重装相同依赖
EXPOSE 8000 声明应用预期使用的端口 这是元数据,不会自动发布到 Windows
CMD ["python", "app.py"] 指定默认启动程序及参数 exec 形式直接启动程序,避免额外 Shell 层;可被运行时命令覆盖

pip --no-cache-dir 与 Docker 构建缓存是两回事:前者不保留 pip 包缓存,后者仍可以复用已经完成的构建步骤。

Dockerfile 不是 Shell 脚本。 FROM、COPY、CMD 都有 Dockerfile 的语义;只有 Shell 形式的 RUN 等指令会借助 Shell 执行。不同 RUN 也不是同一个持续会话,不能期待前一个 RUN cd ... 改变后续指令的目录,应使用 WORKDIR。

应用绑定 0.0.0.0,意味着监听容器中的网络接口。如果只绑定容器里的 127.0.0.1,发布端口也可能访问不到。这里关闭调试模式,但 Flask 自带服务器仍只用于本节开发验证;第 16 节改为 Gunicorn。

参考:Dockerfile 官方指令、Python 官方镜像。

9. docker build 做了什么?最后一个点是什么意思?

先创建下一节的 .dockerignore,再在项目目录执行:

bash 复制代码
docker build -t docker-demo:v1 .
  • docker build 请求构建镜像。
  • -t 给构建结果设置名称和标签。
  • docker-demo 是本地镜像仓库名,v1 是标签,不会自动向任何 Registry 发布。
  • 最后一个 . 表示当前目录是构建上下文,不是"随便补上的结束符"。

构建上下文定义了构建可使用的文件范围。默认会从相关位置找到 Dockerfile;COPY requirements.txt . 中的源文件相对上下文查找,目标位置相对镜像内的 WORKDIR 解释。

因此,从项目的父目录执行同一命令,构建上下文就变了;即便用 -f 指定 Dockerfile,COPY 的源路径仍以构建上下文为依据。

BuildKit 会根据需要处理上下文和缓存。不要把它机械理解为每次都会完整上传所有文件,但也不要把几十 GB 的数据目录、虚拟环境和密钥放进可用上下文范围。

来源:Docker 官方镜像层说明。从下往上看基础系统、运行时和应用文件;本例复用基础层,再安装依赖并复制源码。这是概念示意,不代表每个 Dockerfile 指令都新增文件系统层。

构建成功后运行:

bash 复制代码
docker run -d --name basic-web -p 127.0.0.1:8000:8000 docker-demo:v1

该命令从刚构建的镜像创建 basic-web,将本机 8000 转发到容器 8000。浏览器访问 http://localhost:8000,应返回包含 message 和 project 的 JSON。

Windows PowerShell 中也可以执行:

powershell 复制代码
curl.exe -f http://localhost:8000/

-f 让 HTTP 错误返回非零退出状态,便于发现失败;Bash 中使用同样参数的 curl。

完成验证后释放端口:

bash 复制代码
docker stop basic-web
docker rm basic-web

这两条命令停止并删除该测试容器,避免后续 Compose 的 8000 端口冲突。镜像可继续留作对照。

日常开发时,修改宿主机源码不会改变已经构建的镜像。需要重新构建并重建容器,或在开发模式下用 bind mount 挂载源码;不要在运行容器里临时改代码后,把它当作可复现交付方式。

10. .dockerignore 怎么写?它和 .gitignore 有什么不同?

在构建上下文根目录创建 .dockerignore:

text 复制代码
.git
.vscode
.idea
__pycache__
*.py[cod]
.env
.env.*
.venv
venv
env
node_modules
*.log
.pytest_cache
README.md

这些规则分别排除 Git 和编辑器元数据、Python 缓存、环境文件、虚拟环境、前端依赖、日志、测试缓存和当前镜像不需要的说明文件。README.md 是否排除取决于应用是否需要读取它;本项目不需要。

.gitignore 控制 Git 默认不跟踪哪些文件;.dockerignore 控制哪些文件不进入构建上下文。Git 忽略文件并不意味着 Docker 会自动忽略。二者规则语法也并非所有细节完全相同,不应简单互相替代。

密钥、云凭据、生产 .env 不应通过 COPY 写入镜像。之后再用 RUN rm 删除,也不能保证历史层中没有这些内容。构建时必须使用凭据时,应使用 BuildKit 的 secret 挂载;运行时密钥则用受控配置或密钥管理方式注入。

参考:构建上下文与 .dockerignore。

11. Docker 数据怎么持久化?容器重启后数据就会没吗?

先纠正一个常见说法:同一个容器停止、再启动,通常会保留其可写层。删除容器后重新创建,旧可写层才不属于新容器。 如果程序把数据写在内存或临时路径中,丢失原因还可能在程序本身。

存储方式 生命周期与位置 适合场景
容器可写层 随容器存在,删除容器时丢弃 可重新生成的临时文件,不承担长期业务数据
Named Volume Docker 管理的独立存储对象 数据库数据、需要跨容器重建保留的文件
Bind Mount 挂载指定宿主路径 开发源码、由宿主编辑的配置、需要直接访问的文件

Volume 不等于备份:它可以避免容器重建导致的数据丢失,但不能防止误删、数据库逻辑错误或磁盘损坏。数据库还需要自己的持久化策略和备份流程。

11.1 用一个小实验理解 named volume

以下命令在 WSL Bash 执行:创建一个命名卷,用一次性 Python 容器写入文件,再由另一个容器读取。

bash 复制代码
docker volume create demo-data
docker run --rm --mount type=volume,src=demo-data,dst=/data python:3.12-slim python -c 'from pathlib import Path; Path("/data/note.txt").write_text("persisted")'
docker run --rm --mount type=volume,src=demo-data,dst=/data python:3.12-slim python -c 'from pathlib import Path; print(Path("/data/note.txt").read_text())'
docker volume inspect demo-data

volume create 创建 named volume。两条 run 都将它挂载到容器 /data;src 是卷名,dst 是容器路径。第一条命令用 Python 写入文件,第二条读取,应打印 persisted。两个容器退出后因 --rm 被删除,数据仍在卷里。

volume inspect 查看卷元数据。Docker Desktop 返回的挂载路径属于其 Linux 后端,不应当成 Windows 资源管理器中的普通目录直接编辑。

确认这个练习文件不需要了,再执行:

bash 复制代码
docker volume rm demo-data

这会删除卷及其内容。删除前应确保它没有被容器使用,也不是其他项目的数据。

11.2 Bind mount:由你决定宿主路径

在 WSL Bash 中:

bash 复制代码
mkdir -p ~/projects/mount-demo
cd ~/projects/mount-demo
printf '<h1>Bind mount works</h1>\n' > index.html
docker run -d --name bind-nginx -p 127.0.0.1:8081:80 --mount "type=bind,source=$(pwd),target=/usr/share/nginx/html,readonly" nginx:stable

前两条创建并进入目录;printf 写入首页;$(pwd) 展开当前 Linux 绝对路径。容器将该目录作为 nginx 静态文件目录,readonly 防止 nginx 修改宿主文件。

打开 http://localhost:8081 应显示页面。修改宿主机 index.html,刷新即可看到变化,这正是 bind mount 适合开发的原因。

bash 复制代码
docker stop bind-nginx
docker rm bind-nginx

停止并删除测试容器后,宿主文件还在。挂载会遮住容器目标目录原有内容,而不是与镜像文件自动合并;如果挂载一个空目录覆盖 /app,程序可能找不到代码。

本文优先使用 --mount,因为不存在的 bind 源路径默认会报错,更容易发现路径问题。传统 -v 写法在一些情况下会自动创建源目录,可能把本应挂载的文件路径变成空目录。

参考:Volumes、Bind mounts。

12. Windows + WSL2 的项目文件应该放在哪里?

12.1 三种路径不要混用

视角 示例 实际含义
Windows C:\Users\Alice\projects\demo Windows 文件系统上的目录
WSL 访问 C 盘 /mnt/c/Users/Alice/projects/demo 从 Linux 访问同一个 Windows 目录
WSL Linux 文件系统 /home/alice/projects/demo Linux 发行版内的项目目录
容器 /app 容器看到的路径,不会自动对应上述任意目录

WORKDIR /app 设置的是镜像内部目录;COPY 把构建上下文中的文件复制进去;bind mount 则在运行时建立路径映射。三者是不同的操作。

Windows 资源管理器可通过 \\wsl.localhost\Ubuntu\home\alice\projects 等入口访问 WSL 文件,实际发行版名和用户名按自己的电脑替换。也可以在 WSL 项目目录执行:

bash 复制代码
explorer.exe .

该命令通过 Windows 互操作打开当前目录,便于检查文件。不要直接进入发行版内部的虚拟磁盘文件位置修改数据。

12.2 大量小文件时,跨文件系统可能成为瓶颈

如果主要工具链、Python 依赖安装、Linux 编译和文件监视都在 WSL 中运行,通常优先把项目放在 /home/...。微软和 Docker 文档都建议结合实际工具运行位置选择文件系统,减少跨系统访问。

node_modules、大型 Git 工作树、C/C++ 增量构建和大量 Python 文件,会涉及许多元数据与小文件操作,跨 Windows/Linux 文件系统的成本可能更明显。但不能由此断言每个项目放在 C 盘都会慢,也不能给出没有测试依据的倍数。

如果必须使用 Windows 本地工具读取项目、组织规定只能放在某盘,或项目以大文件为主,可以保留 Windows 路径,并比较实际构建时间和热重载表现。不要为性能盲目搬迁重要数据。

VS Code 的 WSL 工作方式适合在 Windows 上编辑 Linux 项目。若配置了相应扩展和 CLI,可在 WSL 项目目录运行:

bash 复制代码
code .

这会打开当前项目;确认编辑器连接到 WSL 环境,避免终端使用 Linux、扩展却使用 Windows 解释器。

12.3 换行符、执行权限、文件名也会影响运行

Shell 脚本出现 /bin/sh^M 或 bad interpreter,优先检查 CRLF。可以在 Git 项目中添加:

gitattributes 复制代码
*.sh text eol=lf
Dockerfile text eol=lf
*.yaml text eol=lf
*.yml text eol=lf

该 .gitattributes 规定相关文本文件使用 LF;已有文件仍需确认是否需要重新规范化。本文主项目不使用入口 Shell 脚本,减少换行和执行位问题。

Linux 对文件名大小写敏感。App.py 与 Dockerfile 中的 app.py 不一定是同一个文件。bind mount 的权限还受路径所在文件系统影响,不能统一用 chmod 777 解决。

参考:微软 WSL 文件系统、Docker WSL2 最佳实践。

13. Docker 端口映射怎么理解?

简写 -p 8080:80 的两个端口分别是 Host Port 和 Container Port:客户端连接宿主机 8080,Docker 的发布规则将流量送到容器网络中的 80。

在本文 nginx 示例中,实际链路是:
#mermaid-svg-wDd952c81QXmRgTr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wDd952c81QXmRgTr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wDd952c81QXmRgTr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wDd952c81QXmRgTr .error-icon{fill:#552222;}#mermaid-svg-wDd952c81QXmRgTr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wDd952c81QXmRgTr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wDd952c81QXmRgTr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wDd952c81QXmRgTr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wDd952c81QXmRgTr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wDd952c81QXmRgTr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wDd952c81QXmRgTr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wDd952c81QXmRgTr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wDd952c81QXmRgTr .marker.cross{stroke:#333333;}#mermaid-svg-wDd952c81QXmRgTr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wDd952c81QXmRgTr p{margin:0;}#mermaid-svg-wDd952c81QXmRgTr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wDd952c81QXmRgTr .cluster-label text{fill:#333;}#mermaid-svg-wDd952c81QXmRgTr .cluster-label span{color:#333;}#mermaid-svg-wDd952c81QXmRgTr .cluster-label span p{background-color:transparent;}#mermaid-svg-wDd952c81QXmRgTr .label text,#mermaid-svg-wDd952c81QXmRgTr span{fill:#333;color:#333;}#mermaid-svg-wDd952c81QXmRgTr .node rect,#mermaid-svg-wDd952c81QXmRgTr .node circle,#mermaid-svg-wDd952c81QXmRgTr .node ellipse,#mermaid-svg-wDd952c81QXmRgTr .node polygon,#mermaid-svg-wDd952c81QXmRgTr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wDd952c81QXmRgTr .rough-node .label text,#mermaid-svg-wDd952c81QXmRgTr .node .label text,#mermaid-svg-wDd952c81QXmRgTr .image-shape .label,#mermaid-svg-wDd952c81QXmRgTr .icon-shape .label{text-anchor:middle;}#mermaid-svg-wDd952c81QXmRgTr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wDd952c81QXmRgTr .rough-node .label,#mermaid-svg-wDd952c81QXmRgTr .node .label,#mermaid-svg-wDd952c81QXmRgTr .image-shape .label,#mermaid-svg-wDd952c81QXmRgTr .icon-shape .label{text-align:center;}#mermaid-svg-wDd952c81QXmRgTr .node.clickable{cursor:pointer;}#mermaid-svg-wDd952c81QXmRgTr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wDd952c81QXmRgTr .arrowheadPath{fill:#333333;}#mermaid-svg-wDd952c81QXmRgTr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wDd952c81QXmRgTr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wDd952c81QXmRgTr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wDd952c81QXmRgTr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wDd952c81QXmRgTr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wDd952c81QXmRgTr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wDd952c81QXmRgTr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wDd952c81QXmRgTr .cluster text{fill:#333;}#mermaid-svg-wDd952c81QXmRgTr .cluster span{color:#333;}#mermaid-svg-wDd952c81QXmRgTr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wDd952c81QXmRgTr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wDd952c81QXmRgTr rect.text{fill:none;stroke-width:0;}#mermaid-svg-wDd952c81QXmRgTr .icon-shape,#mermaid-svg-wDd952c81QXmRgTr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wDd952c81QXmRgTr .icon-shape p,#mermaid-svg-wDd952c81QXmRgTr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wDd952c81QXmRgTr .icon-shape .label rect,#mermaid-svg-wDd952c81QXmRgTr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wDd952c81QXmRgTr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wDd952c81QXmRgTr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wDd952c81QXmRgTr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} localhost:8080
容器网络地址:80
Windows 浏览器
Desktop 端口发布与转发
nginx 监听的容器端口
HTTP 响应沿连接返回

Windows 上的 Docker Desktop 负责把 Windows 可访问的发布端口与 Linux 后端中的容器连接起来,不需要读者手动查出容器 IP 再访问。

要访问成功,至少要同时满足:

  1. 容器处于运行状态,应用没有提前退出。
  2. 容器中的应用监听正确端口,且监听地址允许容器外流量进入。
  3. 端口映射方向正确,宿主端口没有被其他程序占用。
  4. Windows 防火墙、代理和其他网络策略没有阻断目标流量。

EXPOSE 8000 不等于 -p 8000:8000。前者是镜像元数据,后者创建实际发布规则。nginx 的容器端口为 80,所以把它写成 8080:8080 不会使 nginx 自动改变监听端口。

来源:Docker 官方端口发布示例。图中是 macOS 上的 Desktop 4.56.0 示例,用来识别 Port(s) 字段;不是本文 Windows/nginx 容器的实测截图。界面、容器名和资源数值以本机为准。

查看实际发布情况:

bash 复制代码
docker port my-nginx

该命令显示指定容器的端口映射;在第 6 节删除容器后需要先重新创建它才能检查。端口只需本机访问时,明确绑定 127.0.0.1;需要局域网访问时,再根据服务设计调整绑定地址和防火墙。

参考:端口发布、Docker Desktop 网络。

14. Docker 网络基础:两个容器怎样通信?

同一台 Engine 上的 Linux 容器,常使用 bridge 网络。与 Docker 自带的默认 bridge 相比,用户创建的 bridge 网络提供更方便的容器名称解析。因此多容器项目不要依赖默认网络上的名称解析行为。

创建一个网络,并在其中启动 Redis:

bash 复制代码
docker network create demo-net
docker run -d --name cache-demo --network demo-net redis:7.4
docker run --rm --network demo-net redis:7.4 redis-cli -h cache-demo ping

network create 创建用户自定义 bridge。第二条启动名为 cache-demo 的 Redis。第三条启动一次性客户端容器,连接同一网络,redis-cli -h cache-demo ping 通过容器名找到服务;服务已经就绪时应输出 PONG。

如果刚启动时还未就绪,稍后重试该客户端命令。这也说明"容器已经创建"与"服务能处理请求"之间可能有间隔。

这里没有 -p 6379:6379:客户端从同一 Docker 网络直接连接 Redis 的 6379,不经过 Windows 发布端口。发布端口用于容器网络以外的访问,不是容器互通的前提。

清理这个网络实验:

bash 复制代码
docker stop cache-demo
docker rm cache-demo
docker network rm demo-net

依次停止、删除服务容器,再删除没有容器使用的网络。网络实验没有配置业务持久化,应只用于测试。

记住三种地址的用途:

调用位置与目标 地址示例
Windows 浏览器访问发布的应用 http://localhost:8000
Compose 的 app 访问同项目 redis redis:6379
容器访问 Windows 宿主服务 host.docker.internal:宿主服务端口

容器中的 localhost 指该容器自己,不是 Windows,也不是另一个容器。不要硬编码容器 IP;重建后 IP 可能变化,而服务名称可以继续使用。

15. Docker Compose 怎么用?为什么不继续堆 docker run?

一个 Web + Redis 项目至少要维护:应用构建、两个启动命令、网络、端口、环境变量、卷、启动依赖,以及停止与清理顺序。写进终端历史很难审查和复用。

Compose 用一个 YAML 文件描述这些服务,由 docker compose 统一创建和管理。它适合本机开发与部分单机部署流程,但不会自动提供集群调度、高可用和完整发布治理。

先看一个用于说明字段的最小片段;完整可运行配置在下一节:

yaml 复制代码
services:
  app:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - redis

  redis:
    image: redis:7.4
    volumes:
      - redis-data:/data

volumes:
  redis-data:
字段 作用 注意事项
services 定义服务;每个服务可以创建相应容器 名称同时用于同项目网络内的服务发现
build 指定镜像构建上下文或详细构建配置 . 的含义仍是上下文,不是容器工作目录
image 指定服务使用的镜像,也可给构建结果命名 与 build 同时出现时注意构建、拉取策略;本文使用 up --build 明确构建应用
ports 发布宿主端口 YAML 中用字符串便于保持明确的端口格式
volumes 为服务声明挂载 顶层 volumes 定义 named volume,服务内指定挂到哪里
environment 设置容器进程的环境变量 不要在提交的配置里写生产密码
depends_on 表达服务启动依赖 简写不等待应用层真正就绪

截至核对日期,官方文档说明 Compose v2 和 v5 都使用 docker compose 和 Compose Specification;新版 Desktop 已提供 v5。本文按这些现代插件的共同 CLI 流程编写,不把"插件方式"与某个固定主版本号等同。用 docker compose version 确认本机版本。Compose 官方常见问题

现代 Compose 不需要为了"兼容"而添加过时的顶层 version: "3.8"。本文采用默认文件名 compose.yaml;docker-compose.yml 等受支持的名称也可使用,但一个练习目录里不要放几份不清楚谁生效的配置。

Compose 默认给同项目服务创建网络。应用使用 redis 这个服务名连接 Redis,即使没有设置 container_name 也可以工作;这里不强行固定容器名,避免限制扩展和增加命名冲突。

.env 常用于 Compose 文件插值,不等于文件中的所有变量自动进入容器。需要通过 environment 或 env_file 明确传入;环境变量也不等于秘密不会被查看,诊断配置和容器信息时应避免泄露。

depends_on 与健康检查的边界

简单的 depends_on: [redis] 主要解决创建与启动顺序,不保证 Redis 已经接收连接。长写法 condition: service_healthy 会结合依赖服务的 healthcheck,在创建应用前等待健康条件。

这仍不是运行期间的容错机制:Redis 之后可能断开,应用必须处理连接失败和恢复。容器变成 unhealthy 也不会仅因为配置了 restart: unless-stopped 就自动重启;重启策略主要响应容器进程退出等情况。

参考:Compose 服务字段、启动顺序、Compose 网络。

16. 完整实践:Flask + Redis 项目容器化

目标是一个可继续扩展的计数服务:

  • GET / 返回服务信息。
  • GET /health 检查 Redis 连接,正常返回 200,依赖不可用返回 503。
  • GET /counter 读取计数,不产生写入。
  • POST /counter 使用 Redis INCR 原子增加计数。
  • 删除并重建应用与 Redis 容器,保留 named volume 后计数继续存在。

将写操作设计为 POST,避免浏览器刷新、探活或缓存层的 GET 请求意外增加计数。业务数据放 Redis,不放 Gunicorn worker 的 Python 全局变量,从而使两个 worker 共享一致数据。

16.1 项目目录

在 WSL 中创建独立目录,或进入配套项目的 02-flask-redis:

bash 复制代码
mkdir -p ~/projects/flask-redis-demo
cd ~/projects/flask-redis-demo

前者创建目录,后者使后续 Compose 命令在正确项目中执行。

text 复制代码
flask-redis-demo/
├── app.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
└── compose.yaml

16.2 完整应用代码

app.py:

python 复制代码
import os

import redis
from flask import Flask, jsonify, request

app = Flask(__name__)
store = redis.Redis.from_url(
    os.getenv("REDIS_URL", "redis://redis:6379/0"),
    decode_responses=True,
    socket_connect_timeout=2,
    socket_timeout=2,
)
COUNTER_KEY = "docker-demo:counter"


@app.get("/")
def index():
    return jsonify(
        service="flask-redis-demo",
        endpoints={"health": "/health", "counter": "/counter"},
    )


@app.get("/health")
def health():
    try:
        store.ping()
    except redis.RedisError:
        return jsonify(status="unavailable", dependency="redis"), 503
    return jsonify(status="ok")


@app.route("/counter", methods=["GET", "POST"])
def counter():
    try:
        if request.method == "POST":
            value = store.incr(COUNTER_KEY)
        else:
            value = int(store.get(COUNTER_KEY) or 0)
    except redis.RedisError:
        return jsonify(error="Redis is unavailable; retry later"), 503
    return jsonify(counter=value)

REDIS_URL 将地址从代码中抽离;默认值使用 Compose 服务名。decode_responses=True 让读取结果使用字符串。连接和读写超时设为 2 秒,是为了在本地实验中及时发现故障,实际值应结合业务延迟调整。

INCR 在 Redis 中完成原子操作,避免 Python 侧"先读再加再写"的并发竞争。捕获 Redis 异常后返回 503,让调用者知道依赖暂时不可用;这里不暴露完整连接地址和异常堆栈。

本例没有对 POST 做自动重试:如果响应在网络中丢失,客户端无法仅凭超时判断写入是否已经成功。真实业务若要求安全重试,需要幂等设计。

16.3 依赖文件

requirements.txt:

text 复制代码
Flask==3.1.2
redis==6.4.0
gunicorn==23.0.0

三项分别是 Web 框架、Python Redis 客户端、WSGI 服务进程。redis==6.4.0 是客户端库版本,不代表 Redis 服务端必须是 6.4。

版本用于固定本例的直接依赖,不能把它解释为完整依赖锁文件。正式维护时应锁定传递依赖,并定期检查安全更新;镜像 digest、系统包和目标架构也属于复现条件。

Gunicorn 面向 Unix 环境,不适合直接在原生 Windows Python 中运行。本例运行在 Linux 镜像中,WSL 本地也可用于开发验证。Flask 的 Gunicorn 部署说明

16.4 Dockerfile:服务进程、日志与非 root 用户

dockerfile 复制代码
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

RUN useradd --create-home --uid 10001 appuser
COPY --chown=appuser:appuser app.py .
USER appuser

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "--error-logfile", "-", "app:app"]

在前面基础指令之外,这里新增的设置有明确用途:

设置 原因
PYTHONDONTWRITEBYTECODE=1 不生成 .pyc 缓存,避免代码目录产生不必要写入
PYTHONUNBUFFERED=1 减少标准输出缓冲,方便及时读取容器日志
useradd --create-home --uid 10001 appuser 构建时创建有固定 UID 的普通用户及 home 目录
COPY --chown=... 让复制的应用文件有明确所有权,避免依赖默认 root 所有权
USER appuser 容器启动后的服务以普通用户运行;8000 不要求特权绑定
--bind 0.0.0.0:8000 监听容器网络接口,使发布端口可到达
--workers 2 使用两个 worker 展示共享 Redis 数据的场景;不是通用性能最优值
两个日志选项的 - 将访问日志和错误日志输出到标准流,由 Docker 收集
app:app 导入 app.py 模块中的 Flask 对象 app

这里直接用 exec 形式启动 Gunicorn,让它作为主进程接收停止信号。没有把后台启动命令、tail -f 等拼在一起维持"看起来还活着"的容器。

本应用不向 /app 写入业务数据,因此不需要挂载可写源码目录。若以后加入上传文件,应为上传路径设计单独卷和权限。

16.5 完整 .dockerignore

text 复制代码
.git
.vscode
.idea
__pycache__
*.py[cod]
.env
.env.*
.venv
venv
env
node_modules
*.log
.pytest_cache
README.md

规则作用与第 10 节一致。这份 Dockerfile 只复制 app.py,仍保留忽略规则,以限制构建可用内容并避免以后扩展 COPY 时误带敏感文件。

16.6 完整 Compose 文件

compose.yaml:

yaml 复制代码
name: docker-wsl-demo

services:
  app:
    build: .
    image: docker-wsl-demo-app:v1
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      REDIS_URL: redis://redis:6379/0
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:7.4
    command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec"]
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  redis-data:

重要配置说明:

  • name 固定本练习的 Compose 项目名,便于识别资源。默认创建的卷通常名为 docker-wsl-demo_redis-data;实际以检查结果为准。改项目名可能创建另一套卷,看起来像"数据没了",实际上旧卷仍在。
  • app.build 指定构建上下文,image 为构建结果命名。下一步使用 up --build,明确要求先构建应用。
  • app 只发布本机 8000,Redis 不发布宿主端口。应用通过项目网络连接 redis:6379;URL 末尾 /0 指 Redis 数据库编号。
  • Redis 的 command 覆盖默认启动参数,启用 AOF,并采用每秒 fsync 策略;数据目录 /data 挂到 named volume。
  • Redis 的健康检查实际执行 redis-cli ping;应用检查自己的 /health,间接验证 Redis 依赖。Python 标准库可执行检查,不必为探活另外安装 curl。
  • interval 是检查间隔,timeout 是单次检查的超时,retries 是判定不健康的连续失败阈值,start_period 是启动宽限期。这些是本地小项目的示例值,不保证适合任何数据库规模。
  • restart: unless-stopped 便于进程异常退出或 Engine 重启后恢复服务,但尊重显式停止。它不代替健康检查后的故障处理。
  • json-file 的 max-size 和 max-file 对日志轮转设置示例上限,避免练习服务无限累积日志;数值应结合实际日志量调整。

Volume 与 Redis AOF 是两个层面的设置 :前者保存文件,后者让 Redis 将写入落到文件。everysec 在异常掉电等情况下仍可能丢失最近约一秒的写入,不等于绝对不丢数据;也不代替备份。Redis 持久化说明

本例是本机学习环境,不配置公网入口和 Redis 认证。即使不发布 Redis 端口,也不应把它直接视为适合多租户或公网生产环境;正式部署还需认证、访问控制、密钥管理和备份。

16.7 配置检查、构建与启动

在 compose.yaml 所在目录执行:

bash 复制代码
docker compose config -q
docker compose up -d --build
docker compose ps
docker compose logs --tail 50 app redis

config -q 验证 Compose 配置,不打印完整解析结果;它不能替代运行测试。up -d --build 构建应用并创建、后台启动项目服务。ps 查看服务状态和端口;logs 读取两个服务的最近日志。

app 依赖 Redis 的健康条件;首次下载、构建和服务启动需要时间。如果 ps 显示健康检查正在启动,先看日志,不要因为几秒内页面没响应就删除所有资源。

16.8 HTTP 与数据链路验收

Windows PowerShell 执行:

powershell 复制代码
curl.exe -f http://localhost:8000/health
curl.exe -f http://localhost:8000/counter
curl.exe -f -X POST http://localhost:8000/counter
curl.exe -f -X POST http://localhost:8000/counter
curl.exe -f http://localhost:8000/counter

第一条验证依赖健康;第二条读取当前值;两条 -X POST 各增加一次;最后一条验证读到增加后的值。全新卷的预期序列是 0 → 1 → 2 → 2,如果卷里已有数据,数值会从原值继续增加。

WSL Bash 使用相同参数的 curl。没有必要在 Windows 主机安装 Python 或 Redis 来运行这个容器项目。

再从容器内部确认网络和存储:

bash 复制代码
docker compose exec app python -c 'import socket; print(socket.gethostbyname("redis"))'
docker compose exec redis redis-cli GET docker-demo:counter
docker compose exec redis redis-cli INFO persistence
docker compose exec app id

第一条在应用容器中解析服务名,证明不依赖固定 IP;第二条直接读取实际 Redis key,结果应与 API 相符;第三条查看持久化状态,如 AOF 是否启用;第四条查看应用的运行用户,应是 Dockerfile 中的普通用户。

exec 是在现有运行容器中执行额外命令,不是重新创建服务。不要把人工 exec pip install ... 当成长期更新依赖的方法,这种修改没有进入 Dockerfile。

16.9 依赖中断与恢复验收

bash 复制代码
docker compose stop redis

这只停止 Redis。此时 Windows PowerShell 执行:

powershell 复制代码
curl.exe -i http://localhost:8000/health
curl.exe -i -X POST http://localhost:8000/counter

-i 显示响应头,便于观察状态码。这两项应得到 503,表明应用进程还在,但依赖不可用;日志和健康状态能帮助区分这两类问题。

恢复 Redis:

bash 复制代码
docker compose start redis

这启动同一个 Redis 容器。等待其就绪后,重新检查 /health 和 /counter,应恢复 200 并保留此前计数。应用的客户端会在后续请求中重新建立连接;depends_on 并不是这里恢复连接的原因。

16.10 删除并重建:验证数据持久化

先读取并记下当前计数,再执行:

bash 复制代码
docker compose down
docker compose up -d

down 删除本项目服务容器和默认网络,默认不删除 named volume 。up -d 使用已有镜像和卷重新创建服务;此处未改代码,不必重新构建。

待健康后重新 GET /counter,应与删除前一致。这是比简单 restart 更有价值的持久化检查,因为容器可写层已经被替换。

修改应用代码或依赖后,使用 docker compose up -d --build 构建并更新。仅 docker compose restart 不会把宿主源码复制进旧镜像,也不会应用所有已修改的创建配置。

16.11 停止与清理:保留数据和删除数据分开

目的 命令 数据影响
暂时停止项目 docker compose stop 保留容器和卷
继续启动现有容器 docker compose start 使用已有容器及其配置
删除容器和网络,保留计数 docker compose down 保留 named volume
完全删除本练习数据 docker compose down --volumes 删除该配置管理的非 external 卷,包括计数数据

最后一条是破坏性清理,只有确认练习数据不需要时才执行。不要把它写进日常启动脚本。

如还需要删除本例构建的镜像,先 down,再执行:

bash 复制代码
docker rmi docker-wsl-demo-app:v1

这删除应用镜像;如果其他容器还引用它,应先识别引用关系,不要用强制删除绕过检查。

16.12 如何迁移到 Python、C/C++、Web 或 AI 项目

不变的是工作流:确定依赖边界,写构建文件,明确启动命令,将运行配置与镜像分离,将持久数据放在卷中,最后验证网络与重建行为。

  • Python :检查依赖是否需要系统库或编译器。slim 缺少工具时,应显式安装需要的系统包,或使用多阶段构建生成 wheel。
  • C/C++:将编译器、构建工具和库版本写入构建阶段,再将产物复制到运行阶段;运行时仍要关注动态链接库和目标架构。
  • Web:把构建时配置与运行时配置区分开。前端静态资源中的变量可能在构建时已写死,运行容器时再改环境变量未必生效。
  • AI:模型文件不一定适合打进镜像,可挂载模型缓存;GPU 驱动、WSL GPU 能力和容器运行条件需要另外验证,Docker 不会凭空提供显卡或正确驱动。

本地跑通只是起点。部署到服务器时,还要重新考虑 Linux Engine 安装、外部入口、TLS、权限、备份和资源限制,不能直接把 Windows Desktop 的配置当成生产服务器方案。

17. Docker Desktop 占用内存太高怎么办?

17.1 先区分容器、WSL 进程和缓存

任务管理器中 vmmem 或 vmmemWSL 的占用,通常反映相关虚拟化环境的资源,不等于某一个 Docker 容器的应用堆内存。除了容器,还有其他 WSL 进程、Linux 文件缓存和构建任务。

构建时大量读写文件会增加缓存。任务结束后,内存返回 Windows 的时间受 WSL 版本、回收策略和当前工作负载影响。不能仅凭任务管理器数值高,就认定 Docker 内存泄漏。

先看容器资源:

bash 复制代码
docker stats --no-stream

该命令输出当前容器资源快照,避免持续刷新。它不能完整代表 WSL VM 的全部占用,但可以发现某个容器是否异常消耗内存或 CPU。

再在个人 WSL 发行版中检查:

bash 复制代码
free -h
ps aux --sort=-%mem

free -h 用易读单位显示 Linux 内存和缓存;ps 列出当前环境可见进程并按内存占比排序。Ubuntu 中看到的进程列表不一定包含 Desktop 后端的所有容器进程,不能把它与 docker stats 当作相同范围。

按"是不是仍在构建 → 是不是有不需要的服务 → 是不是某个进程持续增长 → 是否缓存回收问题"的顺序排查,比先固定一个很小的内存上限更可靠。

17.2 .wslconfig 管的是 WSL2,不是某个容器

在 Windows PowerShell 打开:

powershell 复制代码
notepad.exe "$env:USERPROFILE\.wslconfig"

$env:USERPROFILE 是当前 Windows 用户目录;文件应保存为 .wslconfig,不要误存成 .wslconfig.txt。较新 WSL 还提供 WSL Settings,可优先通过官方支持的设置界面管理。

下面仅是16GB 内存机器的一个配置示例,不是推荐所有读者直接复制的数值:

ini 复制代码
[wsl2]
memory=8GB
processors=4
swap=2GB
配置 作用与取舍
memory 限制 WSL2 VM 可用内存上限,不是启动时立即预留全部容量;上限过低可能让构建或数据库被 OOM 终止
processors 限制分配的逻辑处理器数量,不是单个容器的 CPU 配额;会影响编译和多个 WSL 任务
swap 设置交换空间大小,用磁盘缓解内存压力;不能把磁盘交换当成等价的物理内存

这份文件作用于 WSL2 环境,会影响其他 WSL2 发行版及相关工作负载。它与 Linux 发行版中的 /etc/wsl.conf 不同,也不等同于 Compose 的容器资源限制。

根据机器总内存、Windows IDE 占用、项目峰值和并行服务数量选择上限。8GB 电脑和 64GB 工作站不应套同一数值;AI 模型和大型编译项目也不能只按这个计数器的需求配置。

保存工作、停止需要正常关闭的服务并退出 Docker Desktop 后,可在 PowerShell 执行:

powershell 复制代码
wsl --shutdown

该命令关闭所有运行中的 WSL 发行版和 WSL2 VM,会中断相关会话与容器;不要在数据库写入或构建过程中随手执行。之后重新打开 Docker Desktop,再检查配置和资源表现。

17.3 自动内存回收和 Resource Saver 有版本差异

微软文档中的实验设置包含 autoMemoryReclaim,支持 disabled、gradual、dropCache 等模式。默认值和支持条件随 WSL 版本变化,不应照抄旧教程称"默认一定关闭",也不必为了看起来配置完整而再写一遍当前默认值。

Docker Desktop 的 Resource Saver 在不同后端下行为不同;不要套用 Hyper-V 的完整 VM 暂停行为,断言 WSL2 模式下一定会立即释放所有内存。先更新 WSL,并按对应后端文档理解回收机制。

参考:微软 WSL 全局配置、Docker Desktop 设置。

18. Docker 镜像下载慢或失败,应该怎样排查?

先看失败发生在哪个阶段:

失败位置 访问目标可能是什么 不应该直接归因于什么
docker pull 或 Dockerfile 的 FROM Registry、认证服务、镜像层分发端点 Python 依赖源
RUN pip install ... Python 包索引及文件下载端点 Docker Hub 镜像拉取速度
RUN apt-get ... 基础系统的软件包源 一定是 Docker daemon 出故障

18.1 第一步:确认 Engine 和目标地址

bash 复制代码
docker info
docker pull hello-world

info 先排除未连接 Engine;pull 用小镜像检查基本 Registry 链路。一个小镜像成功不代表所有镜像层端点都可用,但能减少变量。

报错若是 manifest unknown,先核对仓库名和标签;no matching manifest 可能是目标平台不受支持;unauthorized 或限流错误应检查认证、权限和当前服务政策,而不是先改 DNS。

18.2 第二步:检查 Windows DNS 和 HTTPS

PowerShell 中:

powershell 复制代码
Resolve-DnsName registry-1.docker.io
curl.exe -I https://registry-1.docker.io/v2/

Resolve-DnsName 检查 Windows 能否解析该域名;curl.exe -I 请求 HTTP 响应头,检查 HTTPS 连接是否完成。未带认证访问 Docker Hub Registry /v2/ 返回 401 是可预期的认证挑战,不等于 Registry 不可用。

这些检查只代表 Windows 当前工具的链路。Engine 或构建器可能使用不同的代理配置,而且镜像层下载还会经过其他端点;浏览器能打开 Docker 官网,也不能证明整个拉取链路可用。

TLS 错误应检查电脑时间、组织代理证书和受信任证书配置,不要为了消除报错关闭证书校验。DNS 不通时先检查 VPN、网络策略与 WSL 网络状态,不应直接把某个公共 DNS 地址写死到所有机器。

18.3 第三步:确认代理配置在哪一层

网络必须经代理时,检查 Docker Desktop Settings 中的 Proxies ,选择系统代理或按组织要求配置手动代理。Desktop 的代理配置应按 Desktop 文档处理,不要机械复制 Linux daemon 的 daemon.json 代理配置来替代。

WSL 终端里的 HTTP_PROXY、Windows 系统代理、Desktop 的代理、构建步骤内部访问外网的代理,并不是天然同步的一个设置。拉取正常而 pip install 失败时,继续检查构建过程的包源与代理链路。

不要把代理用户名和密码写到 Dockerfile 的 ENV、公开 Compose 文件或截图中。如果失败来自企业网络策略,应使用组织认可的代理、证书与 Registry。

18.4 第四步:认证、Registry 与镜像加速服务

需要账号权限或提高合法额度时,可执行:

bash 复制代码
docker login

该命令按 CLI 提示完成默认 Registry 的认证,具体登录流程随版本变化。不要在命令行参数中直接填写会进入 Shell 历史的明文密码;私有 Registry 应使用其明确地址和组织要求的认证方式。

镜像加速地址只有在服务提供方仍运营、明确支持目标 Registry、访问条件与信任关系清楚时才有价值。不要复制来源不明的"永久免费镜像源"列表。企业内部缓存或官方明确提供的服务,可以按其最新文档配置。

Engine 的 Registry mirror 并不能自动加速所有 Registry、pip 和系统包下载。配置前确认服务作用范围;配置后通过实际拉取验证,并保留回退方式。

参考:Desktop 代理设置、Docker Hub 使用与限额。

19. Docker Desktop 启动不了怎么办?常见故障排查表

排查时先判断失败层:Windows/WSL → Desktop/Engine → 镜像构建与拉取 → 容器进程 → 网络与存储 → 应用依赖 。错误在哪一层,就优先检查哪一层,避免所有问题都从重装开始。

先看日志,再决定是否重启;"全部删掉重来"很容易顺手带走数据。

表中的 Docker 命令可在已配置的终端执行;Windows 专属命令注明 PowerShell。容器名 等占位项需要替换为实际对象。

现象 可能原因 检查命令或入口,以及检查目的 解决方向
有 docker 命令,但无法连接 daemon Desktop 未运行;Engine 未就绪;context 或环境变量指向其他 Engine docker version 看 Server 是否可达;docker context ls 看当前上下文及端点 启动 Desktop,确认 Linux 后端;选回正确上下文;检查是否遗留远程连接变量
Docker Desktop 一直 Starting WSL 组件、虚拟化、资源不足或后端状态异常 PowerShell:wsl --status、wsl --version、wsl -l -v 检查 WSL;Desktop Troubleshoot 收集诊断 更新 WSL、确认虚拟化与磁盘空间;保存工作后有序退出 Desktop、关闭 WSL 再启动;保留诊断,不先恢复出厂
WSL 版本过旧,或 --version 不识别 老版 WSL、更新渠道或策略限制 PowerShell:wsl --version 查组件;wsl -l -v 查发行版类型 按微软文档更新,必要时处理系统更新策略;WSL 软件版本与 VERSION 2 要分开看
WSL Integration 没出现,或看不到 Ubuntu 使用 Windows containers;后端选择不同;没有个人 WSL2 发行版 PowerShell:wsl -l -v;Settings 检查后端和容器模式 切换 Linux containers,确认 WSL2 后端与发行版类型;只用 Windows CLI 时不要求必须有 Ubuntu
docker pull 超时 DNS、代理、认证端点或层下载网络不可达 PowerShell:Resolve-DnsName registry-1.docker.io 查解析;curl.exe -I https://registry-1.docker.io/v2/ 查 TLS/响应;docker info 看 Engine 按第 18 节分层检查,不用未知镜像源掩盖问题;区分超时、证书、权限和限流
端口已占用,服务无法创建 Windows 程序、旧容器,或系统保留端口范围占用 docker ps 看已发布端口;PowerShell:Get-NetTCPConnection -LocalPort 8000 -State Listen 查监听;netsh interface ipv4 show excludedportrange protocol=tcp 查保留范围 确认占用者后调整宿主端口,如改为 127.0.0.1:8001:8000;不要随意停止系统服务
容器启动后立即退出 主程序正常结束;入口参数错误;依赖缺失;OOM docker ps -a 查退出容器;docker logs --tail 100 容器名 看程序错误;docker inspect 容器名 --format '{``{json .State}}' 看退出码和 OOM 标记 根据日志修复 CMD、依赖或资源;一次性任务退出可能正常;-d 不会让已经结束的程序继续运行
容器访问不到宿主机服务 使用了容器 localhost;宿主监听或防火墙不允许访问 docker compose exec app python -c 'import socket; print(socket.gethostbyname("host.docker.internal"))' 查解析;PowerShell:Get-NetTCPConnection -State Listen 查宿主监听 Desktop 中使用 host.docker.internal 和实际宿主端口;核对服务绑定、代理与防火墙;不要直接套 Linux Server 的网络结论
volume 或 bind mount 权限错误 应用 UID/GID 与目录所有权不符;挂载只读;源路径错误 docker compose exec app id 查运行用户;docker inspect 容器名 --format '{``{json .Mounts}}' 查类型、源和读写属性;WSL:ls -ld 路径 查目录权限 按实际写入目录配置所有权和权限;检查挂载是否覆盖代码;不要递归 chmod 777 或一律改为 root
磁盘占用越来越大 镜像、构建缓存、退出容器、日志和卷数据累积 docker system df -v 查 Docker 对象与共享大小;docker buildx du 查当前构建器缓存 分类型清理无用对象,先保护卷数据;设置日志轮转;逻辑删除不保证 VHD 立刻缩小
镜像删不掉 运行或已停止容器引用;多个标签指向同一镜像 docker ps -a --filter ancestor=docker-demo:v1 查引用;docker image ls 查标签 先确认引用容器的用途,停止并删除确实不需要的容器,再删目标镜像;不默认使用强制删除

19.1 CLI 连接错 Engine 时,重装往往无效

先检查:

bash 复制代码
docker context show
docker context ls

show 显示当前 context 名称;ls 列出配置及端点。若确实连接错了,可执行 docker context use 实际上下文名 切换,名称应从列表中选择,不要假定每台机器都叫 desktop-linux。

PowerShell 检查影响 Docker 的环境变量:

powershell 复制代码
Get-ChildItem Env:DOCKER*

该命令列出 DOCKER_HOST、DOCKER_CONTEXT 等当前存在的变量。若它们来自过去的远程连接配置,应确认用途后再移除或修改;这些变量可能让 CLI 没有连接你刚启动的 Desktop。

同时存在两个 Engine 时,"镜像在另一边看不到"可能只是连接目标不同,不是镜像被删除了。

19.2 Starting 状态:先保留诊断,再考虑重置

如果 wsl --status 本身就失败,先解决 WSL。如果 WSL 正常而 Desktop 后端一直不就绪,在官方 Troubleshoot 入口查看诊断、错误提示和日志。

保存工作后退出 Desktop,执行 wsl --shutdown 再启动,属于有影响范围的重启措施;它会结束 WSL 会话。若重复无效,继续用诊断结果定位,不应在没有备份时执行 Factory Reset、卸载内部发行版或删除虚拟磁盘。

恢复出厂和手工删除 docker-desktop 等操作可能丢失容器、镜像或卷,应遵循当前官方步骤并先保护数据。Desktop 官方排错入口

19.3 磁盘清理要按对象进行

先查看 docker system df -v。镜像的层可能共享,表中显示的各镜像大小不能简单相加;构建缓存、容器可写层和 volume 也需要分别看。

确认范围后,可有选择地执行:

bash 复制代码
docker image prune
docker builder prune

image prune 默认清理 dangling 镜像,不等于删除所有未使用镜像;builder prune 清理构建缓存,后续构建可能变慢。两者都会提供交互提示,应阅读清理范围。

docker volume prune 和带卷删除选项的清理命令可能删除当前没有容器引用、但仍有价值的数据;不作为本文默认清理步骤。docker system prune -a 也不应作为"空间不足就复制"的通用药方。

Docker 对象清理后的逻辑空闲空间,与 Windows 看到的后端虚拟磁盘文件大小不是一个数字。磁盘映像回收和迁移应按当前 Desktop 支持的功能处理,不要手动删除 VHDX 文件来"腾空间"。Docker 清理说明

20. Docker 常用命令速查:只保留开发高频操作

表中需要替换 容器名、网络名、卷名。涉及项目的命令在对应 compose.yaml 目录执行。删除操作只对确认不需要的对象使用。

类别 命令 用途
镜像 docker image ls 列出本地镜像,与 docker images 等价
镜像 docker pull python:3.12-slim 获取指定镜像标签
构建 docker build -t docker-demo:v1 . 从当前上下文构建并命名镜像
镜像 docker rmi docker-demo:v1 删除指定本地镜像引用
容器 docker ps -a 同时查看运行与退出容器
容器 docker stop 容器名 / docker start 容器名 停止或启动已有容器,两条分别执行
容器 docker rm 容器名 删除已停止容器及其可写层
日志 docker logs --tail 100 -f 容器名 先显示最近 100 行,再跟随日志
执行 docker exec -it 容器名 sh 在现有容器中启动交互 Shell;镜像需含 sh,输入 exit 退出
网络 docker network inspect 网络名 检查网络和连接到它的容器
Volume docker volume ls / docker volume inspect 卷名 列出卷或查看指定卷元数据,两条分别执行
资源 docker stats --no-stream / docker system df 查看资源快照或 Docker 对象磁盘占用,两条分别执行
Compose docker compose config -q 验证配置是否可解析
Compose docker compose up -d --build 构建并创建、更新后台服务
Compose docker compose ps 查看本项目服务状态
Compose docker compose logs --tail 100 -f app 跟随应用服务日志
Compose docker compose exec app sh 在应用服务容器中进入 Shell
Compose docker compose down 删除服务容器和默认网络,默认保留 named volume

exec 中的 -i 保持标准输入,-t 分配终端。自动化执行时不一定需要交互终端;精简镜像可能没有 Bash,因此这里使用较普遍的 sh。

命令不是越多越好。能看状态、读日志、检查配置、重建应用、确认挂载和网络,已经可以完成多数初步排查。Docker CLI 参考

21. Docker 不适合解决什么问题?

Docker 不是虚拟机的完全替代品。 Linux 容器共享所在 Linux 环境的内核,并不为每个容器提供独立内核。Windows 上运行 Linux 容器仍需要 Linux 后端;需要完整操作系统隔离或不同内核能力时,应重新评估方案。

Docker 不是部署平台本身。 镜像和容器运行解决交付的一部分,但域名、TLS、负载均衡、备份、审计、故障恢复与发布回滚仍需要设计。

Docker 不是 Kubernetes 的替代品。 二者处理的层次不同。本文的单机开发流程无需扩展到集群,但不能把 Compose 的能力等同于完整的集群编排。

Docker 不是环境问题的万能解决方案。 使用漂移的标签、没有锁定依赖、遗漏系统库、错误传入环境变量、架构不匹配、GPU 驱动缺失,仍然会失败。容器化的意义,是把这些边界显式化并可检查。

隔离也不是绝对安全保证。 高权限模式、宿主目录挂载、Docker API 访问和不可信镜像,都可能扩大影响范围。不要把"运行在容器里"当作可以随意执行未知程序的理由。

对于一个没有复杂依赖、无需共享环境的简单单机脚本,虚拟环境或普通安装可能已经足够。决定是否 Docker 化,应看是否需要环境交付、服务组合和部署一致性,而不是为了增加几个配置文件。

22. 从安装到项目:形成可以复用的工作流

本文的学习顺序可以沿着这条链路复查:
#mermaid-svg-xHGSzKhZatLQ2gwv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xHGSzKhZatLQ2gwv .error-icon{fill:#552222;}#mermaid-svg-xHGSzKhZatLQ2gwv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xHGSzKhZatLQ2gwv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xHGSzKhZatLQ2gwv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xHGSzKhZatLQ2gwv .marker.cross{stroke:#333333;}#mermaid-svg-xHGSzKhZatLQ2gwv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xHGSzKhZatLQ2gwv p{margin:0;}#mermaid-svg-xHGSzKhZatLQ2gwv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv .cluster-label text{fill:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv .cluster-label span{color:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv .cluster-label span p{background-color:transparent;}#mermaid-svg-xHGSzKhZatLQ2gwv .label text,#mermaid-svg-xHGSzKhZatLQ2gwv span{fill:#333;color:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv .node rect,#mermaid-svg-xHGSzKhZatLQ2gwv .node circle,#mermaid-svg-xHGSzKhZatLQ2gwv .node ellipse,#mermaid-svg-xHGSzKhZatLQ2gwv .node polygon,#mermaid-svg-xHGSzKhZatLQ2gwv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xHGSzKhZatLQ2gwv .rough-node .label text,#mermaid-svg-xHGSzKhZatLQ2gwv .node .label text,#mermaid-svg-xHGSzKhZatLQ2gwv .image-shape .label,#mermaid-svg-xHGSzKhZatLQ2gwv .icon-shape .label{text-anchor:middle;}#mermaid-svg-xHGSzKhZatLQ2gwv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xHGSzKhZatLQ2gwv .rough-node .label,#mermaid-svg-xHGSzKhZatLQ2gwv .node .label,#mermaid-svg-xHGSzKhZatLQ2gwv .image-shape .label,#mermaid-svg-xHGSzKhZatLQ2gwv .icon-shape .label{text-align:center;}#mermaid-svg-xHGSzKhZatLQ2gwv .node.clickable{cursor:pointer;}#mermaid-svg-xHGSzKhZatLQ2gwv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xHGSzKhZatLQ2gwv .arrowheadPath{fill:#333333;}#mermaid-svg-xHGSzKhZatLQ2gwv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xHGSzKhZatLQ2gwv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xHGSzKhZatLQ2gwv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xHGSzKhZatLQ2gwv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xHGSzKhZatLQ2gwv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xHGSzKhZatLQ2gwv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xHGSzKhZatLQ2gwv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xHGSzKhZatLQ2gwv .cluster text{fill:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv .cluster span{color:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xHGSzKhZatLQ2gwv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xHGSzKhZatLQ2gwv rect.text{fill:none;stroke-width:0;}#mermaid-svg-xHGSzKhZatLQ2gwv .icon-shape,#mermaid-svg-xHGSzKhZatLQ2gwv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xHGSzKhZatLQ2gwv .icon-shape p,#mermaid-svg-xHGSzKhZatLQ2gwv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xHGSzKhZatLQ2gwv .icon-shape .label rect,#mermaid-svg-xHGSzKhZatLQ2gwv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xHGSzKhZatLQ2gwv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xHGSzKhZatLQ2gwv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xHGSzKhZatLQ2gwv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 检查与更新 WSL2
安装并验证 Docker Desktop
运行镜像并管理容器
用 Dockerfile 构建项目镜像
配置 Volume 与 Network
用 Compose 管理真实项目
验证日志 健康 数据与重建

以后接手一个新项目,可以依次问:

  1. CLI 正在连接哪个 Engine,基础环境是否就绪?
  2. 应用依赖和构建上下文是否完整,敏感文件是否排除?
  3. 主进程是否正确启动,端口是否真正可达?
  4. 数据是否独立于容器生命周期,重建后能否恢复?
  5. 服务是否通过名称通信,依赖不可用时行为是否明确?
  6. 配置、日志和清理步骤是否能被别人复用?

能回答并验证这些问题,才算建立起可继续用于开发项目的 Docker 基础工作流。

下一步可以选择三个方向:把镜像构建与测试接入 CI/CD;在 Linux 服务器上部署并补齐运维条件;优化镜像层、依赖缓存和多阶段构建。它们都应建立在当前项目已经能稳定构建、启动、重建和排错的基础上。

官方资料与更新入口

以下链接用于核对系统要求和具体命令。配图已随文章保存,并注明来源;截图用于说明字段和检查位置,不能替代本机的安装与运行验证。

主题 官方资料
Windows 安装要求与安装方式 Docker Desktop for Windows
WSL2 后端、集成与文件性能 WSL backend、WSL best practices
Desktop 设置、代理、资源 Settings
Desktop 许可 License agreement
WSL 安装与命令 Install WSL、Basic commands
WSL 文件与资源配置 Filesystems、Advanced configuration
Windows 10 生命周期 Windows 10 Home and Pro
Dockerfile 与构建上下文 Dockerfile reference、Build context
存储 Volumes、Bind mounts
网络 Bridge、Port publishing、Desktop networking
Compose Services、Startup order、Networking、Environment、Compose FAQ
示例镜像 Python、nginx、Redis
应用服务与持久化 Flask / Gunicorn、Redis persistence
故障与清理 Desktop troubleshooting、Pruning、CLI reference

配图来源与署名

技术图片来自 Docker Docs 源码仓库,遵循其 Apache-2.0 许可。原有标注与商标保留;WebP 图片只转换为 PNG,未改动内容。Windows 检查图和跨平台 Desktop 示例均有版本说明,不作为本文实测结果。

两处表情来自 Twemoji,作者为 Twitter 及项目贡献者,遵循 CC BY 4.0,图片未修改。

相关推荐
阿钱真强道1 小时前
30 嵌入式操作系统 | 上位机界面:Flask 网页显示 + 控制闭环
后端·python·flask
郝学胜-神的一滴1 小时前
AI 编程智能体 02:AI智能体到底是什么
开发语言·人工智能·python·程序人生·pycharm
shimly1234561 小时前
(undone) 解析 qemu-6.0.0 源码 (4) 添加 niubi board
windows
天衍四九-1 小时前
Docker Compose企业实战系列(三):Vue/React前端项目容器化完整部署
前端·vue.js·docker
Guarding and trust2 小时前
python系列之多线程编程
python
陈陈CHENCHEN2 小时前
【Kubernetes】纯 IPv6 K8s 集群访问外部 IPv4 服务
云原生·容器·kubernetes
梅雅达编程笔记2 小时前
03_网页表格数据抓取
爬虫·python·beautifulsoup·pandas·数据采集
迅猛龙办公室2 小时前
Python实现求两个数字之和
java·开发语言·python
企业数字化笔记2 小时前
视频成片怎么稳定导出?编码选择、GPU/CPU降级与媒体交付验收
java·python·ffmpeg