
1. 为什么我最终选择了Docker来搞定所有部署先说说我的真实经历。上个月帮朋友把一套已经跑了三年的Python项目部署到新服务器第一件事就是装MySQL 8.0然后发现系统自带的MySQL是5.7数据迁移、配置文件、字符集……每一项都在制造麻烦。我当场决定所有组件全部用Docker容器化部署重新来一遍。这个决定让原本要折腾两天的环境问题压缩到一下午就全部解决。如果你正在被版本冲突、环境依赖、跨机器部署这些事反复折磨这篇手册就是写给你的。我会从Windows上最容易卡住的Docker Desktop安装开始讲到MySQL 8.0、Redis主从这些真实业务里高频使用的容器部署方式再到Docker Compose把整套环境固化成一份YAML文件最后把容器网络不通这一类玄学问题拆开揉碎。内容尽量按我实际操作时踩过坑的顺序来写不是官方文档的翻译。1.1 从一次MySQL环境升级引发的连锁反应那次升级的痛点很典型服务器上已经装了MySQL 5.7但项目代码里用到了MySQL 8.0才支持的窗口函数。直接卸载重装风险太大因为系统里还有其他老应用依赖5.7的客户端lib。用Docker之后我拉一个mysql:8.0镜像映射3307端口启动新实例老实例继续跑在3306两边完全隔离。这样的场景在业务部署中太常见了Docker最直接的价值就是让不同的服务依赖不同的运行环境变成一件不需要纠结的事。这也是我写完全手册的出发点不是教你把Docker当成一个普通的软件装上而是让你理解它如何改变部署的思维方式。拿MySQL这件事来说如果没有Docker你需要考虑操作系统的包管理器、安装源、存储路径、权限、配置文件位置、开机自启脚本……每一样都可能因为系统版本不同而千差万别。而Docker镜像把MySQL、Redis、Nginx这些软件连同它们需要的运行时环境一起打包你只需要关心暴露哪个端口、挂载哪个目录、启动参数怎么传。1.2 Docker容器化部署到底改变了什么我习惯用一个比喻来理解Docker它就像集装箱运输出现之前的码头。以前的码头工人需要逐个处理形状各异的货物而现在无论里面装的是衣服还是机器零件外面都是统一尺寸的集装箱吊车、卡车、轮船都能无缝对接。Docker镜像就是这个集装箱容器就是正在运输途中的货物状态。你不用担心这箱货里面装的是Ubuntu还是CentOS、是MySQL还是Redis因为装箱的那一刻里面的一切已经被验证过是能正常工作的。具体到日常部署场景容器化带来的改变有三个层面。第一可移植性我在Mac上构建好的镜像到Linux服务器、Windows服务器上都能跑只要对方有Docker引擎。第二隔离性多个应用共享一台服务器时它们各自有独立的文件系统、网络栈和进程空间一个应用崩溃不会拖垮另一个。第三可重复性以前部署靠运维手记某一步忘了就被坑现在部署靠镜像和Compose文件只要文件在随时能复现一套一模一样的环境。1.3 读这篇手册你需要提前准备什么我的建议是手里有一台Windows 10/11专业版或家庭版电脑内存至少8GB并且能访问Docker Hub拉取镜像。不需要任何Docker基础但你需要会打开命令行无论是CMD、PowerShell还是Windows Terminal。本手册涉及的命令都以bash和Windows命令行兼容为主如果提示命令找不到先检查Docker Desktop是否已经启动。另外特别提醒一点不要在生产环境里用我们演示用的快速启动命令那些命令为了方便会省略很多安全配置。读完全文后你应该能够自己写出带数据卷、带网络隔离、带资源限制的完整部署方案。2. Windows上安装Docker Desktop最容易卡住的那一步如果你在Windows上安装过Docker Desktop大概率见过这样一个报错virtualization support not detected docker desktop failed to start because v后面通常还有半句话比如virtualization is disabled in BIOS。我第一次装的时候也卡在这里一度以为是Docker Desktop版本问题折腾了三个小时后才发现罪魁祸首是BIOS里的虚拟化开关没打开。这一节我就把整个过程拆开讲照着做十分钟内能启动。2.1 先确认虚拟化支持是否打开Virtualization support not detectedDocker Desktop在Windows上运行容器本质上依赖底层的虚拟化能力。无论你选择WSL2模式还是Hyper-V模式前提都是CPU的虚拟化功能已开启。Intel的VT-xAMD的SVM名字不同作用相同。Windows 10以上一般情况下会在任务管理器的性能选项卡底部显示虚拟化已启用/已禁用。如果显示禁用那就需要进入BIOS开启。不同主板的BIOS入口不一样开机时按Del、F2、F10都有可能。安全推荐的做法是重启系统在开机画面出现时根据屏幕提示进入UEFI/BIOS设置然后在Advanced或CPU Configuration里找到Intel Virtualization Technology或SVM Mode把它设为Enabled。保存退出后重新进入Windows再打开任务管理器确认虚拟化已经变成已启用。这里有一个很多人不知道的点即使Windows系统里看不到虚拟化状态也不代表一定没开启。某些电脑厂商默认关闭虚拟化却在系统里装了很多虚拟机软件导致Docker Desktop启动时报virtualization support not detected但其实Hyper-V又在运行。所以最稳妥的办法是先把WSL2的状态理顺再决定Docker用哪种后端。2.2 推荐安装路径WSL2模式与Hyper-V模式如何选现在的Docker Desktop默认推荐WSL2后端因为它比Hyper-V模式启动更快、内存占用更可控、文件共享也更顺畅。WSL2是适用于Linux的Windows子系统的第二代它是一个轻量级虚拟机但和普通的Hyper-V虚拟机不一样它能在几秒内启动和Windows文件系统可以双向访问。我的建议是只要能安装WSL2的Windows 10/11版本一律用WSL2模式。安装过程其实非常简单我简化成三步。第一步以管理员身份打开PowerShell执行wsl --install这条命令会启用所需的Windows功能并安装默认的Ubuntu发行版。如果该命令报错说明你的系统版本太旧需要手动开启虚拟机平台和适用于Linux的Windows子系统两个Windows功能然后重启。第二步重新启动后在PowerShell里运行wsl --set-default-version 2确保WSL默认使用第二代架构。第三步下载Docker Desktop安装包直接双击安装安装过程中它会自动检测WSL2并询问是否启用。如果一切正常安装完成后桌面会出现Docker Desktop图标第一次启动会要求你接受服务协议然后它会在几秒钟内启动引擎。2.3 启动反复失败的排查顺序与修复命令有时候你发现虚拟化也开了、WSL2也装了Docker Desktop图标还是转圈后弹出错误。这时别急着重装按这个顺序排查。先用wsl --status查看WSL的版本状态确认默认版本是2。如果输出显示的是WSL 1执行wsl --set-default-version 2再把已安装的发行版迁移过去。然后查看Windows功能状态用PowerShell执行Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform如果VirtualMachinePlatform显示State : Disabled需要启用Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart之后重启电脑再启动Docker Desktop。还有一类启动失败是因为Windows的容器功能没装好。管理员PowerShell执行Enable-WindowsOptionalFeature -Online -FeatureName Containers -All如果以上都检查正常Docker Desktop仍然失败你可以打开日志Docker Desktop界面点右上角问题反馈查看C:\Users\你的用户名\AppData\Local\Docker\log里的host和engine日志。最常出现的WSL相关错误通常是WSL2内核太旧执行一次wsl --update就能解决。3. Docker日常使用的第一性原理镜像、容器、数据卷装好Docker后真正开始用之前必须搞懂三个核心概念。我在教学时发现很多人记不住命令不是记性问题而是没有理解这三个东西的关系。搞懂了它们Docker的命令就不再是一堆需要死背的参数表。3.1 镜像与容器用一个跑步比喻讲清楚把镜像理解成模板或类把容器理解成模板运行出来的实例。我写Java的时候觉得这比喻最贴切镜像就像Java里的Class定义容器就像new出来的Object对象。同一个Class可以new出无数个对象同一个镜像可以启动无数个容器。对象和对象之间互不影响容器和容器之间同样互相隔离。实际操作中镜像可以被共享、被推送、被版本管理。docker pull mysql:8.0拉下来的是一个静态的只读模板docker run mysql:8.0就是基于这个模板启动一个容器进程。你可以在这个容器里面写文件、改配置、安装临时工具但这些改动不会影响原始镜像。如果这个容器被删除改动也就没了。想要保留改动就要用到数据卷或者通过Dockerfile构建新的镜像。3.2 最常用的容器操作命令手册我认为新手最需要记的容器操作命令就十几条按场景分类你不需要会写复杂的Shell脚本只要会用下面这些就够了。拉取镜像docker pull nginx:latest查看本地已有的镜像docker images启动一个交互式容器常用于调试docker run -it --name debug-box ubuntu:22.04 bash后台守护容器最常用docker run -d --name web-app -p 8080:80 nginx:latest查看正在运行的容器docker ps查看所有容器包括已停止的docker ps -a查看容器日志docker logs -f web-app进入正在运行的容器内部docker exec -it web-app bash停止/启动/重启容器docker stop web-app docker start web-app docker restart web-app删除容器必须先停止docker rm web-app如果你频繁创建容器记住加上--rm参数在退出时自动清理docker run --rm -it ubuntu:22.04 bash这些命令里我认为docker exec和docker logs是最值钱的调试工具。遇到容器起不来先看日志日志看不出问题再exec进容器内部看状态比在宿主机上瞎猜效率高得多。3.3 数据卷不配置就等着数据丢失这是新手最容易忽略的一个点。容器删除后容器内的所有非镜像文件都会消失。如果你的MySQL数据写在容器内部目录/var/lib/mysql容器一删数据库就没了。解决办法是用数据卷。数据卷有两种常用方式命名卷和绑定挂载。命名卷由Docker管理绑定挂载则把宿主机的某个目录直接映射到容器目录。我用一个例子说明docker run -d \ --name mysql8 \ -e MYSQL_ROOT_PASSWORD123456 \ -v mysql-data:/var/lib/mysql \ mysql:8.0这里的mysql-data:/var/lib/mysql就是命名卷。卷名是mysql-data挂载到容器的/var/lib/mysql目录。将来删除容器再重建只要指定同样的卷名数据还在。绑定挂载更直观适合开发和配置文件变更docker run -d \ --name nginx \ -v /home/user/nginx/html:/usr/share/nginx/html \ -p 80:80 \ nginx:latest这样你直接修改宿主机的/home/user/nginx/html里的文件nginx容器立刻就能读到。我强烈建议任何有状态的服务比如MySQL、Redis、PostgreSQL启动时必须配置数据卷否则你就是在赌容器永远不会被误删。我见过不止一次有人把生产数据库跑在容器里三个月然后一次docker rm -f毁了全部数据。4. 用Docker部署MySQL 8.0并完成生产级初始化MySQL是容器化部署中最典型的例子因为它有数据持久化、时区、字符集、远程权限、配置文件挂载这些必须考虑的细节。如果你能完整跑通一个生产级MySQL容器Docker的大部分概念也就掌握了。4.1 一条命令启动MySQL以及必须追加的容器参数最简单的启动命令长这样docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourStrongPassword \ mysql:8.0这样确实能跑起来但你很快就会遇到几个问题时区不对、中文乱码、容器一删数据全丢。所以我建议在任何真实项目里启动命令都要加上数据卷挂载和时区配置docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourStrongPassword \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ -v /path/to/my.cnf:/etc/mysql/conf.d/my.cnf \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci这里解释几个参数的重要性。-e TZAsia/Shanghai是设置容器时区如果不设置MySQL的SYSDATE()、NOW()会返回UTC时间和你的业务日志对不上。-v mysql-data:/var/lib/mysql是数据持久化前面已经强调过。-v /path/to/my.cnf:/etc/mysql/conf.d/my.cnf是挂载自定义配置文件MySQL默认会读取这个目录下的配置文件。最后的--character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci是直接向mysqld进程传入启动参数把默认字符集改成utf8mb4这是避免中文乱码的关键。4.2 时区、字符集、密码策略与远程连接的坑MySQL 8.0和5.7有很多不一样的地方。首先是默认密码策略升级了如果你指定一个类似123456的弱密码即使MYSQL_ROOT_PASSWORD123456也能启动但客户端连接时可能报错提示密码太弱。不过这个问题通常在容器内部不生效因为Docker初始化脚本在创建root用户时就会执行。为了安全我还是建议用强密码。其次是远程连接问题。MySQL容器默认只让root从localhost登录。你想从宿主机或者另一台服务器连进这个容器必须进入容器修改root用户的hostdocker exec -it mysql8 mysql -uroot -p输入密码后执行ALTER USER root% IDENTIFIED WITH mysql_native_password BY YourStrongPassword; FLUSH PRIVILEGES;注意MySQL 8.0默认的认证插件是caching_sha2_password有些老客户端和它不兼容连接时直接报Authentication plugin caching_sha2_password cannot be loaded。上面这条SQL把认证方式改成mysql_native_password可以解决绝大多数兼容性问题。另一个坑是端口占用。如果宿主机已经装了MySQL占用3306端口你需要把容器端口映射改掉比如-p 3307:3306。这里的含义是把容器的3306端口映射到宿主机的3307端口容器内部应用访问还是3306但从外面连接要用3307。4.3 实际部署中遇到的三个典型问题我实际操作MySQL容器时遇到过三个高频问题都是网上搜不到现成答案的。第一个是docker logs mysql8里不断提示mbind: Operation not permitted。这个警告其实是WSL2环境下内存分配机制的问题只要容器能正常初始化不影响使用可以忽略。如果你实在想消除可以把Docker Desktop的设置里Resources的内存调大或者改用Linux宿主机部署。第二个是重启容器后MySQL服务无法启动。常见原因是数据卷目录的权限被改掉了比如你手滑在宿主机执行了chown -R root:root导致容器内的mysql用户无法写入。解决办法是进入宿主机数据卷目录把属主改回MySQL容器内的用户ID通常UID是999或者干脆删除本地数据卷重建当然这只有开发环境才建议这么做。第三个是我自己踩过的忘记挂载时区配置结果日志和数据库时间全都差了8个小时。当时排查了很久发现容器里的/etc/localtime不是上海时区后来用-e TZAsia/Shanghai重新创建容器解决。这里建议你把时区纳入部署检查清单不要等出了问题再处理。5. 在Docker里搭建Redis主从一步步从单机到复制集群Redis主从是缓存层高可用的基础。我在Docker里搭主从时踩的坑比MySQL还多主要是容器间通信问题。这一节我会用最简单的方式在一个自定义网络里启动一个Redis主节点和一个从节点并验证数据复制。5.1 先准备Redis配置文件几个必须改的项Redis镜像默认不带外部配置文件你需要在宿主机准备好。我通常在项目目录下建一个redis/conf文件夹里面放两份配置redis-master.conf和redis-slave.conf。主节点配置最少需要改这些bind 0.0.0.0 appendonly yes protected-mode no dir /databind 0.0.0.0是让Redis监听所有网络接口否则容器外无法访问。appendonly yes开启AOF持久化避免重启丢数据。protected-mode no是在容器环境下让从节点能正常连接主节点生产环境下建议用防火墙来限制访问范围。从节点配置比主节点多一行bind 0.0.0.0 appendonly yes protected-mode no dir /data replicaof redis-master 6379注意replicaof后面跟的是主节点在Docker网络里的主机名而不是IP地址。这样即使容器重新创建导致IP变了主从关系也不会断。这是Docker自定义网络比直接用宿主机IP更稳定的原因。5.2 容器间相互通信的主从启动命令先创建一个自定义网络让容器之间用容器名互相访问docker network create redis-net启动主节点docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ -v /path/to/redis/conf/redis-master.conf:/etc/redis/redis.conf \ -v /path/to/redis/data/redis-master:/data \ redis:7.0 \ redis-server /etc/redis/redis.conf启动从节点docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ -v /path/to/redis/conf/redis-slave.conf:/etc/redis/redis.conf \ -v /path/to/redis/data/redis-slave:/data \ redis:7.0 \ redis-server /etc/redis/redis.conf这里关键点是端口映射宿主机的6380映射到从节点的6379这样外部可以同时访问主从两个端口而容器之间的复制流量走内部网络的6379端口不受映射影响。如果你不想提前准备配置文件也可以用命令行参数直接启动。主节点docker run -d --name redis-master --network redis-net -p 6379:6379 redis:7.0 redis-server --appendonly yes从节点docker run -d --name redis-slave --network redis-net -p 6380:6379 redis:7.0 redis-server --replicaof redis-master 6379 --appendonly yes这种方式适合快速验证生产环境还是用配置文件更清晰。5.3 验证复制状态INFO replication等操作启动完成后进入从节点容器检查复制状态docker exec -it redis-slave redis-cli在Redis命令行中执行INFO replication重点关注几个字段role:slave表示当前是从节点master_host:redis-master表示主节点地址master_link_status:up表示主从连接正常slave_repl_offset会随同步数据增加然后回到主节点写入数据docker exec -it redis-master redis-cli SET foo bar再到从节点读取docker exec -it redis-slave redis-cli GET foo如果能读到bar说明主从同步已经通了。我还遇到过一种情况master_link_status:down日志里报Unable to connect。这种时候先检查两个容器是否在同一个网络里用docker network inspect redis-net看容器列表再检查主节点的protected-mode和bind配置。绝大多数主从失败都是这两点引起的。6. Docker Compose把一套多容器部署固化成YAML代码当你需要同时部署MySQL、Redis、后端应用、前端Nginx时挨个docker run会让命令变得又臭又长。Docker Compose的价值就是让你把整套部署拓扑写进一个YAML文件用docker compose up一键起来。这一节会用一个MySQLRedis主从的例子说明Compose文件的核心写法。6.1 为什么放弃一长串docker run改用Compose我最早管理多容器时把所有docker run命令复制到文本文件里每次部署都手动执行一遍。问题很明显只要版本一更新文件就会乱只要有一台新服务器就要复制一堆命令只要涉及容器间依赖手工启动顺序很容易出错。Docker Compose把这些问题集中解决掉。它用声明式配置告诉你这个项目有哪些服务、每个服务用什么镜像、对外暴露哪些端口、挂载哪些目录、依赖哪些其他服务。别人拿到你的docker-compose.yml不需要额外说明就能在你机器上复现一套相同的环境。我认为这已经不只是工具使用而是一种基础设施即代码的雏形。6.2 一个MySQLRedis主从的docker-compose.yml实操下面这个文件是我常用模板的简化版可以直接保存为docker-compose.yml使用version: 3.8 services: mysql8: image: mysql:8.0 container_name: mysql8 environment: MYSQL_ROOT_PASSWORD: YourStrongPassword TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql - ./conf/mysql/my.cnf:/etc/mysql/conf.d/my.cnf command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci restart: unless-stopped redis-master: image: redis:7.0 container_name: redis-master command: redis-server --appendonly yes --protected-mode no ports: - 6379:6379 volumes: - redis-data:/data restart: unless-stopped redis-slave: image: redis:7.0 container_name: redis-slave depends_on: - redis-master command: redis-server --replicaof redis-master 6379 --appendonly yes --protected-mode no ports: - 6380:6379 volumes: - redis-slave-data:/data restart: unless-stopped volumes: mysql-data: redis-data: redis-slave-data:这里有几个细节需要解释。container_name是固定容器名称方便你写脚本时引用不写的话Compose会自动生成项目名_服务名_序号这样冗长的名字。depends_on控制启动顺序比如从节点会在主节点启动后再启动但这并不保证主节点已经初始化完成——对于Redis复制来说影响不大但如果你的应用启动后立刻要连接数据库还需要在应用侧做好重试。volumes下面的三段声明创建了命名卷。在Compose里你不再需要手动docker volume create只要声明了卷名docker compose up时会自动创建。6.3 Compose常用生命周期命令与优雅关闭启动整套环境docker compose up -d-d表示后台运行。第一次执行会先拉镜像之后每次启动都很快。查看当前状态docker compose ps查看所有服务日志这个命令在排错时非常有用docker compose logs -f如果只想看某一个服务的日志docker compose logs -f mysql8停止并删除所有由Compose管理的容器、网络但不会删除命名卷里的数据docker compose down如果想要连数据卷一起清掉加-v参数操作前需要非常谨慎docker compose down -v我自己写Compose文件时还习惯加一个.env文件来管理环境变量比如MYSQL_ROOT_PASSWORD这种敏感信息就不直接写在YAML里。Compose会自动读取同目录下的.env文件然后在YAML中用${MYSQL_ROOT_PASSWORD}引用。这在团队协作时能避免把密码提交到代码库但到底怎么做取决于你自己对安全和便利的权衡。7. 容器网络不通从现象到根因的排查链路docker网络不通应该是所有Docker使用者绕不过去的痛点。我在技术群里看到过上百次提问容器已经启动了端口也映射了为什么就是连不上这一节我把排查思路整理成一条清晰的链路你照着一步步走大多数网络问题十分钟内能定位。7.1 端口通了应用连不上的常见误会有一个非常经典的误区在宿主机上curl http://localhost:3306通了就以为MySQL容器没问题但你的后端应用在另一个容器里却连不上。为什么因为后端容器访问localhost指向的是自己不是宿主机。它应该用MySQL容器的IP或者容器名来访问而不是localhost。所以我的第一个建议是不要用localhost作为容器间通信的地址。如果两个容器在同一个Docker网络中直接用对方容器名作为主机名如果在宿主机上访问用127.0.0.1映射端口如果要在同一台机器跨容器、跨网络访问优先把两个容器放进同一个自定义网络。另外容器内应用监听地址也会导致端口通了但连接失败。例如你的Java应用启动时只监听了127.0.0.1:8080那么即使容器端口映射到宿主机8080:8080外部访问依然会被拒绝。排查手段是进入容器docker exec -it app-container netstat -an | grep 8080在容器里看到:::8080或0.0.0.0:8080才是正确监听方式。如果只有127.0.0.1:8080说明应用配置需要改成监听所有接口通常是把server.address设为0.0.0.0。7.2 bridge、host、自定义网络的选型与限制Docker默认的网络配置很容易让新手混淆。默认的bridge网络是每个容器启动时自动加入的容器之间可以通过IP互通但容器名不能作为主机名使用除非你使用docker run --link现在已不推荐。所以单独用docker run启动多个容器时它们都在默认网络里虽然能互相访问IP但IP会变动不适合用来做集群。更好的选择是创建一个自定义bridge网络就像第5章里的redis-net。在自定义网络里容器名自动做DNS解析而且网络隔离更灵活。创建命令docker network create mynet启动容器时指定--network mynet同一个网络里的容器就能用容器名互访。host网络模式是让容器直接共享宿主机的网络栈不再有独立IP也就不用做端口映射。它性能最好但Docker Desktop在Windows/Mac上对host网络模式的支持有差异很多时候无法直接使用。我的建议是在Windows本机做开发调试时优先使用端口映射在Linux服务器上且对网络性能有极高要求时再考虑host模式。7.3 一套可复用的网络排查命令清单最后分享一套我每次排查网络问题时都会执行的清单。首先看端口监听是否正常docker port container-name这个命令列出容器端口与宿主机端口的映射关系。如果结果为空说明你启动时没有加-p参数外部自然无法访问。再看容器实际IPdocker inspect -f {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} container-name拿到IP后从宿主测试连通性ping 172.17.0.2如果ping不通大概率是网络驱动或防火墙问题。然后进入另一个容器内测试docker exec -it app-container bash curl http://db-container:3306如果curl报could not resolve host说明当前网络里没有db-container这个主机名确认两个容器是否在同一个自定义网络。如果curl卡住不动说明端口被防火墙或者应用本身拦截。从宿主机检查端口是否被监听netstat -an | findstr 3306LISTENING状态且地址是0.0.0.0:3306说明正常。如果没有监听很可能是Docker Desktop没有真正启动或者端口映射没有生效。Windows环境下还要关注防火墙。Docker Desktop通常会自动配置Windows防火墙规则但如果你的网络环境策略比较严格还是要手动放行Docker相关的端口和进程尤其是com.docker.backend.exe。排查时可以先临时关闭防火墙测一下确定是它的问题再放行。8. 一些只有亲自动手才会知道的容器部署小经验到这一节你已经把安装、基础命令、MySQL部署、Redis主从、Compose编排和网络排查都过了一遍。作为收尾我想分享几条长期实践后沉淀下来的经验它们不是某个具体命令的参数而是策略层面的东西。第一永远锁定镜像版本。别用latest标签。你今天docker pull mysql:latest拉到的版本和三个月后拉到的版本可能完全不同一次docker compose pull就能让线上环境悄悄升级一个大版本。应该固定为mysql:8.0.36这样具体的版本号。同样镜像仓库里记住digest是最保险的但普通项目至少要做到指定主版本。第二容器里不要存任何需要长期保存的数据。数据卷是唯一出路。如果你发现自己正在往容器内部写配置文件或者日志文件停下来想想能不能挂载到宿主机。容器是一次性用品随时可以被替换但数据不行。第三给所有容器设置restart: unless-stopped。无论是docker run还是Compose都要加上这个重启策略。因为它能让你在服务器意外重启后不用手动一个个重新启动容器。对于没有该参数的容器Docker引擎启动时不会自动拉起它们。第四用docker compose config检查你的YAML文件。这个命令会解析docker-compose.yml并输出完整的配置能帮你发现格式错误、缩进问题、环境变量未定义等情况。我见过不少同事因为YAML缩进不对而反复up/down其实执行一次config就能看到问题。第五不要怕删除容器重建。只要数据卷还在删除容器只是删掉一个运行实例数据完全不受影响。很多人第一次用Docker时对docker rm -f非常恐惧生怕把服务器弄坏。实际上只要数据卷配置正确容器随便删重建才是Docker部署的日常。我在实际项目中已经养成了一套固定习惯新项目一律用Compose定义整套开发环境镜像版本写死数据卷显式声明端口映射做到一台服务器绝不冲突。这套流程从单机到小集群都很稳定希望这篇手册也能帮你形成自己的部署流程。下次再遇到部署难题不妨先问一句——这个组件能不能直接容器化跑起来