前阵子有朋友找我排查环境问题:一台上线不久的服务器上,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 wasn't 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 a+r /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 KEY=VALUE | 传入环境变量 |
| --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和--cpus,Docker 默认不对容器做资源限制。一个出问题的应用容器可能会吃满宿主机所有内存,导致整台服务器响应迟缓甚至卡死。一个比较稳妥的启动姿势是显式限制资源上限:
docker run -d --name app \ --memory=512m \ --cpus=1 \ --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_PASSWORD=YourStrongPass \ -e MYSQL_DATABASE=appdb \ -e TZ=Asia/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主从:容器间用服务名通信而不是localhost
Redis 主从架构用 Docker 搭建非常快,但有一个概念必须转变:同一个 Docker 网络内的容器,互相访问时要用容器名或服务名,而不是 localhost。因为每个容器都是独立 IP,localhost 只指向容器自己。
先创建一个自定义网络:
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只会删除容器和默认网络,不会删除