1. 为什么你需要Docker:先搞清楚它解决什么问题
1.1 "在我电脑上是好的":环境不一致是最大的隐性成本
做开发这些年,我发现自己最怕听到的一句话不是"系统崩了",而是"在我电脑上是好的"。这句话背后的含义,往往是一个项目换一个环境跑不起来,要么是Java版本对不上,要么是数据库连接串指向了别人机器上的旧库,要么是某个依赖库版本不一致导致行为诡异。你花在"让程序跑起来"上的时间,往往比写代码的时间还要多。
Docker解决的就是这个问题。它把应用连同运行环境一起打包成一个标准化的"容器",无论在开发机、测试机还是生产服务器上,只要这台机器上有Docker引擎,跑起来的容器行为就是一致的。你不再需要写一页又一页的"环境部署文档",不再需要担心"为什么生产环境和本地差了0.0.1版本就不行"。
从技术实现上看,Docker利用了Linux内核的namespace和cgroup能力,实现进程级别的隔离和资源限制。每个容器共享宿主机内核,但拥有自己独立的文件系统、网络栈、进程空间。比起虚拟机,容器没有Hypervisor这一层,所以启动速度更快、资源占用更低、单机可以跑的实例更多。
1.2 镜像、容器、仓库:用一套生活类比讲清三个概念
Docker入门时最容易绕晕的,就是镜像(Image)、容器(Container)、仓库(Repository)这三者的关系。我一般用"蛋糕模具"来讲:镜像是模具本身,容器是从模具里扣出来的蛋糕成品。模具不会因为蛋糕被吃掉而变化,同一个模具可以反复扣出无数个完全一致的蛋糕。
更准确一点说,镜像是只读的模板文件,里面包含了操作系统的基础层、运行环境、应用代码、配置文件。你执行docker run的时候,Docker在镜像之上加了一个可写层,这个可写层加上运行时状态,就是你真正在运行的容器。容器可以被启动、停止、删除,删除容器不会影响镜像本身。
仓库则是集中存放镜像的地方,最知名的就是Docker Hub。docker pull相当于从仓库把模具搬回本地,docker push则是把你的模具上传发布。企业内部一般会搭建私有仓库,比如用Harbor或Registry,避免把内部镜像扔到公网上去。
1.3 Docker改变了交付物:从"部署文档"变成"镜像"
过去交付一个项目,交付物是一堆代码、数据库初始化脚本、环境配置说明、部署步骤文档。收件方得先准备服务器环境、装依赖、改配置、跑脚本,中间任何一个环节不对,流程就卡住了。
用了Docker之后,交付物变成了一个镜像。你在本地把代码、环境、依赖全部固化进镜像,docker push到仓库,服务端只需要docker pull然后docker run,一条命令完成部署。这个转变看起来小,实际上彻底改变了开发和运维的协作方式:开发者对自己构建的镜像负责,运维不再需要猜测"这个服务到底依赖什么系统库"。
所以我的建议是:如果你是刚开始学Docker,别着急背命令,先把"镜像只读、容器可变、仓库分发"这个心智模型建立起来。后面的所有命令,都是围绕这三者以及它们之间的转换关系在操作。
2. 安装Docker的拦路虎:从Windows到Linux的踩坑实录
2.1 Windows下的虚拟化检测失败:先别急着装Docker Desktop
Windows用户遇到的第一个大坑,几乎都是同一个报错:Docker Desktop failed to start because virtualisation support wasn't detected。我第一次碰到这个报错的时候也蒙了,机器明明配置不差,怎么会不支持虚拟化?
这个问题的根源在于:Docker Desktop在Windows上不是直接运行Linux容器,而是借助WSL 2或者Hyper-V创建一个轻量级Linux虚拟机。如果CPU虚拟化没有开启,或者Windows的虚拟化相关功能没有启用,Docker Desktop就起不来。
排查顺序我建议这样来:
打开任务管理器,切到"性能"标签,点"CPU",看右下角的"虚拟化"是否是"已启用"。如果是"已禁用",需要进BIOS开启Intel VT-x或AMD-V,这一步每个主板厂家的菜单不同,但一般在Advanced或Configuration菜单下能找到。
如果是Win11或较新的Win10,确认"适用于Linux的Windows子系统"和"虚拟机平台"这两个Windows功能是否开启。在PowerShell(管理员)里执行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart别忘了重启。这两个功能开关不重启不会生效,我见过不少人改了设置不重启,折腾半天以为装坏了。
还有一个容易被忽略的坑:如果你本身是在虚拟机里装Windows(比如用VMware或VirtualBox做实验),需要在虚拟机的CPU设置里勾选"虚拟化Intel VT-x/EPT或AMD-V/RVI",这就是所谓的嵌套虚拟化。云服务器上没有这个选项,物理机上没问题,虚拟机上经常翻车。
2.2 Linux安装的差异与权限坑:docker命令报permission denied怎么办
Linux下安装Docker,Ubuntu和CentOS的套路不太一样。Ubuntu可以用官方脚本一把梭:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.shCentOS 7建议用yum安装,但官方源里的版本往往比较老,热词里那个"centos7升级docker"说的就是这个问题。老版本Docker和新镜像之间经常出现兼容性问题,升级时建议先卸载旧版本:
sudo yum remove docker docker-common docker-selinux docker-engine sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install docker-ce docker-ce-cli containerd.io装完之后立刻就会遇到权限问题。如果当前用户不是root,执行docker ps大概率会看到:
docker: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock原因是/var/run/docker.sock的属组是docker,普通用户不在这个用户组里。解决办法是把当前用户加进docker组,然后重新登录(或者执行newgrp docker):
sudo usermod -aG docker $USER注意,即使加了用户组,也要重新登录终端才能生效。有些教程会让你直接用root跑,那样确实省事,但在团队环境里不推荐,所有容器操作都走root权限,风险太大。
2.3 镜像加速配置:下载慢或超时的第一道关卡
装好Docker之后第一件事不是创建容器,而是配置镜像加速器。不配置的话,从Docker Hub拉镜像在国内的体验非常糟糕,几兆的镜像等了几分钟还在转圈。
不同系统的配置路径略微不同。Docker Desktop在设置里搜索"registry-mirror"直接填地址,Linux则在/etc/docker/daemon.json里配置:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.nju.edu.cn" ] }配置完重启Docker:
sudo systemctl daemon-reload sudo systemctl restart docker验证是否生效:docker info,输出里能看到Registry Mirrors一栏列出了你配置的地址。
有一点要提醒,镜像加速只对docker pull命令从Docker Hub拉取镜像时生效,对docker build过程中有些基础镜像的拉取也有效。但如果你用的是私有仓库或者自己搭建的Registry,加速器完全不影响。另外,这些公共加速地址的稳定性参差不齐,建议配置两个以上兜底。
3. 镜像、容器、数据卷:用一套命令逻辑串起核心操作
3.1 先分清镜像和容器的关系:一个是模板一个是实例
很多人命令背了不少,但一遇到docker stop和docker rm就分不清区别。这里的关键是搞清楚镜像和容器之间的关系。
镜像(Image)是一个只读文件,你永远不应该去修改一个镜像,任何修改都是基于它再构建一个新镜像。容器(Container)是镜像的运行实例,有生命周期,可以被启动、停止、删除。docker stop只是把容器停了,容器文件还在磁盘上;docker rm才真正把它删掉。
我举一个实战场景:你拉了一个MySQL 8.0镜像,docker run启动一个容器实例,然后在里面建了一些测试库。现在你想清掉这些数据重新来,直接docker rm把容器删掉,再docker run一次,你会得到一个和之前完全一样初始状态的新容器——这就是镜像不可变带来的好处。
3.2 容器生命周期管理:run、ps、exec、stop、rm一套走完
容器管理的核心命令其实不多,按生命周期串起来就够用了:
# 创建并启动一个容器 docker run -d --name my-nginx -p 8080:80 nginx # 查看运行中的容器 docker ps # 查看所有容器(包括已停止的) docker ps -a # 进入容器内部执行命令 docker exec -it my-nginx bash # 停止容器 docker stop my-nginx # 启动已停止的容器 docker start my-nginx # 删除容器 docker rm my-nginxdocker exec -it是要重点说说的,这是排查问题的常用入口。容器是一个最小化的运行环境,很多镜像里没有vim、没有ps、甚至没有bash只有sh,进去之后你可能发现命令不齐全,这是正常的。需要额外工具的话,在容器内用apt或apk现场装,但装完之后容器一删就没了,别指望固化在镜像里,要想固化就得改Dockerfile重新构建镜像。
3.3 端口映射和数据卷:让容器真正接入你的业务
docker run最重要的两个参数,除了--name,就是-p和-v。
-p做端口映射,格式是宿主机端口:容器端口。容器内部有自己独立的网络命名空间,宿主机上的localhost:3306默认是访问不到容器里的MySQL的,必须把宿主机的某个端口映射到容器的3306端口上。这也意味着,同一台机器上你可以用不同宿主机端口跑多个MySQL实例,互不干扰。
-v做数据卷挂载,格式是宿主机目录:容器内目录。容器是临时的,一旦删除,容器内所有写入的数据都没了。想让数据持久化,就要把它写到宿主机目录里。比如MySQL的数据目录挂载到宿主机上的指定路径,即使容器删了,数据还在。
这两个参数组合起来,一条典型的生产级命令长这样:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=yourpassword \ mysql:8.0-e用来设置环境变量,镜像是通过读取环境变量来决定初始配置的,比如MySQL镜像要求必须设置MYSQL_ROOT_PASSWORD才能初始化完成。
3.4 环境变量是镜像的"配置开关"
用Docker部署软件,最核心的思路就是"用环境变量配置行为",而不是改配置文件。每个官方镜像的文档页都会列出支持哪些环境变量,比如:
- MySQL镜像:
MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD - Redis镜像:
REDIS_PASSWORD(新版可能不支持) - PostgreSQL镜像:
POSTGRES_USER、POSTGRES_PASSWORD、POSTGRES_DB
环境变量的好处是把"运维要改的配置"显式化,不要求运维去懂应用内部的配置格式。这也是Docker容器化和传统部署方式的一个核心差异:传统部署改配置文件,容器化部署传环境变量。
4. 第一个实战:Docker部署MySQL 8.0与Redis主从
4.1 MySQL 8.0部署:从拉镜像到客户端连接全流程
有了前面的基础,我们来做一个完整的实战。首先是部署MySQL 8.0,这个场景在项目里太常见了。
第一步,拉镜像:
docker pull mysql:8.0第二步,创建数据目录并启动容器:
mkdir -p /data/mysql docker run -d \ --name mysql8 \ -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=MyRoot@123 \ --restart=always \ mysql:8.0--restart=always是很容易被忽略但极其重要的参数。它保证Docker守护进程启动时自动拉起这个容器,或者容器异常退出后自动重启。生产环境建议加,否则服务器重启后一堆容器处于Exited状态,服务全挂。
第三步,验证容器状态和数据目录:
docker ps ls /data/mysql # 应该能看到数据库初始化的文件初始化需要一点时间,如果马上连接失败,别慌,先docker logs mysql8看看是不是还没初始化完成。
第四步,进入容器连接测试:
docker exec -it mysql8 mysql -uroot -p这里要特别提一个MySQL 8.0的坑:如果你用比较老的客户端连接,可能报Authentication plugin 'caching_sha2_password' cannot be loaded。原因是MySQL 8.0默认认证插件改成了caching_sha2_password,而旧客户端(比如5.x时代的驱动)只支持mysql_native_password。解决办法是在MySQL里改一下root用户的认证插件:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'MyRoot@123'; FLUSH PRIVILEGES;当然,最好是升级程序里的MySQL驱动,毕竟用老认证插件会降低安全性,但如果作为内部开发环境,临时改一下也情有可原。
4.2 Redis主从架构:一条命令拉起从库
Redis主从是另一个高频场景,用Docker搭比传统方式简单得多。这里假设你已经有一个Redis主库在运行:
docker run -d \ --name redis-master \ -p 6379:6379 \ redis:7.0 redis-server --appendonly yes然后拉一个从库,通过--replicaof(旧版叫--slaveof)指定主库地址:
docker run -d \ --name redis-slave \ -p 6380:6379 \ redis:7.0 redis-server \ --replicaof 宿主机IP 6379需要注意,容器之间通过宿主机IP访问,不能写localhost,因为每个容器有自己独立的网络命名空间,从库容器里的localhost指向自己,不是宿主机。
验证主从状态:
docker exec -it redis-slave redis-cli info replication输出里role:slave、master_link_status:up就说明主从正常。刚才在从库上执行写命令会报错(READONLY),这是正确的表现,主从架构里从库本来就是只读的。
用Docker搭Redis主从的好处是环境的隔离和一致性。你在宿主机上清掉容器重新拉一次,得到的还是一个干净环境,不会像传统方式那样残留一堆配置文件和数据目录。
4.3 Docker Compose:用一个yaml文件编排多容器应用
单个容器用docker run还算简单,但一旦涉及多个容器(比如应用容器+MySQL+Redis),命令就变得冗长且难以维护。Docker Compose就是干这个的,它用一个YAML文件描述整个应用栈。
一个典型的示例,docker-compose.yml:
version: '3.8' services: mysql: image: mysql:8.0 container_name: app-mysql restart: always environment: MYSQL_ROOT_PASSWORD: MyRoot@123 MYSQL_DATABASE: appdb ports: - "3306:3306" volumes: - /data/mysql:/var/lib/mysql redis: image: redis:7.0 container_name: app-redis restart: always ports: - "6379:6379" command: redis-server --appendonly yes volumes: - /data/redis:/data app: image: myapp:1.0 container_name: myapp restart: always ports: - "8080:8080" depends_on: - mysql - redis environment: DB_HOST: mysql REDIS_HOST: redis启动整个应用栈只需要一条命令:
docker-compose up -d停止并移除所有容器:
docker-compose down注意depends_on只是控制启动顺序,不保证依赖服务已经可用。MySQL虽然启动了,但可能还没完成初始化,应用容器连接会失败。生产环境一般要在应用代码里加连接重试机制,或者用healthcheck和depends_on的condition来做更精确的控制。
Compose还有一个面向生产环境的推荐做法:把不同环境的配置用docker-compose.override.yml拆开,或者通过环境变量文件.env来做差异化配置。热词里提到的"redis docker compose 生产环境部署",核心就是:容器配置模板化、数据持久化、日志轮转、副本数量按需调整。这也是从demo走向生产环境必须跨过的一道坎。
5. 运行中的救火队:权限、日志、资源占用的常见坑
5.1 Docker权限错误的三个典型场景和对应解法
Docker权限相关的报错非常高频,而且看起来都差不多,但原因可能完全不同。我总结了三个最典型的场景:
场景一:docker: permission denied while trying to connect to the Docker daemon socket。原因是当前用户不在docker用户组,执行sudo usermod -aG docker $USER然后重新登录。
场景二:Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?。这个不是权限问题,而是Docker守护进程根本没起来。Linux上执行sudo systemctl start docker,Windows上检查Docker Desktop是否在运行。
场景三:容器内部访问外部资源时报权限错误,比如写某个挂载目录失败。这种情况通常是宿主机目录的权限设置导致,容器内用户(比如MySQL的mysql用户UID是999)对宿主机目录没有写权限。解决方法是调整宿主机目录属主,使其匹配容器内用户的UID:
chown -R 999:999 /data/mysql这类问题在Linux上特别常见,因为Linux对文件权限严格,不像Mac或Windows的文件共享机制那么"宽容"。
5.2 排查容器问题的黄金三件套:logs、ps、inspect
容器跑起来了但服务访问不通,或者启动后马上退出,这时候怎么排查?我的固定思路是三条命令轮流用。
docker logs是第一个看的,尤其是容器启动失败的场景。比如命令写错了、环境变量缺失、端口被占用,日志里基本都会直接告诉你:
docker logs 容器名docker logs -f可以实时跟踪日志输出,适合观察启动过程。还有一个冷门参数--tail,只显示末尾多少行:
docker logs --tail 100 -f 容器名第二个是docker ps -a。docker ps只看运行中的容器,-a连退出的也一起显示。你会看到类似:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES abc123xx mysql:8.0 "docker-entrypoint.s…" 2 minutes ago Exited (1) 2 minutes ago mysql8Exited (1)的退出码就是程序报错退出的信号,配合docker logs基本能定位问题。
第三个是docker inspect,它输出容器的完整JSON配置信息。当需要确认容器挂载了哪些卷、设置了什么环境变量、网络模式是什么、端口映射是否生效时,用它:
docker inspect mysql8如果输出太长,可以用--format过滤关键信息:
docker inspect --format='{{.HostConfig.Binds}}' mysql8热词里提到一个"docker: unexpected eof"的报错。这个我遇到过,一般出现在拉镜像或者推送镜像过程中网络中断,镜像层数据不完整导致的。解决办法是先重试,还是不行的话清理残留再拉:
docker image prune -f docker pull 镜像名:标签5.3 容器日志无限增长与脏镜像清理
Docker用久了,有个很奇怪的现象:宿主机磁盘空间越来越少,甚至报警。查来查去,往往是容器日志文件在悄悄膨胀。
默认情况下,容器的标准输出日志全部累积写在/var/lib/docker/containers/<container-id>/目录下的json文件里,不设上限的话,一个每天打印大量日志的容器,几个月能吃掉几十甚至上百GB磁盘。
解决方法是给Docker配置日志轮转。修改/etc/docker/daemon.json,添加:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }意思是单个日志文件最大10MB,保留3个历史文件,超出就轮转删除。配置完重启Docker生效。这个配置只能作用于新创建的容器,老容器需要重建才会应用新的日志策略。
另外两个常用的清理命令:
# 查看磁盘占用分布 docker system df # 清理所有不再使用的资源(容器、镜像、网络、构建缓存) docker system prune -aprune -a是重手术,会删掉所有没有被正在运行的容器使用的镜像,包括你可能后面还要用的旧版本镜像。执行前最好docker system df先看一眼,确认没有误删的风险。
5.4 容器资源限制:别让一个容器拖垮整个机器
还有一个容易被忽视的坑:默认情况下容器没有CPU和内存限制,它可以用完宿主机的所有资源。生产环境里如果某个应用在容器内发生内存泄漏,极端情况下会影响同机器上的其他服务。
所以建议在docker run或Compose配置中明确资源限制:
docker run -d \ --name myapp \ -m 512m \ --cpus=1 \ myapp:1.0-m限制最大内存512MB,--cpus限制最多使用1核。对应Compose写法:
deploy: resources: limits: cpus: '1' memory: 512M设定资源限制并不是单纯为了"扣容量",更重要的是让这个容器的行为可预期。出问题的时候,容器被内存限制杀掉(OOM)总比把宿主机整挂好得多。日志里看到Killed字样,多半就是内存超限了。
回到文章开头说的"在我电脑上是好的",其实用了Docker之后,这句话消失了,但新的问题出现了:环境变多了,容器编排变复杂了,你需要操心的事情从"装环境"变成了"管容器"。不过好消息是,Docker这套工具链把原来靠人肉维护的部署流程,变成了可以写进文件、可以版本管理、可以自动化的标准流程,这才是它真正的价值。我个人习惯是每部署一个服务,都会同步更新docker-compose文件并提交到项目仓库里,下次换机器、换环境、加副本,一条命令搞定,不用再翻聊天记录找当初是怎么启动的。