
你有没有过这种经历本地装个 MySQL想升级版本结果依赖库冲突包管理器反复报错最后只能重装系统或者同事给你发个项目明明你环境里都装好了依赖一运行还是一堆幺蛾子。后来我开始接触 Docker才真正体会到什么叫“环境即代码”。这篇文章不是什么官方文档翻译就是我自己从零摸 Docker 的完整路径把踩过的坑、想明白的原理、能直接复制的命令一次性讲清楚特别适合刚接触容器化、装完 Docker 不知道下一步干什么的朋友。1. 先想明白容器到底把什么问题解决了1.1 我的环境怎么又“炸”了在没有 Docker 之前我最烦的一件事就是环境不统一。开发环境跑得好好的一部署到测试服务器缺这个库少那个包版本还不完全一致。更头疼的是多项目并存的情况A 项目要 Python 3.8B 项目要 Python 3.10C 项目还要老旧的 MySQL 5.7你总不能不停切环境变量、反复卸载重装。容器化解决的就是这个痛把你的应用连同它的运行环境一起打包像一个“集装箱”一样运到哪里都能原样跑起来。底层系统是 CentOS 也好、Ubuntu 也好、Windows 也好只要有 Docker 引擎在这个容器的内部环境就是一致的。对零基础的人来说先记住这句话就够了Docker 让应用不再依赖宿主机的环境。1.2 镜像、容器、仓库三件套Docker 里最核心的三个概念是镜像、容器、仓库理解它们你就能看懂后面所有操作了。镜像Image可以理解成一个“模板”或“安装包”。MySQL 的镜像里不光有 MySQL 程序还有它依赖的系统库、配置文件、启动脚本全部打包好了。容器Container镜像运行起来之后就是一个容器。一个镜像可以同时运行出多个容器彼此互不影响。打个比方镜像就像光盘里的系统安装镜像容器就是你根据这个镜像装出来的“一台台电脑”。仓库Registry存放镜像的地方。最常用的公共仓库就是 Docker Hub你执行docker pull mysql:8.0就是从仓库里把 MySQL 8.0 镜像拉回本地。命令docker run是“基于镜像创建并启动容器”docker pull是“把镜像从仓库拉到本地”。两者经常连起来用先拉镜像再启动容器。实际上docker run会检测本地没有镜像时自动拉取但新手我建议还是养成先docker pull的习惯避免把网络超时和容器启动失败混在一起排查。1.3 与虚拟机相比容器为什么更轻在没接触 Docker 前我们的第一反应是装虚拟机。VMware 里跑一个 Ubuntu再在里面部署应用好像也能隔离环境。但虚拟机有很明显的短板每个虚拟机会包含一套完整的操作系统内核和硬件虚拟化层占用几个 GB 的内存起步启动也需要几十秒甚至更久。容器则完全不同。它和宿主机共享内核没有自己的操作系统内核只把用户态的程序、库、配置打包进去。这就让容器启动往往是一两秒的事内存占用也只有几十 MB 到几百 MB。说到底容器本质上是宿主机上的一个受隔离的进程而不是一台“虚拟电脑”。理解了这个区别你就不会再用管理虚拟机的思路去管理容器了。2. 安装 DockerWindows 和 Linux 的两条不同路线2.1 Windows 上 Docker Desktop 的完整安装流程Windows 上最主流的安装方式是用 Docker Desktop。这个工具自带图形界面能直观地看到容器状态、日志对新手非常友好。安装流程看起来很简单但很多朋友卡在了启动阶段所以我把完整步骤拉出来。确认系统版本Windows 10 64 位专业版/企业版/教育版或者 Windows 11。家庭版也能用但需要额外开启 Hyper-V 的一些底层组件容易出问题。开启 Windows 相关功能在“控制面板—程序和功能—启用或关闭 Windows 功能”里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”确定后重启。更新 WSL2 内核打开 PowerShell 执行wsl --update老朋友告诉我这一步最容易漏。如果不更新Docker Desktop 启动时会提示找不到 WSL2 内核。到 Docker 官网下载 Docker Desktop 安装包安装时保持默认勾选“Use WSL 2 instead of Hyper-V”。完成后重启电脑启动 Docker Desktop看到左下角鲸鱼图标变绿就说明引擎正常运行了。有个细节如果在第 2 步勾选后重启仍然提示虚拟化问题多半是 BIOS 里的虚拟化开关没开。开机进 BIOS 设置找到 Intel VT-x 或 AMD-V 的选项设为 Enabled。这个坑我帮朋友远程排查过他卡了一整天最后发现只是虚拟机监控程序没开启。2.2 虚拟化支持检测失败时的排查路径Docker Desktop failed to start because virtualization support wasnt detected是这个安装过程里出现频率最高的报错。它一句话就把人打懵了我电脑明明是新买的怎么会不支持虚拟化其实大部分情况不是 CPU 不支持而是没开启或没正确联动。按这个顺序排查通常能解决 90% 的问题打开任务管理器切到“性能”标签看“虚拟化”那栏是否显示“已启用”。如果显示“已禁用”进 BIOS 开启 VT-x/AMD-V无效则检查主板型号上网搜虚拟化开关的准确位置。确认“虚拟机平台”和“适用于 Linux 的 Windows 子系统”这两个 Windows 功能都已勾选缺一个都会导致 Docker Desktop 检测失败。在 PowerShell 执行wsl --status查看默认版本是不是 2。如果显示 V1执行wsl --set-default-version 2然后重新启动 Docker Desktop。如果你装了第三方的安全软件或系统优化工具建议先退出再启动 Docker Desktop。有几次莫名其妙的启动失败就是优化工具把底层服务给禁用了。2.3 Ubuntu 等 Linux 发行版的安装命令如果你用的是 Ubuntu 或 CentOS 这类 Linux 服务器一般不需要 Docker Desktop直接安装 Docker Engine 就够了。服务器上图形界面意义不大还要多占内存。Ubuntu 全家桶推荐先卸载可能冲突的旧包然后安装依赖再添加 Docker 官方软件源。不过更省事的是用官方安装脚本执行后它会自动完成源配置和安装curl -fsSL https://get.docker.com | bash脚本跑完后执行sudo systemctl enable --now docker把 Docker 服务设为开机自启。CentOS 7 的经典安装命令是yum install -y docker装好之后systemctl start docker。需要说明的是 CentOS 7 自带的 docker 包版本比较老如果你非常在意新功能建议先配置 Docker 官方 yum 源再安装 docker-ce。2.4 装完立刻要做的两件事加用户权限和配镜像加速安装完成不等于配置完成。很多新人第一次执行docker ps就报permission denied while trying to connect to the docker daemon socket原因很简单当前用户不在 docker 用户组里。sudo usermod -aG docker $USER newgrp docker第一句把当前用户加入 docker 组第二句让用户组变更立即生效。执行完再跑docker ps就不会匿名失败。第二件事是配置镜像加速。国内网络环境下直接从 Docker Hub 拉镜像经常慢到怀疑人生。加速的思路不是“绕过”而是配置 registry mirror 镜像加速服务。在/etc/docker/daemon.json写入{ registry-mirrors: [ https://docker.1ms.run, https://docker.xuanyuan.me ] }写完重启 Docker 服务sudo systemctl restart docker。需要注意加速地址选可用、合规的即可不同网络环境下的可用性差异很大。如果你的网络环境访问 Docker Hub 本来就快这步可以跳过。3. 从拉镜像到看懂常用命令把第一个容器跑起来3.1 拉镜像失败或太慢先换源再查网络我刚装好 Docker 后做的第一件事是拉 hello-world 镜像结果卡了五分钟没动静最后显示超时。新手很容易在这里崩溃。后来意识到镜像下载慢大多是网络问题第一手段是配置前面说的镜像加速而不是反复重试。配置完加速后docker pull hello-world应该几秒钟就完成。如果仍然很慢可以用docker info确认当前 registry mirror 是否生效里面会直接打印出配置的镜像加速地址列表。拉镜像时还要注意 tag 的选择。docker pull mysql默认拉 latest 标签也就是最新版。生产环境不推荐这么做因为你不知道今天部署的 MySQL 到底是哪一个具体版本。正确做法是精确到版本号docker pull mysql:8.0。3.2 一条 docker run 命令拆开讲镜像拉完后我们先跑一个测试容器docker run --name first-container hello-world它会输出一段欢迎文本然后容器就退出了。这个例子只能证明 Docker 装好了但离真实使用还差得远。我们来把一条接近实战的docker run命令拆开看docker run -d --name nginx-test -p 8080:80 -v /opt/html:/usr/share/nginx/html nginx:1.25参数逐个解释-d后台运行容器不占用当前终端。--name给容器起名字后续操作都用这个名字而不是容器 ID。-p 8080:80端口映射。冒号左边是宿主机端口右边是容器内部端口。外部访问宿主机的 8080 端口就会转到容器的 80 端口。-v /opt/html:/usr/share/nginx/html目录挂载。把宿主机的/opt/html目录映射到容器内的 Nginx HTML 目录你改宿主机的文件容器内立刻同步。最后的nginx:1.25是镜像名和标签。新手最容易搞混的就是-p的方向。我习惯这样记访问你的是外部容器在内部所以-p 宿主端口:容器端口。访问的时候永远只关心宿主机那个端口。3.3 必备命令速查与容器生命周期容器从创建到删除核心命令就这么多我按使用频率列出来# 查看所有命令帮助及镜像帮助 docker --help # 查看正在运行的容器加 -a 看所有含已停止的 docker ps docker ps -a # 停止、启动、重启容器 docker stop nginx-test docker start nginx-test docker restart nginx-test # 查看容器日志-f 是持续跟踪 docker logs -f nginx-test # 进入容器执行命令-it 表示交互式终端 docker exec -it nginx-test bash # 删除容器-f 是强制删除运行中的容器 docker rm -f nginx-test # 查看本地镜像 docker images # 删除镜像 docker rmi nginx:1.25还有两个坑提醒你docker start和docker run不是一回事run是创建并启动新容器start是启动已经存在的容器以及docker exec后面要跟容器名而不是镜像名。进入容器后容器内部是一个精简的 Linux 环境但很多工具可能没装全这种现象是正常的。3.4 容器删除后数据会消失吗很多小白把容器当虚拟机用一切数据都往容器里写。然后某天执行docker rm -f把容器删了发现里面的数据全部没了。原因就是容器是可写层的数据是临时的容器删除后容器本身的数据也会被销毁。所以那些需要长期保留的数据比如数据库文件、日志、上传的附件必须通过-v挂载到宿主机上。所有持久化操作一律不要依赖容器内部目录。这是一个定位问题非常关键的心态容器是可以随时舍弃的数据在外面才是安全的。如果你发现自己“不舍得删容器”说明当初的设计就有问题。4. 用 MySQL 8.0 做一次“真正能用”的容器化部署4.1 为什么部署数据库比跑 hello-world 复杂得多跑 hello-world 只验证了 Docker 基本可用而部署一个 MySQL 8.0 才能体现容器化真正的好处。数据库是有状态服务意味着你的数据不能丢数据库又要对外提供服务所以端口映射必须正确数据库还需要初始化账号密码和数据库名。一条接近可用的 MySQL 部署命令长这样docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDRoot123456 \ -e MYSQL_DATABASEtestdb \ -v /data/mysql:/var/lib/mysql \ mysql:8.0执行完等十几秒让它初始化然后docker ps看容器是否处于 Up 状态。如果容器反复退出用docker logs mysql8看日志绝大多数情况是密码策略、端口被占用或目录权限的问题。4.2 数据持久化为什么必须挂载目录命令中的-v /data/mysql:/var/lib/mysql是这整条命令最重要的一部分。它把宿主机目录/data/mysql挂载到容器里的 MySQL 数据目录。MySQL 的所有库表数据都写在/var/lib/mysql挂载后即使容器被删掉重新创建一个 MySQL 容器并挂载同一个目录数据依然还在。建议你把数据目录单独建好并确认权限满足要求。有的系统上容器内 mysql 用户对挂载目录没有写权限启动会失败解决办法是mkdir -p /data/mysql chown -R 999:999 /data/mysql999 是 MySQL 镜像内 mysql 用户的 uid这个细节不在日志里找半天是看不出来的。4.3 端口映射的地址关系-p 3306:3306表示把宿主机的 3306 端口转发到容器的 3306 端口。如果你的宿主机 3306 端口已经被本机 MySQL 占用了两种方案要么先把本机 MySQL 停了要么改宿主机端口比如-p 3307:3306。连接字符串随之变化mysql -h 127.0.0.1 -P 3307 -u root -p记住一个原则容器内部的端口一般不动动的是宿主机映射的端口。这样即便宿主机上有老版本 MySQL也能通过不同端口和容器内的 MySQL 8.0 共存。4.4 进入容器、备份数据的常用操作MySQL 容器跑起来后很多操作需要在容器内部完成。最常用的两种# 在容器内执行 mysql 客户端 docker exec -it mysql8 mysql -uroot -p # 在容器内执行 mysqldump 备份 docker exec -it mysql8 mysqldump -uroot -p testdb /data/backup/testdb.sql还有一种不需要进入容器也能执行 SQL 的方法把 SQL 通过 stdin 管道传进去。echo SELECT 1; | docker exec -i mysql8 mysql -uroot -p注意这里用的是-i而不是-it因为没有交互终端的分配。这种写法适合在脚本里批量执行 SQL。4.5 常见失败的排查MySQL 容器化部署中最常见的三个问题现象原因解决办法容器启动几秒后停止数据目录权限不对或配置挂载了空目录覆盖了初始化脚本检查/data/mysql权限或换一个空的挂载目录宿主机无法连接 3306端口映射没生效或宿主机本机 MySQL 占用docker ps确认映射行ss -tlnp密码一直报错环境变量拼写错误或初始化未完成检查MYSQL_ROOT_PASSWORD是否拼错等 mysql 日志出现ready for connections再连5. 用 Redis 主从搭建理解容器间通信5.1 桥接网络和容器间互访装完 MySQL我们对容器“外部访问”有概念了。接下来要做的是容器和容器之间的通信。以 Redis 主从为例从节点要去连接主节点它访问主节点时用的是 IP 和端口但容器重启后 IP 会变这就很不稳定。Docker 的解决方案是自定义网络。在同一个自定义网络里的容器可以用容器名互相访问Docker 内部 DNS 会把名字解析成对应 IP。这就像你按姓名找人而不用记手机号。先创建网络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-slave \ --network redis-net \ -p 6380:6379 \ redis:7 redis-server --replicaof redis-master 6379到这一步主从关系就已经建立起来。注意从节点的--replicaof redis-master 6379这里的redis-master不是 IP而是容器名。5.2 网络里每个容器的地址分配方式当你新建一个网络再把容器放进去时Docker 会自动为每个容器分配 IP 地址。想查看具体分配情况docker network inspect redis-net输出里能看到redis-master和redis-slave各自的 IP。你不需要手动指定 IPDocker 的嵌入式 DNS 会根据容器名解析。这也是推荐自定义网络而不是直接用默认 bridge 网络的原因默认网络里的容器不能用容器名互访只能靠 IP非常难受。5.3 验证主从同步搭建完成后验证是必须的动作。进入从节点容器执行info replicationdocker exec -it redis-slave redis-cli -p 6379 info replication重点看三处role:slave、master_link_status:up、以及slave_repl_offset的值。如果master_link_status是down说明从节点连不上主节点优先检查两个容器是不是在同一个自定义网络里。给主节点写入一个键再从从节点读取来验证数据同步docker exec -it redis-master redis-cli -p 6379 set greeting hello from master docker exec -it redis-slave redis-cli -p 6379 get greeting能读到值说明主从复制链路是通的。5.4 网络不通的排查思路容器间网络问题十次有九次是“不在同一个网络”。排查路径按这个顺序来docker network ls看网络列表确认两个容器都在同一个自定义网络里。docker network inspect 网络名对比两个容器的连接信息。进入任一容器执行ping对方容器名。再检查防火墙或安全组是否拦截了容器网段之间的流量。在 Docker 场景下还有一类隐蔽问题端口冲突。比如你把从节点的宿主机端口写成6379:6379而主节点已经占了 6379从节点启动不会报错但实际端口映射是失效的。所以从节点用6380:6379是更规范的做法只是把容器内的 Redis 标准端口暴露出来外部访问时用 6380 即可。6. Docker Compose把多容器编排写进一份配置6.1 什么时候需要 Compose当我开始同时部署 MySQL、Redis、Nginx 和应用服务时每次都要手动敲四五条docker run命令参数又多又容易漏删除时还得一个个docker rm。这个阶段就该上 Docker Compose。它本身不是容器引擎而是一个编排工具让你把多个容器的启动配置写在一个 YAML 文件里一条命令全部拉起。不需要急着背语法只要记住它把docker run的命令行参数翻译成了 YAML 里的键值结构。端口对应ports环境变量对应environment目录挂载对应volumes网络对应networks。本质上就是在写一份“多个容器的启动说明书”。6.2 用 Compose 一键拉起 MySQL 和 Redis以我们前面部署的 MySQL 和 Redis 为例写一份docker-compose.ymlservices: mysql8: image: mysql:8.0 container_name: mysql8 restart: always ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORDRoot123456 volumes: - /data/mysql:/var/lib/mysql redis7: image: redis:7 container_name: redis7 restart: always ports: - 6379:6379 command: [redis-server, --appendonly, yes]在文件所在目录执行docker compose up -d-d表示后台启动。看到Started字样后用docker compose ps查看服务状态。下次要停掉全部服务执行docker compose down。注意down默认不会删除数据卷所以数据还留着这个设计很安全。Compose 还会自动为项目创建一个独立的默认网络所有 service 之间可以用 service 名互相访问。比如某个应用容器需要连接 MySQL可以直接写mysql8:3306不需要手动创建网络。6.3 编排文件里的几个易错点Compose 文件看起来简单踩坑的点不少。首先是 YAML 缩进必须用空格不能用 Tab而且层级关系要严格对齐。很多新人直接复制文件贴进编辑器结果报mapping values are not allowed here多半就是缩进问题。其次container_name是固定容器名如果宿主机已经有一个同名容器docker compose up会直接失败。建议项目少的时候才固定容器名正式项目里一般让 Compose 自动生成带项目前缀的名称避免冲突。还有一个很多人忽略的点restart: always。没有这个配置宿主机重启后容器不会自动启动。加上它Docker 服务一启动就把容器一并拉起来。对需要长时间运行的服务来说这是高频配置项。7. 启动失败与权限问题的现场排查记录7.1 Docker 服务起不来的日志怎么看不管哪种操作系统Docker 服务本身都有日志可查。Linux 上最常见的是Cannot connect to the Docker daemon这通常说明守护进程没启动systemctl status docker journalctl -u docker -n 50日志里如果出现failed to start docker container engine别急着瞎改先看它上下文。常见原因是/var/lib/docker所在磁盘满了、daemon.json 配置格式写错、或者是 root 分区 inode 耗尽。我之前遇到过容器能创建但启动不了最后查出来是/var/lib/docker的 inode 满了清理了一堆旧镜像才好。Windows 上 Docker Desktop 启不来的情况日志位置一般在托盘图标右键菜单的“Troubleshoot”里它会导出诊断包。把日志里第一处标红的错误信息记下来搜索比自己盲猜效率高得多。7.2 Docker CLI 权限问题的两种解法权限问题集中在 Linux 上典型报错是permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock原因是当前用户没有访问 docker.sock 的权限。正规解法是我前面提到的把用户加入 docker 组sudo usermod -aG docker $USER如果不想退出重登执行newgrp docker临时生效。临时生效后当前终端立刻就能用 docker 命令但新开的终端还要等重新登录后才生效。另一种“暴力”解法是sudo chmod 666 /var/run/docker.sock把 socket 完全放开。这个操作虽然简单但等于把 Docker 权限开放给本机所有用户存在很大的安全问题只适合本机只有一个用户且完全可信的测试环境。在服务器上请永远用 docker 组方案。7.3 用户们最常见问题的快速对照表这里把零基础用户最常问的问题整理成表遇到问题可以对照处理现象原因操作docker ps报 permission denied用户不在 docker 组执行 usermod 并重新登录镜像下载非常慢没配镜像加速配置 daemon.json 的 registry-mirrors 后重启 docker容器启动后立即退出应用报错或参数不正确docker logs 容器名查看错误细节宿主机访问不到容器端口端口映射冲突或映射方向写反docker ps查看 PORTS 列重新确认host:container容器内代码能跑宿主机访问不了容器内进程只监听了容器网卡检查应用监听地址是否为 0.0.0.0docker compose命令不存在Compose 插件未安装或版本太老升级 Docker Engine 到新版本自带 compose 子命令最后说点个人体会。很多人学 Docker 时一股脑背命令实际上真正挡住新手的从来不是命令数量而是那几个关键心智模型镜像和容器的关系、端口映射的方向、数据卷的持久化、网络里的容器名通信。把这四块想通了后面的部署、编排、排错都会顺很多。我的建议是装好 Docker 后别急着看长篇文档先用docker run把 Nginx 跑起来再把 MySQL 跑起来最后把它俩用 Compose 一起管理。这个过程走完你就算真正入门容器化了。后面再遇到环境问题你的第一反应就不会是“重装系统”而是“起个容器试试”。这条路我亲身走过希望这篇记录能帮你少绕几个弯。