
1. 安装Docker的第一步虚拟化、WSL2与权限那些坎如果你搜过Docker安装大概率看到过这几类热门问题virtualization support not detected、permission denied while trying to connect to the docker api、docker desktop failed to start because virtualisation support wasnt detected。没错Docker入门的第一个大坑根本不在命令本身而在环境能不能把Docker跑起来。我当年第一次在Windows上装Docker Desktop双击图标就给我弹了个VT-x is not enabled的报错当时一脸懵。后来才明白Docker在Windows上本质是靠虚拟化技术跑一个Linux虚拟机所以宿主机必须开启硬件虚拟化支持。你在BIOS/UEFI里找到Intel VT-x或AMD SVM相关的选项把它从Disabled改成Enabled保存重启这一步能解决80%的启动失败问题。1.1 Windows环境先别急着下载Docker DesktopWindows 10/11建议优先开启WSL2流程并不复杂以管理员身份打开PowerShell执行wsl --install装完后重启系统命令行里输入wsl --status确认WSL版本已经是2。再去Docker官网下载Docker Desktop安装包。安装过程中会检测WSL2是否就绪选上Use WSL 2 instead of Hyper-V这个选项如果系统支持的话。这里有个容易被忽略的点如果你用的是老版本Windows 10比如1903之前的版本wsl --install可能不认。那种情况建议先把系统更新到21H2以上省得折腾旧版的WSL手动安装流程。Windows 11用户基本没有这个烦恼装完就能用。还有一类报错特别坑Docker Desktop能启动但容器一跑就卡住或者拉镜像到一半就断。如果系统开了代理类软件Docker Desktop的网络模块经常和它打架。我的处理办法是在Docker Desktop的Settings - Resources - Proxies里手动指定代理地址而不是让Docker自动检测。这个坑在搜索词里虽然没有直接出现但很多下载失败、网络不通其实都是它引起的。提示安装完Docker Desktop后如果右下角小鲸鱼图标一直是starting...状态超过几分钟先别急着重装。去看看事件查看器里的Docker服务日志多半是WSL内核版本太旧在PowerShell里跑wsl --update就能解决。1.2 Linux环境包管理器安装与换源Linux上的安装相对清爽以Ubuntu/Debian系为例# 移除可能存在的旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release # 添加官方GPG密钥与仓库 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 安装 sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-pluginCentOS/RHEL系则是用yum install -y yum-utils加仓库源再装套路大同小异。安装完成后先别急着跑容器执行sudo systemctl enable --now docker把开机自启和当前启动一起搞定。装完后验证一下docker version应该能同时看到Client和Server的版本号。如果只有Client、没有Server信息说明daemon没跑起来看看systemctl status docker的输出再对症下药。1.3 权限问题的根因docker组与socket搜索词里permission denied while trying to connect to the docker api出现频率极高现实情况是90%的新手都遇到过。你明明装好了Docker执行docker ps却报这个错原因很简单Docker daemon的socket文件/var/run/docker.sock需要root权限而当前用户不在docker组里。解决办法# 把当前用户加入docker组 sudo usermod -aG docker $USER # 重新登录或者刷新当前会话的组权限 newgrp docker然后docker ps就不需要sudo了。这背后有个安全逻辑值得说一句加入docker组等于把本机的root权限拱手让出一半因为Docker容器可以挂载宿主机的任意目录。所以生产服务器上别图省事把所有账号都丢进docker组没准哪天就被利用提权了。2. 镜像管理先学会拉镜像再谈容器环境装好了下一步就是解决镜像从哪来的问题。Docker镜像可以理解成一个只读的安装包模板容器则是这个模板跑起来后的实例。基础命令的中心就是围绕这两个对象展开的。先列一套镜像操作的核心命令这些够你日常用# 查看本地已有的镜像列表 docker images # 从远程仓库拉取镜像不带tag默认拉latest docker pull nginx:1.25 # 搜索镜像其实更多人在网页上搜 docker search mysql # 给镜像打标签新标签 同一个镜像ID的另一个别名 docker tag nginx:1.25 myregistry/nginx:backup # 删除镜像-f是强制删除 docker rmi nginx:1.25 # 查看镜像分层信息 docker history nginx:1.252.1 镜像与容器的关系模板与实例很多初学者把docker images和docker ps -a搞混。我习惯用一个类比镜像好比一个操作系统的安装光盘容器就是基于这张光盘装出来的电脑。你可以用同一张光盘装出无数台电脑每台电脑里的文件随便改光盘本身永远不变。删除镜像不会影响正在运行的容器但如果你删掉了镜像再想从那个镜像里启动新容器就得重新拉了。docker pull背后的分层机制值得了解Docker镜像是由多个只读层堆叠的nginx和mysql这种官方镜像很多基础层是共享的。所以你会发现拉完一个镜像后再拉另一个镜像瞬间完成——因为大部分依赖层已经在本地缓存了。这也是docker history能看到每一层内容的原因。2.2 镜像下载慢的解决思路docker镜像下载慢是搜索词里的老面孔。如果说拉镜像像下载游戏更新包Docker默认走的那个海外仓库源在国内访问的速度经常让人抓狂。常规解法是给Docker配置registry mirror镜像源在/etc/docker/daemon.json里写sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json -EOF { registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com ] } EOF写完重启Dockersudo systemctl daemon-reload sudo systemctl restart dockerdocker info里如果能看到Registry Mirrors字段列出了你配置的地址说明生效了。说实话源也不是百分百稳定有时候这个源挂了换另一个源就好。另外拉小镜像的时候网络波动容易超时可以用docker pull --platform linux/amd64指定架构避免拉到错误平台的镜像白白浪费流量。注意daemon.json文件必须是标准的JSON格式注释不能写末尾逗号不能多。我见过不少人在这里少写一个逗号整个Docker daemon直接起不来。2.3 镜像的tag管理与删除细节docker tag这个命令看着简单实际特别实用。我维护测试环境时经常需要保留多个版本的镜像比如app:v1.0.1和app:v1.0.2打完新包后直接改tag指向然后docker rmi旧tag释放空间。注意docker rmi删除的是tag引用如果同一个镜像ID还有别的tag指向镜像本体不会真的被清除。用docker images -a能看到所有镜像ID真正想清理孤儿镜像需要用docker image prune。还有一个细节docker rmi如果报image is being used by stopped container说明有已经停止的容器还在引用它。这时候要么docker rm删除容器要么直接docker rmi -f强删。我建议先删容器再删镜像养成好习惯因为-f强删在某些版本下可能留下悬空镜像。3. 容器跑起来之后生命周期、日志与进出交互镜像只是材料的准备阶段Docker的核心操作都在docker run这条命令上。它也是拆解点最多的命令参数排列组合能写一篇文章。一个典型的MySQL 8.0启动命令大概是这样的docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEtestdb \ -v mysql_data:/var/lib/mysql \ --restartalways \ mysql:8.0四个关键参数分别干四件事-d后台运行--name给容器起名-p把宿主机3306端口映射到容器3306-e传入环境变量初始化MySQL的root密码和初始库-v挂载数据卷保证数据不丢--restartalways让容器在Docker重启后自动拉起。生产环境的MySQL容器基本就是这套模板。3.1 docker run参数逐项拆解-ddetach是最常见的后台运行方式不加它容器会在前台跑CtrlC退出后容器就停了。想快速调试时可以故意不加-d直接看输出方便定位问题。-p的参数格式是宿主机端口:容器端口。容器端口是容器内部服务真正监听的端口比如nginx是80、MySQL是3306宿主机端口是外部访问用的。你可以把3306映射成33060:3306外面连33060也能到达容器里的MySQL这么做的好处是可以绕开宿主机3306端口被占用的冲突。-e用来传环境变量不同镜像的变量名要查文档。MySQL认MYSQL_ROOT_PASSWORDPostgreSQL认POSTGRES_PASSWORDRedis主从则常用REPLICAOF之类的参数。新手最常犯的错是给MySQL镜像乱传环境变量导致root密码初始化失败然后一遍遍mysql容器一直重启。--restartalways这个参数我强烈建议加。不加的话服务器一重启你的业务容器全部是Exited状态得手动一个个docker start。除非你是临时调试的容器否则一律--restartalways。3.2 启动、停止、重启与删除容器生命周期的基础命令必须肌肉记忆# 启动一个已存在但停止的容器 docker start mysql8 # 停止容器给进程一个优雅退出的机会 docker stop mysql8 # 强制杀掉容器 docker kill mysql8 # 重启容器 docker restart mysql8 # 删除容器必须先停止或者加-f强删 docker rm mysql8 # 删除所有已停止的容器 docker container prunedocker stop发的是SIGTERM信号容器里的应用能收到并做清理工作docker kill直接发SIGKILL相当于拔电源。日常操作先用stop实在扛不住才用kill。我排障的时候为了彻底重置环境经常docker rm -f一把梭反正数据都挂在数据卷里容器本身无所谓。3.3 进入容器的三种姿势调试阶段最核心的需求是进容器里看看。# 方式一在新容器里执行一次性命令 docker run --rm -it nginx:1.25 bash # 方式二在运行中的容器里打开交互shell docker exec -it mysql8 bash # 方式三附着到容器的主进程慎用 docker attach mysql8docker exec -it是最常用的。-it是-i和-t的组合-i保持标准输入打开-t分配一个伪终端。少了-t你进去之后写命令会像在无TTY环境下执行各种乱码和奇怪行为少了-i你根本输不了命令。docker attach和exec的区别在于attach附加到容器的主进程PID 1如果你CtrlC退出会直接把容器主进程杀掉exec则是另起一个子进程进去退出不影响主进程。所以日常排障一律用exec只有需要观察主进程输出时才用attach。进容器之后第一件事我一般是cat /etc/os-release确认系统版本然后ps aux看看进程状态。官方镜像很多是基于精简系统构建的里面连vim、curl都没有需要先apt-get update apt-get install -y curl vim才能干活。这也是为什么很多人进不去容器或者里面啥也没有——不是卡住了是里面真的啥也没有。3.4 日志查看与实时跟踪容器内部就像一个黑盒出问题第一件事看日志# 查看全部日志 docker logs mysql8 # 跟踪最新日志类似tail -f docker logs -f mysql8 # 只看最近100行 docker logs --tail 100 mysql8docker logs -f配合另一个终端调bug是排障黄金搭档。比如容器一直显示restarting日志一看MYSQL_ROOT_PASSWORD is empty根因直接定位不用瞎猜。要注意的是docker logs收集的是容器主进程往标准输出和标准错误流写的内容。如果容器里的应用把日志写到文件而没打标准输出你是看不到的得配合docker exec进去看文件。搜索词里还有访问docker容器内的mysql这种需求本质就是端口映射。拿到docker ps里PORTS列的信息0.0.0.0:3306-3306/tcp表示宿主机所有网卡的3306端口都指向容器3306。如果你只想让内网访问改成-p 127.0.0.1:3306:3306这样只有本机能连多了层安全保护。4. 数据卷挂载让容器里的数据活过容器的一生容器是吃干抹净不留痕的。你往容器里写文件docker rm删容器数据跟着一起没了。MySQL里的业务数据要是这么丢哭都来不及。所以数据持久化是Docker用法的必修课搜索词里docker安装mysql失败、访问docker容器内的mysql这类问题一半根因都在数据卷用法上。4.1 容器无状态设计的痛点理解Docker的设计哲学是掌握数据卷的前提。官方设计理念是容器应该是无状态的、可随时销毁重建的。但现实业务里MySQL、Redis这些中间件天生有状态数据必须活过容器的一生。于是就有了数据卷volume和绑定挂载bind mount两种机制。初学者最容易踩的坑是把数据写在容器可写层里。你正跑着MySQL往里写了半天数据然后觉得版本要升级docker rm再加新版本容器忘了挂载旧数据目录所有数据瞬间蒸发。我当年第一次给同事演示Docker部署数据库时干过这事数据丢了之后被骂了好久。4.2 三种数据持久化方式对比Docker提供三种数据持久化方式用表格看最清楚方式命令形式数据存放位置适用场景数据卷Volume-v mysql_data:/var/lib/mysqlDocker管理目录如/var/lib/docker/volumes/容器迁移、备份恢复绑定挂载Bind Mount-v /宿主路径:/容器路径宿主机任意指定目录开发调试时改代码即时生效tmpfs挂载--tmpfs /容器路径内存重启即清空临时缓存、敏感数据不落盘-v后面的写法要分清前半段如果是纯名字就是命名卷如果是/开头的绝对路径就是绑定挂载。很多人把/myapp和myapp写混结果一个是绑定宿主机根目录下的myapp一个是在Docker数据区里创建一个卷行为完全不一样。4.3 挂载实战MySQL数据目录给MySQL挂载数据卷的完整流程# 先创建命名卷 docker volume create mysql_data # 把卷挂到容器的MySQL数据目录 docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORD123456 \ -v mysql_data:/var/lib/mysql \ mysql:8.0 # 查看卷详情 docker volume inspect mysql_datadocker volume inspect输出里有Mountpoint字段告诉你在宿主机上的实际路径。很多人用docker exec进容器看/var/lib/mysql发现里面有大量文件这是MySQL的初始数据文件而在宿主机路径同样能看到这些文件因为卷和容器目录是同一个地方。生产环境我更推荐用绑定挂载到明确目录比如-v /data/mysql:/var/lib/mysql这样备份、恢复文件都直观可控。但要注意挂载目录的权限MySQL容器里的进程默认以mysql用户运行UID通常是999。宿主机目录如果权限是root:root 755容器写不进去。处理办法是chown -R 999:999 /data/mysql或者用chmod -R 777图省事。这个坑特别隐蔽报错往往不是权限拒绝而是mysqld: Cant create/write to file排查半天才意识到是UID不匹配。4.4 挂载的排错权限与覆盖问题挂载之后如果发现容器里目录是空的别急着怀疑挂载失败。先把容器停下来再认真看docker inspect的Mounts字段。有一个经典坑是你把宿主机的空目录挂到一个容器里原本有数据的目录空目录会直接覆盖容器里的目录内容看起来就像数据消失了一样。如果容器里本来有初始化脚本或者配置文件先确保宿主机挂载目录里该有的文件都在。文件权限的问题也值得多说一嘴。容器内外是两套用户体系宿主机看到的是数字UID不会知道容器内的文件属于哪个用户名。比如你在宿主机把/data的所有者设成root容器内的mysql用户照样写不进去反过来你在容器里用root创建了文件宿主机上可能因为root_squash之类的原因访问受限。解决思路只有一个保证宿主机挂载目录的UID/ GID和容器内运行用户一致。5. 网络连通性端口映射与跨容器通信docker网络不通、docker容器里的ros2 humble、docker部署微服务项目这些热搜词背后核心都是容器网络模型。Docker容器默认在隔离的网络里不配置映射的话宿主机和外部都访问不到容器内部的服务。5.1 端口映射的两种模式-p参数看起来简单实际有几种变体# 随机宿主机端口映射到容器端口 docker run -d -P nginx:1.25 # 指定宿主机端口映射容器端口 docker run -d -p 8080:80 nginx:1.25 # 指定IP、宿主机端口映射 docker run -d -p 127.0.0.1:8080:80 nginx:1.25 # 同时映射TCP和UDP docker run -d -p 8080:80/tcp -p 8080:80/udp nginx:1.25-P大写P是随机分配一个宿主机空闲端口配合docker ps看实际映射端口。这个方式适合临时调试不适合正式服务。小写-p是把指定端口映射出去遇到宿主机端口被占用会直接报错。关于docker网络不通最常见的原因是防火墙。宿主机开防火墙后-p 8080:80在Docker Desktop的WSL2环境里通常没问题但在Linux服务器上ufw或firewalld可能会把转发规则拦掉。排查命令是# 宿主机上测端口通不通 curl http://localhost:8080 # 看iptables的Docker链 iptables -L -n | grep docker5.2 跨容器通信自定义网络docker部署微服务项目的场景下容器间通信是刚需。老式的做法是用--link参数让容器互通现在官方推荐用自定义网络# 创建bridge网络 docker network create app-net # 两个容器都加入同一网络 docker run -d --name nginx1 --network app-net nginx:1.25 docker run -d --name nginx2 --network app-net nginx:1.25 # nginx1里直接ping nginx2的容器名能通 docker exec nginx1 ping nginx2自定义网络的精髓在于同一网络内的容器可以直接用容器名互相访问Docker内置了DNS解析。比如微服务里的order-service调用user-service直接写http://user-service:8080就行。容器重建后IP会变但容器名不变DNS解析会自动指向新IP。网络类型选择也不少bridge是默认的NAT模式适合单机多容器host模式让容器直接共享宿主机网络栈性能最好但端口冲突风险高none模式用于完全隔离。跨主机通信则涉及overlay网络配合Swarm或者K8s那已经是进阶话题了。5.3 网络不通的排查链路遇到容器启动正常但访问不了的情况我的排查顺序是先确认容器在运行docker ps看STATUS列。再确认映射生效docker port 容器名看端口映射。然后宿主机测连通curl 127.0.0.1:映射端口。宿主机通了外部不通查防火墙、云安全组。宿主机都不通docker logs 容器名看服务有没有真正监听端口或者docker exec进容器里netstat -tlnp确认服务状态。还有一个容易忽略的点容器内部服务监听的地址。-p 3306:3306只是端口转发容器里的MySQL如果只监听127.0.0.1外部流量依然进不来。很多镜像的默认配置是监听0.0.0.0所以没问题但自己构建的镜像很容易栽在这里。6. 从入门到收尾清理、备份与镜像导出Docker用了一段时间之后你会发现自己服务器上堆积了无数停止的容器、悬空的镜像、无用的数据卷。这时候基础命令的另一半价值就体现出来了——收尾能力。6.1 清理命令别再手动删文件# 清理所有已停止的容器 docker container prune -f # 清理悬空镜像没有被任何容器引用的镜像 docker image prune -f # 清理无用数据卷 docker volume prune -f # 全量清理停止的容器无用网络悬空镜像构建缓存 docker system prune -adocker system prune -a这个命令要慎用它会把所有不被使用的镜像全删了包括你本地缓存的那些基础层。下次重新构建镜像时得全部重新拉取速度堪比从零开始。我一般在磁盘快满时才跑一次全量清理平时只跑docker container prune -f清理停止的容器就够了。6.2 镜像保存与加载内网服务器没有外网访问权限时镜像迁移是刚需。两个最常用的命令# 把镜像保存为tar文件 docker save -o nginx.tar nginx:1.25 # 加载tar文件为镜像 docker load -i nginx.tardocker save保存的是镜像本身比docker pull的压缩包要大因为包含完整的镜像层。传输到内网机器后docker load即可。注意如果镜像有多个tagdocker save默认只保存你指定的那个tag引用的镜像层。想保存完整镜像ID可以用docker save -o full.tar 镜像ID。6.3 容器提交与迁移docker commit这个命令争议比较大。它能把一个容器的当前状态打包成新镜像相当于对一台电脑的硬盘做快照# 把容器提交为新镜像 docker commit mysql8 mysql8-custom:latest # 导出容器的文件系统到tar docker export -o mysql8.tar mysql8我的观点是docker commit只适合快速保留现场比如你手动在容器里装了一堆调试工具想保存这个环境。真正规范的镜像构建一定用Dockerfile因为commit会把容器运行过程中产生的临时文件、日志、历史命令全部塞进镜像镜像臃肿还不透明。唯一的问题是手动装好的环境用commit保存确实省事但后续维护文档还得靠Dockerfile。docker export和docker save完全不一样。export导出的是容器文件系统不包含元数据导入后也不是镜像而是根文件系统得用docker import再转为镜像。这两个命令方向搞反的人不少我建议记一句话save作用于镜像export作用于容器。7. 排障思维先复原现场再谈解决问题Docker基础命令学到这个程度已经能覆盖80%的日常使用。但真正让你敢在生产环境用的是遇到问题时的一套排障方法论。搜热词里那些docker服务启动失败、docker权限错误、docker容器一直重启其实都能用一套思路快速定位。我的排障习惯是第一步docker ps -a看容器状态。Exited(0)说明进程正常退出多半是启动参数或环境变量的问题Exited(1)或更高编号说明进程遇到致命错误Restarting则说明进程一直在崩溃重启。第二步docker logs看应用日志。这一步能解决90%的问题密码错了、配置路径错了、端口被占用日志里全看得明明白白。第三步docker inspect看容器配置。重点看State字段的Error信息、Mounts挂载情况、Config.Env环境变量。很多容器起来但功能不对的问题都出在环境变量写错。第四步docker exec进容器手动跑一次命令。比如MySQL起不来进容器里手动执行mysqld --verbose --help看看实际报错。这套链路走下来绝大多数Docker相关的问题都能定位出根因。剩下的难缠问题大概率出在Docker守护进程层面那就要看/var/log/docker.log或者journalctl -u docker了。有一点要特别提醒排查生产环境的容器问题时千万别随手一个docker rm -f把容器删了。先docker inspect和docker logs留存现场信息确认数据都安全再动手清理或重建。我见过有人为了图省事一条docker rm -f把唯一一份日志容器删了最后只能从数据卷里捞数据损失惨重。最后分享一个我自己的习惯给每条docker run命令加上--restartalways和--name去掉是不行的。容器有了名字日志、端口、inspect都方便有了重启策略服务器宕机恢复后服务能自动拉起。这两行参数听着基础实际生产环境救过我太多次。还有个小技巧docker run之前先docker pull一下手动确认镜像能拉到、网络没毛病再安心去写完整的启动命令。不然你写了一大堆参数结果发现镜像拉不下来落地体验特别差。Docker基础命令说到这剩下的就是多练多用。找个云主机装个Docker把MySQL、Redis、Nginx各部署一遍再挂上数据卷、配上网络、跑通端口映射你就能理解这套工具真正的使用逻辑了。毕竟命令就那么几条真正值钱的是你踩过的那些坑和从坑里爬出来的经验。