
前阵子有朋友找我排查环境问题一台上线不久的服务器上MySQL 8.0 一启动Redis 服务就开始丢连接再仔细看Java 服务依赖的某个系统库版本也被一连串升级动作搞坏了。他说明明装的时候每一步都照着文档来为什么一合在一起就崩。这种场景我做运维这些年见过太多次根子不在哪条命令而在于所有软件共享同一个操作系统环境——你装的东西越多依赖、端口、配置互相踩踏的概率就越高。后来我搭了一套 Docker 容器环境把 MySQL、Redis、应用各放进各的沙箱半小时收工。这篇内容我打算写给两类人看一类是从没用过 Docker想搞明白“容器到底是什么、值不值得学”的开发者另一类是已经在用 docker run但碰到过服务起不来、数据丢、容器删了不会恢复等问题的使用者。全文不会只贴命令我会把为什么这样做的逻辑、部署部署中最容易踩的坑、以及从开发机走向生产时你会遇上的设计问题一次性讲清楚。1. 容器到底解决了什么问题从一次环境冲突说起1.1 一个我再熟悉不过的报错现场很多人第一次对 Docker 心动不是看了什么高深的技术文章而是真被环境问题折磨过。我自己职业生涯里最狼狈的一段时间就是给公司维护一套“不能乱动”的服务器主机上跑着老业务Python 3.6 写的升级操作系统自带的 OpenSSL 都不敢新项目要用 Python 3.11又要求 MySQL 8.0还希望 Redis 主从复制能快速搭起来做缓存。这些需求单看都不难但堆在同一台机器上就成了环境地狱。问题的本质其实很朴素物理机上所有进程共享的不只是 CPU 和内存还有文件系统、环境变量、动态链接库、全局端口。你在系统层装了 A 库的 2.0 版本另一个服务依赖 1.0 的某个行为它可能不会立刻报错但会在某个深夜以奇怪的方式崩一次。虚拟化技术可以隔离但传统虚拟机给整台机器装操作系统开销大、启动慢、分发困难。Docker 这类容器技术则选择了另一个角度不虚拟整台机器只把“应用运行所需的文件、配置、依赖、系统调用环境”打包成一个独立空间多个容器共享同一个宿主内核却各自拥有逻辑上隔离的文件系统和进程空间。1.2 镜像和容器安装包与运行实例的分工围绕 Docker 最常听到的两个词是“镜像”和“容器”。网上说镜像像“类”、容器像“对象”这个类比没问题但我觉得还有更贴近直觉的讲法镜像像一张装好系统和软件的“安装光盘”你随时可以用它创建出多台电脑每台电脑都是同一个安装环境容器就是从这个安装光盘里启动出来的那一台台正在运行的机器。其中“层”的概念是 Docker 比传统安装包优雅很多的地方。镜像不是一大块静态文件而是由一层层只读文件系统堆叠而成的。你用 Dockerfile 构建时每一行指令产生一层下次构建如果前面几层没变Docker 可以直接复用缓存只有变化的部分才重新生成。这也是为什么拉同一个基础镜像没那么占地方——本地已经有相同的底层镜像层时增量下载即可。更重要的含义是发布与回滚可以基于版本化管理。我给某个服务打镜像时会习惯把版本号作为 tag 固化下来比如 app:v1.2.3上线时拉指定版本出问题回滚到上一个 tag 就行。容器不是玄学它只是把“运行环境”这件原本靠人工记忆和手写文档的事变成了可验证、可复现、可分发的产物。1.3 都是“容器”但 Docker 容器和编程里的容器是两码事学习 Docker 的第一步反而是要理清词义因为“容器”这个词在计算机领域被用得太泛滥了。很多初学者看到“STL 容器”“Java 容器”会产生困惑难道它们也能隔离环境吗并不是。C 里 vector、map、list 这类容器指的是在内存中存放数据的数据结构“容器”描述的是一种组织数据的方式Java 语境下的容器早期更多指 Tomcat 这类能承载 Web 应用的运行环境或者是 Collection 集合框架和进程隔离没有直接关系。Python 里判断“某个数据是否是指定容器的内容”同样是在谈集合类型。真正做成进程级隔离和资源隔离的 Docker 容器属于操作系统层面的技术它和编程语言里的“容器”唯一共同点只是都借用了“把东西装起来”这个生活词汇。在云原生技术栈里Docker 指容器运行时而 Kubernetes 里的 Pod 可以理解为一组共享网络和存储的容器的调度单元所谓 HPA 又是针对 Pod 副本数量做弹性伸缩的机制。把这些概念拆开你就不会被一堆“容器”热搜词绕晕。2. 安装流程与翻车现场Windows、Linux和镜像加速2.1 Docker Desktop在Windows上的虚拟化检测与修复在 Windows 上装 Docker大概率会遇到 Docker Desktop 这个工具。它是带图形界面的客户端把 Docker 引擎、Compose、Kubernetes 单机模式都整合到了一起。安装文件双击就能装但很多人卡在了第一次启动时的报错最常见的提示是Docker Desktop failed to start because virtualisation support wasnt detected看到这个提示第一反应不要是卸载重装。Docker Desktop 在 Windows 上默认使用 WSL2 后端也就是说它其实是在 WSL2 提供的轻量级 Linux 环境中运行 Docker 引擎。报错说“检测不到虚拟化支持”可能的原因有好几种要按顺序排查到任务管理器 - 性能 - CPU确认右下角的“虚拟化”是否显示“已启用”。如果显示禁用需要进 BIOS/UEFI 开启 Intel VT-x 或 AMD-V。以管理员身份打开 PowerShell运行systeminfo看最后一段里 Hyper-V 相关要求是否都满足。如果虚拟化已启用但仍报错多半是 Windows 功能没开全。需要开启“适用于 Linux 的 Windows 子系统”和“虚拟机平台”两个可选功能可以执行下面两条命令dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完后重启系统再到 Microsoft Store 安装或更新 WSL执行wsl --set-default-version 2。还有一个容易忽略的点如果机器上已经开启了 Hyper-V但 Windows 沙盒等相关组件状态异常也会导致 Docker Desktop 起不来。可以用管理员 PowerShell 执行bcdedit /set hypervisorlaunchtype auto再重启。2.2 在Ubuntu/CentOS上安装Docker Engine的最低成本路径Linux 服务器上不需要 Docker Desktop安装 Docker Engine 就好。Ubuntu/Debian 系建议走官方 apt 源CentOS/RHEL 系用 dnf/yum 的官方仓库。看到网上那种“curl 一个脚本一键安装”的教程要谨慎不是不能用而是你不知道脚本会在系统上执行什么生产环境尽量选择可审计的官方方式。Ubuntu 20.04/22.04/24.04 上比较标准的做法是sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release 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 \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginCentOS/RHEL 7 以上的路径是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-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker安装完成后如果不想每条命令都加 sudo可以把当前用户加入 docker 组sudo usermod -aG docker $USER然后重新登录一次。这里必须提醒一句加入 docker 组相当于授予了等同于 root 的权限因为 docker 进程本身有 root 权限能操作 docker 就能挂载宿主机目录、控制宿主机文件。个人开发机图方便没问题生产环境还是要收敛操作权限。2.3 镜像加速配置与服务启动失败的排查链路很多人在安装完成后的第一个动作是拉镜像然后发现下载进度条纹丝不动或慢得离谱。这里先说明一下Docker 默认从 Docker Hub 拉取公共镜像Docker Hub 的镜像分发节点分布在不同地域你要是恰好赶上链路不佳的时段下载大镜像确实会很痛苦。一个通用的解法是配置镜像加速器也就是在 Docker 的 registry-mirrors 里填上可达性更好的镜像仓库地址。在 Linux 上编辑 /etc/docker/daemon.json{ registry-mirrors: [ https://你的专属加速地址.mirror.aliyuncs.com ] }这个地址从哪里来各云服务商的容器镜像服务控制台都会提供“镜像加速器”页面注册后能拿到一个专属地址也有社区维护的公共镜像加速站但稳定性参差不齐我个人更推荐使用云服务商的专属地址。改完配置后执行sudo systemctl daemon-reload sudo systemctl restart docker docker info看到Registry Mirrors部分出现你配置的地址就算生效。如果 Docker daemon 在修改配置之后起不来多半是 JSON 文件语法写错了。别急着盲改先看日志journalctl -u docker -n 100 --no-pager日志里出现类似invalid character的报错时基本可以断定是 daemon.json 的引号或逗号问题。用一行命令校验 JSON 结构比肉眼检查靠谱得多python3 -m json.tool /etc/docker/daemon.json我在实际运维里见过太多因为逗号放错位置导致 docker 服务起不来的案例所以建议把 JSON 校验当成改配置后的固定动作。另一个常见启动失败原因发生在启用了 firewalld/ufw 的系统上Docker 要创建 docker0 网桥和 iptables 规则如果内核模块没加载会报类似“docker0: iptables: No chain/target/match by that name”的错误。可尝试先加载关键内核模块sudo modprobe br_netfilter确认docker0网桥能够创建出来再启动服务。排查这类问题不需要死记硬背关键是形成“日志优先、配置文件校验、内核与系统服务状态依次检查”的链路多数服务启动失败都可以按这个思路定位。3. 镜像、容器、数据卷先想明白再敲命令3.1 按生命周期理解高频命令docker 命令非常多逐条背没有意义建议按“镜像生命周期”和“容器生命周期”两条线去理解。镜像相关的是构建、拉取、查看、删除、打标签pull、build、tag、push、images、rmi、inspect。容器相关的是创建、启动、停止、查看进程、进入内部、看日志、删除run、start、stop、restart、ps、exec、logs、rm。高频参数集中在 docker run 上参数作用-d后台运行容器--name给容器起名字便于管理-p 宿主机端口:容器端口端口映射-v 宿主机目录:容器目录目录挂载-e KEYVALUE传入环境变量--network指定网络--restart重启策略如 unless-stopped--memory / --cpus限制内存与 CPU新手最容易忘的是给容器加 --name。不加名字时容器的 ID 是一串乱码一样的十六进制字符串后来你想停它还得先查 ID非常麻烦。规范一点的做法是每个容器启动时都配上明确的名称并用docker ps -a检查所有容器状态。注意docker ps默认只显示运行中的容器加上 -a 才能看到已经退出的容器。3.2 容器删除后数据还在吗三种挂载方式这是对 Docker 误解最深的一个点。很多人以为容器和虚拟机一样删除容器只是把运行实例关掉数据还在“虚拟硬盘”里。但 Docker 容器的默认特征恰恰是“无状态”容器内写入的文件只要容器被删除就可能随之消失。存储位置决定命运这是理解容器持久化的第一原则。处理数据有三种常见方式。第一种是 bind mount也就是把宿主机上某个目录直接映射进容器里比如-v /data/mysql:/var/lib/mysql。这种方式的优点是你知道数据放在哪、备份方便、想看的文件直接用宿主机命令就能看缺点是把权限控制交给了宿主机目录宿主机目录如果被误删数据一样丢。第二种是 named volume通过docker volume create或在 -v 参数里直接给一个名字如-v mysql-data:/var/lib/mysql数据由 Docker 管理存放在 /var/lib/docker/volumes/ 下。它比 bind mount 更“干净”跨主机迁移时通过卷驱动处理更方便适合需要 Docker 自己管理生命周期的数据。第三种是 tmpfs 挂载数据只存在内存里容器停止即清空适合缓存和临时文件生产环境通常不会拿它保存关键数据。3.3 端口映射、容器网络与资源限制要理解-p 8080:80先得明白容器有自己的网络命名空间它在内部看自己是独立主机有自己的 IP。默认 bridge 网络下的容器可以对外访问互联网但从外部访问容器则需要端口映射。-p 8080:80的含义是把宿主机的 8080 端口转发到容器的 80 端口之后你访问宿主机 IP 的 8080就能进到容器的 80 服务。于是出现一个新手必踩的坑同时启动两个容器都写-p 3306:3306第二个容器启动必然失败报错类似Bind for 0.0.0.0:3306 failed: port is already allocated宿主机端口是全局资源不能重复占用。解决办法是给第二个容器改宿主机的映射端口比如3307:3306。还要注意--memory和--cpusDocker 默认不对容器做资源限制。一个出问题的应用容器可能会吃满宿主机所有内存导致整台服务器响应迟缓甚至卡死。一个比较稳妥的启动姿势是显式限制资源上限docker run -d --name app \ --memory512m \ --cpus1 \ --restart unless-stopped \ app:1.0.0容器运行期间用docker stats观察每个容器的 CPU、内存占用比在宿主机上逐个对比进程高效得多。这也能帮你尽早发现异常容器。4. 实战用容器拉起MySQL 8.0、Redis主从再用Compose编排4.1 MySQL 8.0的数据目录外置与备份恢复假设我现在要在一台干净服务器上部署 MySQL 8.0首先会建好宿主机数据目录mkdir -p /data/mysql8/data /data/mysql8/conf /data/mysql8/backup chown -R 999:999 /data/mysql8/data数据目录属主设为 999是因为 MySQL 官方镜像内部以 mysql 用户运行它的 UID 通常是 999。这一步处理不好容器启动后可能因为无权写入数据目录而失败。接着启动容器docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourStrongPass \ -e MYSQL_DATABASEappdb \ -e TZAsia/Shanghai \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/data:/var/lib/mysql \ --restart unless-stopped \ mysql:8.0为什么要把数据目录挂到宿主机因为容器本身随时可以被删掉重建镜像可以重新拉但只要 /data/mysql8/data 里的数据文件还在新容器一启动就能无缝接管。这也让升级镜像版本变得可回退先停旧容器用新 tag 启动同一个数据目录如果启动异常马上切回旧镜像。镜像首次启动会做初始化耐心等一段时间观察日志docker logs -f mysql8看到类似ready for connections的日志就说明 MySQL 已可用。用容器内客户端连一下docker exec -it mysql8 mysql -uroot -pMySQL 8.0 默认认证插件是 caching_sha2_password一些老语言的客户端驱动不支持会报认证失败。出现这种情况不要硬刚直接在容器里为业务创建独立账号CREATE USER app% IDENTIFIED BY appPass123; GRANT ALL PRIVILEGES ON appdb.* TO app%; FLUSH PRIVILEGES;再说备份。很多人会“进入容器执行 mysqldump”但更规范的方式是直接在宿主机上用 docker exec 调用容器内的 mysqldump把备份文件落在宿主机docker exec mysql8 sh -c exec mysqldump -uroot -p$MYSQL_ROOT_PASSWORD appdb /data/mysql8/backup/appdb_$(date %F).sql恢复时反过来cat /data/mysql8/backup/appdb_2025-01-01.sql | docker exec -i mysql8 sh -c exec mysql -uroot -p$MYSQL_ROOT_PASSWORD appdb这样不需要在容器里装任何额外工具备份恢复对宿主机使用者来说就跟操作普通文件一样。4.2 Redis主从容器间用服务名通信而不是localhostRedis 主从架构用 Docker 搭建非常快但有一个概念必须转变同一个 Docker 网络内的容器互相访问时要用容器名或服务名而不是 localhost。因为每个容器都是独立 IPlocalhost 只指向容器自己。先创建一个自定义网络docker network create redis-net启动主节点docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ -v /data/redis-master/data:/data \ redis:7.0 \ redis-server --appendonly yes --requirepass MasterPass123启动从节点docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ -v /data/redis-slave/data:/data \ redis:7.0 \ redis-server --appendonly yes \ --replicaof redis-master 6379 \ --masterauth MasterPass123 \ --requirepass SlavePass123这里有几个细节容易出错。第一从节点依赖--replicaof redis-master 6379使用的 redis-master 是容器名Docker 内置 DNS 会解析到主节点的容器 IP第二如果主节点设置了 requirepass从节点必须配置 masterauth第三从节点对外映射的是宿主机的 6380 端口容器内部还是 6379这是初学者比较容易绕晕的地方。验证主从状态docker exec -it redis-slave redis-cli -p 6379 -a SlavePass123 info replication关注输出中的role:slave和master_link_status:up只要 master_link_status 是 up说明复制正常。你可以试着在主节点写入一个 key再到从节点读能读到就说明数据同步链路是通的。4.3 Compose声明式编排与卷清理的坑用 docker run 启动一个两个容器还行环境一多就会失控。Compose 的价值在于把“一组相关容器”的配置写进一个 YAML 文件项目启动用一条命令搞定。比如上面 MySQL 和 Redis 这套可以整合成services: mysql8: image: mysql:8.0 container_name: mysql8 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: YourStrongPass MYSQL_DATABASE: appdb TZ: Asia/Shanghai ports: - 3306:3306 volumes: - /data/mysql8/data:/var/lib/mysql - /data/mysql8/conf:/etc/mysql/conf.d redis-master: image: redis:7.0 container_name: redis-master restart: unless-stopped command: [redis-server, --appendonly, yes, --requirepass, MasterPass123] ports: - 6379:6379 volumes: - /data/redis-master/data:/data redis-slave: image: redis:7.0 container_name: redis-slave restart: unless-stopped depends_on: - redis-master command: [redis-server, --appendonly, yes, --replicaof, redis-master, 6379, --masterauth, MasterPass123, --requirepass, SlavePass123] ports: - 6380:6379 volumes: - /data/redis-slave/data:/data在文件所在目录执行docker compose up -d整套环境就起来了。日常维护用docker compose ps看状态docker compose logs -f看日志。有一点必须提醒执行docker compose down只会删除容器和默认网络不会删除