先别急着复制那些搜出来的安装命令。你大概率会遇到这种情况照着教程敲完docker run报错一串英文或者 Docker Desktop 装上了启动却提示 virtualization 相关错误又或者镜像拉到一半直接卡死。这些坑我几乎都踩过一遍。这篇文章不会给你堆一堆术语而是从最底层的概念讲起手把手带你把 Docker 装好、用熟再落地几个真实项目比如 MySQL 8.0、Redis 主从、docker compose 编排最后聊聊镜像下载慢、网络不通这类高频问题怎么排查。无论你是刚接触容器化的学生、想快速部署项目的开发还是运维侧需要交付环境的同学按着这篇走一遍基本就能脱离“只会抄命令”的阶段。1. 容器到底是什么——先建立概念模型1.1 传统部署的“环境地狱”先回忆一个特别常见的场景。你本地写了个 Java 项目跑得好好的交给同事一跑报错JDK 版本不对。换个服务器又报MySQL 字符集不对。再过两天生产环境上 Redis 内存配置和本地不一致缓存策略全乱。这不只是 Java 的毛病。Python 项目有 Python 的版本地狱Node 项目有依赖版本地狱甚至操作系统本身的差异都能折腾一整天。传统部署的思路是一台机器上装好所有依赖然后把应用丢进去。问题是依赖之间会互相影响。A 项目需要 Python 3.6B 项目需要 Python 3.11装在一起迟早出事。容器解决的就是这件事把应用和它的运行环境一起打包环境跟着应用走而不是应用去适应环境。你打包出来的那个东西在开发电脑上能跑在服务器上一样能跑行为几乎完全一致。1.2 容器和虚拟机到底差在哪很多人刚接触容器时会和虚拟机搞混觉得“这不就是个轻量虚拟机吗”。差远了。虚拟机是在宿主机上模拟出一整套硬件然后在硬件上装一个完整的操作系统。你在虚拟机里装 Ubuntu里面就有完整的 init 系统、系统库、内核模块启动的时候要先 BIOS 自检再引导操作系统几分钟跑起来很正常。容器不一样它没有自己的内核直接共享宿主机内核只是通过隔离机制把进程、文件系统、网络隔开。打个比方。虚拟机是租整套房子里面家具电器都配齐你想换个大点的客厅还得整间换。容器更像集装箱货物、工具、说明书都封在一个箱子里船能运、卡车能拉、吊机能搬到了地方一开箱就能用。箱子之间互不干扰但都用的是同一艘船的动力系统。区别最直观的体现是体积和启动时间。一个 Ubuntu 虚拟机镜像动辄几个 GB一个 Ubuntu 容器镜像往往只有几十上百 MB虚拟机启动以分钟计容器启动秒级完成。这也是为什么很多人用 Docker 来跑 CI 构建、临时测试用完就扔成本极低。1.3 镜像、容器、仓库抓住这三个词就够了Docker 的体系里最核心的就是三个概念镜像Image、容器Container、仓库Repository。镜像可以理解成一个“只读模板”里面包含了你运行程序所需的一切代码、运行时、系统库、配置文件。它没有状态不能被修改。容器是镜像运行起来之后的实例有自己的文件系统读写层、网络、进程空间是真正在跑的东西。仓库是存放镜像的地方Docker Hub 是默认的公共仓库你可以把镜像推上去分享也可以从上面拉别人做好的镜像。用做饭类比镜像就是一份“菜谱加上冷冻半成品料理包”里面原材料、调料、烹饪步骤都齐了容器就是你按照这份料理包实际做出来放在桌上的一道菜。你可以用同一个料理包做出一模一样的菜做坏了倒掉重做料理包本身不受影响。理解了这三个词后面所有操作都能串起来从仓库拉镜像用镜像创建并运行容器在容器里访问你的应用有需要就把自定义镜像推送回仓库。2. 环境安装Windows 和 Linux 两条路2.1 WindowsDocker Desktop 的前置准备Windows 上最常用的方案是 Docker Desktop它自带图形界面能管理容器和镜像还能配置 WSL2 后端。但装之前有个大坑很多人第一眼看到的错误就是 “virtualization support not detected” 或者类似的虚拟化提示。这是因为 Docker Desktop 依赖 Windows 的虚拟化能力。你得先确认三件事。第一BIOS 里开启了虚拟化。开机进 BIOS不同品牌快捷键不同一般是 Del / F2 / F12找 Intel Virtualization TechnologyVT-x或 AMD-V 相关选项把它设为 Enabled。怎么确认有没有生效打开任务管理器切到“性能”页下方“虚拟化”如果显示“已启用”就说明 BIOS 这关过了。第二Windows 版本要支持 WSL2。Win10 2004 及以上、Win11 都没问题。推荐直接用 WSL2 作为 Docker Desktop 的后端比老的 Hyper-V 方案启动更快资源占用也更小。在 PowerShell管理员里执行wsl --install装完重启再在 PowerShell 里看版本wsl --status如果是 WSL 1就用wsl --set-default-version 2切换。第三安装 Docker Desktop。直接去官网下载安装包装完启动首次启动可能会提示需要启用 WSL 内核更新下载更新包装上就行。等托盘图标变绿在 PowerShell 里敲docker version能正常输出版本号说明环境就绪。2.2 Linux从官方源安装 Docker 引擎Linux 上装的是 Docker 引擎没有 Docker Desktop 那个图形界面但命令用法完全一样。以 Ubuntu 为例先更新索引然后装依赖sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release接着添加 Docker 官方 GPG 密钥和软件源这一步直接决定了你拿到的包是不是最新版。很多教程会让你顺手装docker.io那个包也能用但版本经常滞后真到了用新特性的时候会卡住。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 echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null然后再 update 一次安装sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-pluginCentOS 7 这类老系统也类似只是包管理器换成了yum需要先添加 yum repo再yum install -y docker-ce。装完之后一定要记得设置开机自启sudo systemctl enable docker --now验证一下docker version sudo docker run hello-world能输出 “Hello from Docker!” 就算通。2.3 镜像下载慢先配好镜像加速装完第一件事不是急着拉 nginx、mysql而是先配镜像加速。原因不用多解释默认的 Docker Hub 在全球分发咱们这边的网络往往拉得很慢几 GB 的镜像能拖到怀疑人生。Docker 支持配置 registry mirror在/etc/docker/daemon.jsonWindows 上是 Docker Desktop 设置里的 Docker Engine 配置里这样写{ registry-mirrors: [ https://docker.mirrors.ustc.edu.cn, https://hub-mirror.c.163.com ] }提示不同时间段、不同运营商的网络某一镜像源的可用性和速度会有差异。如果发现某个源拉不动了就换一个。很多云厂商的控制台里也会提供专属加速地址直接用那个更稳定。改完重启 Dockersudo systemctl restart docker再去拉镜像速度快一大截。这一步不做好后面所有实操都会受阻。2.4 装完环境先查这几个状态环境配好之后建议养成两个习惯。一是docker version之后运行docker info看看 Docker 根目录、存储驱动、镜像加速是否都正常。二是新开一个终端执行docker ps如果看到 “permission denied” 之类错误说明权限还没配好见后面的排查章节。3. 从零跑起你的第一个容器3.1 拉镜像、跑容器nginx 来体验一下安装好了先跑一个最简单的 nginxdocker run -d --name web -p 8080:80 nginx:alpine这条命令干了这么几件事-d后台运行--name web给容器起名叫 web-p 8080:80把宿主机 8080 端口映射到容器里的 80 端口nginx:alpine是镜像名加标签alpine 是它的一个精简版本体积小。打开浏览器访问http://localhost:8080能看到 nginx 默认欢迎页就说明容器跑起来了。这时候一个常见疑问是为什么是 8080:80反过来写行不行-p的格式是“宿主机端口:容器端口”。你宿主机可能已经有服务占了 80所以映射到 8080。冒号左边是你实际要访问的端口右边是进程在容器里监听的端口。这个顺序记错排查半天都连不上服务。再看几条基本命令docker ps # 查看运行中的容器 docker ps -a # 查看所有容器包括已退出 docker logs web # 查看容器日志 docker exec -it web sh # 进入容器内部拿到 shelldocker exec -it web sh是调试利器进了容器你想看啥都行。先exit退出容器然后我们停掉它docker stop web docker rm web3.2 端口映射和数据卷为什么容器会“丢数据”很多新手第一次用容器跑了个应用在里面写了个文件然后容器删了重建文件没了于是惊呼容器不持久。这个认知本身并不错容器确实不擅长存数据。原因是容器有自己的可写层这个层和容器生命周期绑定。容器删了可写层也被销毁。想跨容器持久化数据得用数据卷Volume或目录挂载Bind Mount。数据卷是由 Docker 管理的目录挂载名字docker run -d --name mysql-local -v mysql-data:/var/lib/mysql mysql:8.0这样即使容器被删mysql-data这个卷里的数据也在下次用同一个卷名挂载新容器即可。目录挂载是把宿主机某个目录直接映射进去docker run -d --name web -v /home/user/html:/usr/share/nginx/html nginx:alpine这种情况下你改宿主机上的 html 文件容器内立刻生效非常适合开发期调试。一句话记住容器是一次性饭盒数据卷才是你家冰箱。3.3 常用命令速查命令作用常用参数docker pull 镜像名拉取镜像加 tag 指定版本如mysql:8.0docker run 镜像名创建并运行容器-d后台、-p端口映射、-v挂载、--name命名、-e环境变量docker ps查看运行中的容器-a包含已退出的容器docker logs 容器名查看容器日志-f跟随实时输出docker exec -it 容器名 sh进入容器内执行命令也可执行其他命令如mysql -uroot -pdocker stop/start/restart 容器名停止 / 启动 / 重启容器docker rm 容器名删除容器删除前最好先停docker images查看本地镜像docker rmi 镜像名删除镜像有容器在用它时得先删容器docker system prune清理孤儿资源和无用数据-a连未使用的镜像一起清慎用这个表格里的命令不需要背用的地方多了自然就记住了。真正要理解的是-p、-v和-e这三个参数它们是所有常用镜像的通用入口。4. 真实项目落地MySQL 和 Redis 主从4.1 部署 MySQL 8.0参数一个都不能少光跑 nginx 没感觉来部署一个真正的数据库。MySQL 8.0 镜像非常典型因为它涉及密码、字符集、数据持久化、时区等多个维度。最基本的一条命令docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourPass123 \ -v mysql-data:/var/lib/mysql \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci这里解释几个关键点。MYSQL_ROOT_PASSWORD是初始化时设置 root 密码的环境变量。如果不设容器首次启动会因为不知道密码而直接退出。这是镜像约定的行为很多镜像都会在文档里写明需要的 -e 参数跑之前先去 Docker Hub 页面看一眼。-v mysql-data:/var/lib/mysql是数据卷MySQL 数据文件默认存在容器内的这个目录。不加这个参数容器一删数据全没前面讲过的坑就在这。末尾的两行参数不是 Docker 的参数而是传给 MySQL 服务的mysqld参数。Docker run 命令在镜像名之后的参数会被当作容器的启动命令参数传入。8.0 默认字符集是 utf8mb4但显式指定是我个人非常推荐的习惯因为协作时数据库的字符集规则要非常明确中文场景下 utf8mb4 是必须的。启动后进入容器确认docker exec -it mysql8 mysql -uroot -p输入密码执行SHOW VARIABLES LIKE character_set_server;看到 utf8mb4 就对了。这里还要提醒一个实际问题生产环境不要把 root 用于远程连接。虽然本地测试没问题但习惯要养好。先创建一个专用账号CREATE USER appuser% IDENTIFIED BY AppPass456; GRANT ALL PRIVILEGES ON appdb.* TO appuser%; FLUSH PRIVILEGES;然后 Navicat、DataGrip 这类客户端就连appuser别碰 root。4.2 Redis 主从从单机到读写分离Redis 部署本身很简单docker run -d --name redis-server -p 6379:6379 redis:7.0但只有一台 Redis一旦宕机整个缓存层就没了。所以主从是大多数实际场景的刚需。用容器搭主从其实比裸机更简单因为不用提前装 Redis、不用处理系统差异。先跑一个主节点docker run -d --name redis-master -p 6379:6379 redis:7.0再跑一个从节点通过命令参数让它在启动后同步主节点docker run -d --name redis-slave -p 6380:6379 redis:7.0 \ redis-server --replicaof 宿主机IP 6379这里有个隐性问题主从之间通信容器里的localhost指的是容器自己不是宿主机。所以从节点要连主节点不能用127.0.0.1得用宿主机的局域网 IP。在宿主机上执行ip addr或ipconfig查一下把那个 IP 填进去。验证是否同步成功docker exec -it redis-slave redis-cli info replication重点关注role:slave和master_link_status:up。如果看到down大概率是 IP 填错了。注意新版 Redis 用replicaof替代了老版本的slaveof虽然老的还能用但新项目直接写新的省得以后踩坑。4.3 用 docker compose 一键编排多容器MySQL、Redis 分开跑没问题但真实项目往往是前端、后端、MySQL、Redis、消息队列好几个进程要一起启动、一起配置网络。这时候单独 docker run 就太繁琐了docker compose 就是干这个的。假设一个最简业务栈一个后端应用我们先用 nginx 代替、一个 MySQL、一个 Redis。写一个docker-compose.ymlservices: app: image: nginx:alpine ports: - 8080:80 depends_on: - mysql - redis mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: YourPass123 volumes: - mysql-data:/var/lib/mysql redis: image: redis:7.0 volumes: mysql-data:然后一条命令启动整个栈docker compose up -d查看所有服务状态docker compose ps停止docker compose downcompose 的核心价值是让容器之间通过服务名互相访问。比如上面配置里如果后端应用要连 MySQLURL 直接写jdbc:mysql://mysql:3306/db即可。compose 会自动创建自定义网络并且让服务名作为 DNS 可解析。这一点非常香解决了容器间通信的大麻烦。需要注意的是新版 compose 命令是docker compose中间有空格旧版需要单独装docker-compose工具写法是docker-compose。现在大部分环境都支持前者如果你的系统提示找不到命令再考虑装 compose-plugin。5. 进阶把自己的项目也装进容器5.1 写第一个 Dockerfile跑别人做好的镜像只是入门真正把 Docker 用起来得学会把自己的项目打包成镜像。核心就是一个叫Dockerfile的文件。用一个非常小的 Flask 应用举例。假设项目结构myapp/ app.py requirements.txtapp.py内容from flask import Flask app Flask(__name__) app.route(/) def hello(): return hello dockerrequirements.txt内容flask3.0.0Dockerfile 写在同一目录下FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY app.py . EXPOSE 5000 CMD [python, app.py]逐行解释。FROM指定基础镜像这里用的是带 Python 3.11 的 Debian 精简版。WORKDIR设置工作目录后面命令都在这个目录下执行。COPY把本机文件拷进镜像。RUN构建阶段执行的命令这里装依赖。EXPOSE声明容器要监听的端口注意它只是声明真正对外发布端口还是要靠-p。CMD是容器启动时执行的命令。构建docker build -t myapp:v1 .注意最后有个点指的是 Dockerfile 所在目录的上下文路径。运行docker run -d --name myapp -p 5000:5000 myapp:v1浏览器访问http://localhost:5000看到 “hello docker”你的项目已经容器化了。5.2 多阶段构建前端项目打镜像的正确姿势如果你做的是前后端分离项目前端构建会牵扯到 node 环境、打包工具链如果这些体积全塞进行运行时镜像里镜像大得离谱。多阶段构建是标准解法。FROM node:18 AS build WORKDIR /web COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --frombuild /web/dist /usr/share/nginx/html EXPOSE 80 CMD [nginx, -g, daemon off;]第一阶段用 node 镜像把代码构建成静态文件第二阶段用一个轻量 nginx 镜像把第一阶段产出的dist目录拷进去。最终镜像里只有 nginx 和静态文件没有 node 环境体积小很多攻击面也小。这种思路是所有生产级前端镜像的标配。这里必须提一下.dockerignore文件作用和.gitignore类似。把node_modules、dist、__pycache__、日志文件等忽略掉否则会把一大堆没用的文件打进构建上下文既慢又占内存。5.3 微服务项目部署思路有了基础之后你会看到很多项目在仓库里直接带了 Dockerfile 和 docker-compose.yml比如 Dify、GitLab 社区版、青龙面板这类工具型项目官方都提供了现成镜像拉起来就能用。微服务项目一般也是这样处理每个服务单独一个镜像所有服务写进同一个 compose 文件用环境变量控制不同环境的差异。这个阶段最需要补的是镜像版本管理。我的习惯是打上带版本号的 tag而不是一直用latestdocker build -t myapp:1.0.0 . docker tag myapp:1.0.0 myapp:latest推到仓库docker login docker push 你的账号/myapp:1.0.0推上去之后别的机器只需要docker pull 你的账号/myapp:1.0.0就能拿到同样的镜像环境一致性到这里才算真正闭环。6. 常见问题排查与避坑实录6.1 Docker Desktop 启动失败的排查链路现象一Windows 上 Docker Desktop 启动后提示 virtualization support not detected。排查链路从上到下任务管理器“性能”页看虚拟化是否启用如果显示未启用进 BIOS 找虚拟化开关并打开如果已经启用看是否安装了 WSL2 内核更新包Windows 商店里搜 “WSL” 更新一下再看 Docker Desktop 设置里后端是否为 WSL2。现象二连接错误failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine。这个通常不是 Docker 本身坏了而是 Docket Desktop 的 Linux 虚拟机没起来。最简单的办法彻底退出 Docker Desktop在 PowerShell 里执行wsl --shutdown然后重新打开 Docker Desktop等右下角状态变绿。如果有多个 WSL 发行版确认你默认使用的发行版能和 Docker 正常配合。6.2 Linux 下 Docker 权限问题安装完 Docker 后每次用 docker 命令都要在前面加 sudo很麻烦。原因是 Docker 守护进程绑定了 root 权限的 socket只有 root 和 docker 组里的用户能访问。把自己加入 docker 组sudo usermod -aG docker $USER newgrp docker之后当前用户就不用 sudo 了。注意加了 docker 组的用户等同于拥有 root 权限因为 docker 可以挂载宿主机目录。个人开发机没问题生产环境做权限隔离时要特别慎重。如果执行 docker 命令报 “Cannot connect to the Docker daemon”先看守护进程是否还活着systemctl status docker journalctl -u docker -n 50卸载重装是最后手段多数时候是服务没起来或启动时遇到了配置错误日志里都会写。6.3 容器网络不通的几个高频场景新手最常见的网络问题在容器里访问宿主机上的服务用localhost连不上。容器里的 localhost 是容器自己不是宿主机。要访问宿主机需要用它给你的网关地址在容器内执行ip route看默认网关或者直接用局域网 IP。另一个问题是两个容器互相访问。上面已经提过 compose 可以用服务名。不用 compose 时需要手动在同一个自定义网络里跑容器并且用容器名互相访问docker network create app-net docker run -d --name redis --network app-net redis:7.0 docker run -d --name app --network app-net myapp:v1此时 app 容器里访问 Redis直接访问redis:6379而不是 IP。6.4 镜像拉取超时、下载中断即使配了镜像加速偶尔还是会遇到拉取失败要么是某个特定镜像只存在于 Docker Hub要么是网络抖动。我的处理经验是三步走。第一步确认加速配置生效docker info里能看到 Registry Mirrors 列表。第二步换个镜像源。加速地址可以多放几个配置会按顺序尝试。第三步重试。镜像拉取支持断点续传重新执行同样的 pull 命令很多时候接着上次进度继续不用从零开始。6.5 其它高频小坑时区问题。镜像默认是 UTC 时间容器里date看到的比北京时间早八小时。简单解法是启动时挂载宿主机时区文件docker run -v /etc/localtime:/etc/localtime:ro ...磁盘空间暴涨。镜像、容器、数据卷都占地方。定期清理docker system df # 看空间分布 docker system prune -f # 清理无用资源容器删了数据没了。老规矩没挂数据卷就别指望数据活着。生产数据库必须挂卷并且定期备份卷里的目录。latest 标签的教训。latest 不代表版本只代表“最新的”这个镜像今天指向 1.0明天可能就变成 2.0。生产环境要么拉指定版本 tag要么构建后固定 tag不然升级发生在你没准备的时候。最后再分享一个我自己的使用习惯给项目里所有容器统一配上restart: unless-stopped这样服务器重启后服务能自动起来省掉不少半夜被喊起来拉服务的麻烦。我见过太多人学 Docker 卡在环境安装和网络配置上其实这两个东西理解了后面就是按部就班。遇到不会用的镜像先去它的 Docker Hub 官方页面读文档那里有最精准的环境变量和挂载点说明比自己猜参数省太多时间。