我第一次被Docker“教育”是在一次周末加班部署的时候。项目要上线环境是CentOS 7JDK、MySQL、Redis、Nginx一样一样手动装装到一半发现版本不对卸载重来前后折腾了大半天。后来同事跟我说“你试试把整个环境打个包跑在容器里。”我心想这不就是个轻量虚拟机吗结果真正用起来才发现根本不是一回事。这篇文章基本算是我多年Docker使用经验沉淀下来的一套笔记。网上流传的那份“狂神说Docker”笔记我一直留着刚入门时就是跟着它跑通了第一个容器非常感激后来在生产环境踩了不少坑我又在原笔记基础上补了大量实操细节和故障排查经验。今天全部分享出来适合刚接触容器的新手也适合已经用了一段时间、但没系统梳理过命令和原理的人。内容比较长建议先收藏遇到问题再回来翻。先把我最核心的体会放在前面Docker不难难的是理解它的设计思想。一旦想明白“镜像只读、容器可写、数据靠卷、通信靠网络”这四句话后面所有命令和配置都是顺水推舟。1. 装好Docker这一关Windows和Linux双平台避坑实录装Docker看起来是最简单的一步但“virtualization support not detected”和“Docker Desktop failed to start”这两个错误常年霸占Docker相关搜索榜前列。问题不是Docker本身难装而是前置条件太容易被忽略。1.1 Windows装Docker Desktop前先确认虚拟化开关Docker Desktop在Windows上有两种后端老版本用Hyper-V新版本默认走WSL2。不管哪一种前提都是CPU虚拟化必须开启。很多人启动Docker Desktop后看到虚拟化报错第一反应是重装其实问题基本出在BIOS或Windows功能上。排查步骤按这个顺序来打开任务管理器 → 性能 → CPU看右下角“虚拟化”是否显示“已启用”。如果显示“已禁用”重启进BIOS找到Intel Virtualization Technology或SVM ModeAMD平台设为Enabled。确认BIOS没问题后检查Windows功能里是否开启了“适用于Linux的Windows子系统”和“虚拟机平台”。路径是控制面板 → 程序 → 启用或关闭Windows功能。如果用的是老版本Docker Desktop还需要勾选“Hyper-V”同时注意别和其他虚拟机软件抢资源。有个很容易被忽略的细节某些主板的BIOS虚拟化选项藏得很深比如部分品牌台式机在Advanced → CPU Setup里有的需要开机按F2而不是Del。实在找不到就直接用主板型号搜索“怎么开启VT”别硬猜。1.2 WSL2模式修复的最后一招如果你确定BIOS和Windows功能都开了Docker Desktop仍然报虚拟化问题有一种常见情况是WSL2本身没装好。打开PowerShell管理员执行wsl --install wsl --set-default-version 2然后重启电脑再打开Docker Desktop。另外老版本Windows 10跑WSL2会有一堆莫名其妙的问题建议直接把系统升到较新版本省心很多。1.3 Ubuntu与CentOS的安装命令差别Linux下安装Docker相对简单但要注意“系统自带源里的版本”和“Docker官方源里的版本”是两个概念。Ubuntu上很多教程直接让执行apt install docker.io这个包确实能用但版本往往落后。我更推荐用官方源安装sudo apt update sudo apt install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginCentOS 7虽然官方已经不再维护但存量机器还很多升级到新版Docker用这套sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io装完以后有个习惯必须建立起来Docker服务默认不会开机自启。所以每次配置完/etc/docker/daemon.json这类文件记得用下面两条命令收尾sudo systemctl enable docker sudo systemctl start docker1.4 权限报错的根源是用户组很多Linux新手装完Docker执行docker ps直接收到permission denied原因很简单docker命令需要root权限而当前用户不在docker组里。解决办法不是每次都加sudo而是把用户加进docker组sudo groupadd docker sudo usermod -aG docker $USER newgrp docker这里必须提醒一句能加入docker组的用户基本等同于拥有root权限因为Docker可以把宿主机任意目录挂载进容器。多人共用的生产服务器上不要随便给普通用户加这个组。2. 镜像和容器这两个概念搞明白Docker就入门了一半2.1 菜谱和菜的关系很多人一开始把Docker想象成虚拟机这个类比害人不浅。虚拟机里装的是完整操作系统每个虚拟机都是一台独立电脑而Docker容器共享宿主机内核它本质上只是一个隔离的进程运行环境。我更愿意用菜谱和菜来打比方镜像就是菜谱加半成品食材包它是只读的里面定义了环境的一切装了什么系统、什么依赖、什么程序、启动命令是什么。容器就是照着菜谱做出来的一道菜你可以往里面加料、改配置但菜谱本身不会变。仓库就是大菜市场里面有别人做好的各种菜谱比如MySQL菜谱、Redis菜谱、Nginx菜谱。用命令来解释更直观docker pull mysql:8.0 # 去仓库下载MySQL 8.0镜像 docker run -d --name mydb mysql:8.0 # 基于镜像创建并启动容器 docker ps # 查看运行中的容器 docker ps -a # 查看所有容器包括已停止的 docker stop mydb # 停止容器 docker rm mydb # 删除容器 docker images # 查看本地所有镜像 docker rmi mysql:8.0 # 删除镜像注意docker rm删除的是容器docker rmi删除的是镜像很多新手在这俩上面栽跟头。2.2 镜像是分层的镜像的另一个重要特性是分层Layer。你pull一个镜像的时候输出里会显示很多层每一层对应一个小改动。比如一个基于CentOS的镜像底层是系统基础层往上可能是JDK安装层再往上才是你的应用jar包层。分层最大的价值在于复用和节省空间。本地已经有一个CentOS基础层再拉其他基于CentOS的镜像基础层就不用重复下载。这也是Docker镜像比虚拟机文件小得多、启动速度快得多的根本原因。2.3 镜像标签里的学问镜像名后面的冒号是标签tagmysql:8.0和mysql:latest是两回事。生产环境强烈建议固定tag不要用latest。latest会随着官方更新变化今天拉的和明天拉的可能是不同版本等出了问题你根本不知道线上跑的到底是哪个版本。我见过不止一次因为latest导致MySQL大版本意外升级、配置文件不兼容的线上事故。3. 拉镜像慢的解决方案加速器配置与自建仓库3.1 为什么直连Docker Hub经常超时Docker Hub的镜像服务器在海外国内网络环境下拉取大镜像经常出现进度条长时间不动、或者卡在等待状态。这个问题属于基础设施层面的问题自己换DNS、重启网卡都解决不了。主流思路是给Docker配置镜像加速器或者自建私有仓库。3.2 daemon.json配置加速器Docker守护进程会读取/etc/docker/daemon.json加载加速器配置Windows则在Docker Desktop的Settings → Docker Engine里改同样格式的JSON{ registry-mirrors: [ https://docker.1ms.run, https://docker.xuanyuan.me ] }保存后Linux执行systemctl restart dockerWindows重启Docker Desktop即可生效。这里说明一下加速器只对从Docker Hub拉取的公共镜像生效对私有仓库和其他第三方仓库不生效。而且很多公共加速地址会变动建议多配几个失效了及时更换。3.3 企业内网自建Registry如果团队内部经常要分发同一个镜像与其每个人各自去外网拉不如搭一个内部镜像仓库。用极简的Docker Registry做演示docker run -d -p 5000:5000 --restartalways --name registry registry:2推送和拉取方式docker tag mysql:8.0 192.168.1.100:5000/mysql:8.0 docker push 192.168.1.100:5000/mysql:8.0 docker pull 192.168.1.100:5000/mysql:8.0默认情况下Registry走HTTP协议而Docker守护进程只信任HTTPS的仓库所以要在daemon.json里声明内网HTTP仓库{ insecure-registries: [192.168.1.100:5000] }自建仓库的价值不只是拉取快。生产环境里镜像内容可审计、版本可控出问题可以快速回滚到上一个版本这些在运维中比“拉得快”重要得多。4. 数据持久化容器删了不代表数据还在4.1 容器是“用完即走”的容器被删除后容器内的所有数据也会随之消失因为容器写入层本来就是临时的。这是设计思想不是Bug——容器被定位成“无状态”的进程运行环境。但数据库、配置文件、日志这些必须有状态所以Docker提供了卷Volume和绑定挂载Bind Mount两种持久化方式。4.2 -v挂载和命名卷的区别先看最简单的绑定挂载把宿主机的一个目录映射进容器docker run -d --name mysql8 \ -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD123456 \ mysql:8.0这里/data/mysql是宿主机目录/var/lib/mysql是容器里MySQL的数据目录。容器删了宿主机目录里的数据还在下次重新run一个容器把同一个目录挂载进去数据就回来了。命名卷Named Volume则是把目录托管给Docker管理做有状态服务时更推荐docker volume create mysql_data docker run -d --name mysql8 \ -v mysql_data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD123456 \ mysql:8.0两者区别在于绑定挂载的路径由你在宿主机指定方便直接查看和备份命名卷的路径由Docker管理跨主机迁移时更规范。我生产环境的习惯是数据库用命名卷日志和应用配置用绑定挂载。4.3 MySQL 8.0部署实例与字符集坑热词里大家都在搜“docker安装mysql8.0并使用”说明新手踩坑多。先给一套完整的运行命令docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDRoot123 \ -e TZAsia/Shanghai \ -v mysql_data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0进容器测试连接docker exec -it mysql8 mysql -uroot -pMySQL 8.0默认的认证插件是caching_sha2_password老版本的Navicat连接会报认证失败。如果确实需要兼容老客户端可以改认证方式ALTER USER root% IDENTIFIED WITH mysql_native_password BY Root123; FLUSH PRIVILEGES;还有字符集问题。Docker官方MySQL镜像默认字符集可能是utf8mb4但排序规则不一定符合预期中文业务系统建议启动后执行SET NAMES utf8mb4或在my.cnf里显式配置。4.4 数据库备份与恢复容器里的MySQL备份不用进容器直接在宿主机用docker exec配合mysqldumpdocker exec mysql8 sh -c exec mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --databases yourdb backup.sql恢复cat backup.sql | docker exec -i mysql8 mysql -uroot -p这里有个关键细节备份要用sh -c exec ...的形式而不是直接docker exec mysqldump这样能让mysqldump成为容器内的1号进程避免shell残留导致备份不完整或命令挂起。我早期就因为这个吃过亏备份出来的SQL缺了最后一段恢复系统时才追悔莫及。5. 容器网络端口映射与跨容器通信5.1 -p映射端口的本质容器内部有独立的网络命名空间宿主机要访问容器内的端口必须通过端口映射。docker run -p 3306:3306意思是把宿主机的3306端口映射到容器的3306端口左侧是宿主机端口右侧是容器端口。新手经常问为什么改了容器里的端口外面连不上因为映射关系在创建容器时就固定了要改端口映射只能删了容器重新run。Docker不是路由器端口映射是一条不可热更新的规则。5.2 自定义bridge网络才是容器互通的正解默认情况下容器通过名为bridge的默认网络互联可以用IP互访但IP会随容器重建而变化不稳定。更推荐创建自定义bridge网络让容器之间用容器名互相访问docker network create mynet docker run -d --name redis1 --network mynet redis:7 docker run -d --name redis2 --network mynet redis:7在redis1容器里可以直接用redis2作为主机名连接。Docker内置的DNS会自动完成容器名到IP的解析这是服务发现最简单的实现形态。5.3 实战Redis主从集群热词里有“docker安装redis主从”我分享一下标准做法。先建网络再分别启动主从节点docker network create redis-net docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7 redis-server --appendonly yes docker run -d --name redis-slave1 \ --network redis-net \ redis:7 redis-server --replicaof redis-master 6379进入从节点检查状态docker exec -it redis-slave1 redis-cli info replication看到role:slave且master_link_status:up说明主从正常。整个过程最关键的是--replicaof redis-master 6379里用了容器名而不是IP如果写死IP以后主节点重建、IP一变从节点就会失联。这就是自定义网络相比IP直连的最大优势。5.4 外部访问失败先查防火墙端口映射配好了但外部访问不上很多情况是宿主机防火墙拦截。CentOS 7常见的放行方式firewall-cmd --permanent --add-port3306/tcp firewall-cmd --reload如果是云服务器还要去安全组放行端口。最典型的现象是“本机连得上、外部连不上”第一反应应该是查安全组而不是去容器里翻日志。6. Docker Compose多容器编排的正确打开方式6.1 一条docker run解决不了的问题一个典型Web项目可能需要MySQL、Redis、Nginx、应用服务四个容器。如果都靠手敲docker run命令又长又不直观每次重建环境都要重新敲一遍。Compose的作用就是让容器编排变成“写配置文件 执行up”。6.2 一个可直接套用的docker-compose.yml以部署MySQL 8.0加Redis 7.0为例version: 3.8 services: mysql8: image: mysql:8.0 container_name: mysql8 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: Root123 TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql - ./conf/mysql:/etc/mysql/conf.d networks: - app-net redis: image: redis:7 container_name: redis7 restart: unless-stopped command: redis-server --appendonly yes --requirepass Redis123 ports: - 6379:6379 volumes: - redis_data:/data networks: - app-net volumes: mysql_data: redis_data: networks: app-net: driver: bridge启动命令docker compose up -d docker compose ps docker compose logs -f mysql8新版Docker已内置compose子命令docker compose老环境才需要单独安装docker-compose。如果提示找不到命令用pip install docker-compose或安装docker-compose-plugin包。6.3 restart策略要理解到位restart: always不是“重启容器”的意思而是“无论容器以什么方式退出Docker守护进程都会把它拉起来”除非明确docker stop。对于有状态服务我建议用restart: unless-stopped。两者唯一差别在于手动docker stop之后unless-stopped不会自动拉起而always会在Docker服务重启后又把它拉起来。这个区别在“故意停掉一个服务维护”的场景里非常重要。6.4 敏感信息交给.env管理compose文件里写明文密码提交到Git仓库等于把数据库密码公之于众。生产环境至少用.env文件管理# .env MYSQL_ROOT_PASSWORDChangeMe123 REDIS_PASSWORDChangeMe456compose文件里用${MYSQL_ROOT_PASSWORD}引用然后在.gitignore里忽略.env。进一步可以上Docker Secrets或专门的密钥管理服务但对中小团队来说先做到.env不入库已经是很大进步。多说一句很多从GitHub拉下来的开源项目比如Dify这种代码里通常有一个.env.example模板文件部署前第一步就是复制成.env再执行docker compose up -d。很多人部署失败不是命令写错而是漏了cp .env.example .env这一步。7. 自己写Dockerfile把项目变成镜像7.1 核心指令速览Dockerfile是构建镜像的配方文件。最常用的指令有这几个FROM基础镜像一切从这里开始WORKDIR设置工作目录COPY把文件复制进镜像RUN构建时执行的命令EXPOSE声明容器监听端口ENV设置环境变量CMD/ENTRYPOINT容器启动后的默认命令以 Spring Boot 项目为例FROM eclipse-temurin:17-jdk WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]构建docker build -t myapp:v1 .7.2 多阶段构建瘦身Java项目构建需要JDK和Maven运行只需要JRE。如果只用一个基础镜像构建产物里会混入大量编译器内容镜像体积轻松上500MB。多阶段构建可以解决这个问题# 第一阶段构建 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /build/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]构建完用docker images查看运行镜像会小很多。瘦身不是洁癖体积小的镜像拉取快、启动快、攻击面小生产环境收益非常明显。7.3 IDEA一键打包Docker镜像热词里有“idea 打包docker镜像”说明很多Java开发想省掉命令行构建的麻烦。IDEA的做法是在File → Settings → Build, Execution, Deployment → Docker里连接本机或远程Docker。在运行配置里新建Dockerfile运行配置指定Dockerfile路径和镜像名。点击运行IDEA自动完成build。有一点注意如果是远程DockerIDEA构建时需要把上下文目录上传到远程服务器项目大了会慢。这种情况更推荐在CI流水线里构建本地只负责开发和推代码。7.4 微服务部署的常见误区“docker部署微服务项目”这个关键词很火。微服务容器化不是简单地把每个服务打成镜像就完事还有服务发现、配置中心、网关、日志收集一堆问题要处理。如果只有四五个服务用Compose加自定义网络就够如果有几十个服务就要考虑Kubernetes这类编排平台。但上手K8s之前先把Docker本身的网络、卷、健康检查吃透不然直接跳进K8s只会更晕。8. Docker Desktop故障排查手册8.1 “Failed to connect to the docker api at npipe”型错误Windows上Docker Desktop突然连不上报错信息出现npipe:////./pipe/dockerDesktopLinuxEngine多半是Docker引擎挂了不是Docker Desktop整体坏了。常见修复顺序右键Docker Desktop托盘图标选择Restart。不行就退出Docker Desktop打开PowerShell执行wsl --shutdown再重新启动。再不行在Settings → Troubleshoot里点Clean / Purge data这会重置Docker的WSL发行版数据一般能救回来但也会清掉Docker Desktop默认卷里的数据。如果你之前把数据都挂载到宿主机目录或命名卷里第三步就没那么可怕。这也是我一直强调数据必须持久化的原因之一。8.2 “unexpected EOF”类拉取中断docker pull到一半报unexpected eof基本是两种原因网络不稳定导致传输中断或者镜像加速器超时。处理手段是换一个更稳定的加速器重新pull。大镜像本身就是分层下载的中断后重新执行会续传已下载的层不用太担心。8.3 Docker磁盘占用爆炸Docker用久了Windows的C盘会被镜像和构建缓存占满。先看占用docker system df清理docker system prune -a这个命令会删掉所有未使用的镜像和构建缓存执行前注意看输出确认没删错。我平时用更温和的组合docker image prune -a清理无用镜像docker builder prune清理构建缓存按需执行避免把还在用的镜像误删。8.4 WSL2网络卡顿导致容器内DNS异常另一种低频但很折磨人的情况是容器里能ping通IP但解析不了域名同时宿主机网络一切正常。这时候先怀疑WSL2的虚拟网络适配器卡死了。PowerShell执行wsl --shutdown等几秒再启动Docker Desktop绝大多数能恢复。如果频繁出现可以在用户目录下的.wslconfig里给WSL2限制内存避免Docker和Windows抢资源。9. 特定场景与冷门问题从KODBOX到国产环境9.1 网盘类轻应用KODBOX与DVWA的“一条命令”价值docker部署kodbox是很常见的家庭或小团队需求。KODBOX是网盘应用容器化部署在群晖或任意Linux服务器上都很方便数据目录挂载出来就能用docker run -d \ --name kodbox \ -p 8080:80 \ -v /data/kodbox:/var/www/html \ kodbox/kodbox注意数据卷要挂到宿主机足够大的磁盘上网盘的容量是命根子。另外如果接了反向代理记得把WebSocket相关配置带上否则在线预览大文件会断连。类似地安全学习常用的DVWA靶场也能一条命令拉起docker run -d -p 8080:80 vulnerables/web-dvwa这类实验环境用完即删容器化“用完即走”的价值体现得淋漓尽致。9.2 GitLab这种“重量级”选手资源规划比命令更重要GitLab的容器化部署也是一个高频搜索词。gitlab-ce镜像有好几个GB内存建议至少4G2G也能跑但明显卡顿。典型命令docker run -d \ --name gitlab \ -p 8022:22 -p 8080:80 \ --restart unless-stopped \ -v gitlab_config:/etc/gitlab \ -v gitlab_logs:/var/log/gitlab \ -v gitlab_data:/var/opt/gitlab \ gitlab/gitlab-ce:latest部署后要修改/etc/gitlab/gitlab.rb里的external_url改成实际访问的域名或IP否则clone地址会不对。首次启动初始化很慢docker logs -f gitlab里看到“gitlab Reconfigured!”才算完成别以为卡住了。9.3 人大金仓数据库与龙芯架构兼容性要先确认人大金仓数据库在政企场景用得不少官方提供了Docker镜像部署方式和其他关系型数据库类似但特别要注意授权文件的位置通常需要通过挂载方式提供给容器。容器化本身不复杂复杂的是授权协议和厂商的部署要求。很多情况下不是技术不允许而是许可证不覆盖容器场景所以项目初期就要确认“是否允许容器化部署”这个前提。热词里还有“龙芯 docker”。龙芯是LoongArch架构和常见的x86_64、ARM64都不一样很多镜像没有提供LoongArch版本pull时会报not found manifest。解决办法通常是到镜像仓库搜是否有loongarch64标签的镜像。官方没有就基于支持LoongArch的基础镜像自己构建。很多开源社区已经有人在做龙芯适配直接搜“镜像名 loongarch”关键词。跨架构问题的根本就是镜像和宿主机CPU架构必须匹配理解了这一点排查思路就清晰了。9.4 定时任务类面板的依赖管理用Dockerfile固化依赖热词“docker青龙 依赖管理”反映了很多人在Docker里跑定时任务面板的困扰。这类面板本质是脚本执行器容器里需要各种运行时依赖。常见的错误做法是图方便直接在容器里装docker exec -it qinglong bash -c npm install -g xxx这样调试确实快但容器一重建依赖全没了。正确的做法是用Dockerfile构建自定义镜像把依赖固化进去FROM whyour/qinglong:latest RUN npm install -g xxx pip install xxx构建成自己的镜像后再跑容器无论容器怎么重建依赖都在。我的观点一直很明确凡是需要容器环境提供的东西全部用Dockerfile固化这才叫可复现的部署。最后分享几点我自己沉淀下来的经验第一数据卷的命名规范要提前定好。我见过很多团队的volume名字乱七八糟MySQL的数据叫dataRedis的也叫data时间一长根本分不清哪个卷对应哪个服务。建议统一用项目名_服务名_data的格式。第二每一条docker run命令最好留档。如果你习惯用命令启动容器就把命令保存成shell脚本或者写成compose文件别只躺在终端历史里。不然半年以后想查某台机器的MySQL当初是用什么参数启动的根本无从下手。第三容器起不来、日志为空、排查无从下手时按这个顺序走先docker logs看输出再docker inspect看状态最后检查挂载目录的权限。绝大多数启动失败都出在这三件事上别一上来就删容器重建那样永远学不到根因。这次先分享到这儿。Docker的内容远不止这些镜像安全扫描、CI/CD集成、Kubernetes编排都还有大量可挖的东西。后面我会把微服务编排和灰度发布的实战经验陆续整理出来感兴趣的话可以留意后续更新。遇到具体问题也欢迎在评论区把你的docker run命令或完整报错贴出来我们一起看看问题到底出在哪。