刚入行那阵子,我接过一个老项目,光把环境跑起来就折腾了两天。JDK版本不对、Redis本地没装、MySQL编码集不一致、Nginx配置在mac上和Linux上行为还不一样。后来项目里引入Docker,新同事入职,一条docker compose up -d,整套环境十分钟拉齐。
如果你现在还没用上Docker,或者装了Docker但基本靠到处复制粘贴命令、出了问题完全不知道从哪里下手,这篇东西就是给你写的。我会把Docker到底是什么、安装时要避开的坑、最常用的几个操作、一个完整的MySQL实战,以及入门阶段一定会遇到的几个报错,一次性讲清楚。
1. 先搞清楚Docker到底解决了什么问题
1.1 环境不一致:每个程序员都遇到过的“配置地狱”
说一个很经典的场景:你在自己电脑上写好的代码,提交到服务器上,跑起来直接崩。看一眼报错,依赖版本对不上;再一看,Windows下的路径分隔符和Linux下的不一样;继续排查,数据库连不上,因为服务器上压根没装MySQL。
这套问题在行业内有个叫法:works on my machine(在我机器上能跑啊)。根子在于,代码运行的环境不是一个单一的东西,而是操作系统、运行时、依赖库、配置文件、中间件共同作用的结果。任何一环不一样,行为就可能不一样。
Docker解决的就是这个问题。它把应用连同它需要的运行环境一起打包成一个标准化的镜像,任何装了Docker的机器上都能以完全一致的方式运行。我不需要关心目标服务器是CentOS还是Ubuntu,不需要手动装依赖,不需要担心某个依赖的版本跟系统的包管理器冲突。
1.2 镜像、容器、仓库:这三个概念必须彻底理解
很多人初学Docker会卡在术语上,其实这三个词用生活类比一下就通了。
**镜像(Image)**就是一个打包好的模板,里面包含运行应用所需的全部内容:代码、运行时、系统库、配置文件。它只读,不能直接运行,就像安装包或者光盘里的系统镜像。
**容器(Container)**是由镜像创建出来的运行实例。同一个镜像可以创建多个容器,它们之间相互隔离。容器可以启动、停止、删除,容器里的数据在容器删除后通常会消失。
**仓库(Registry)**是存放和分发镜像的地方,最知名的是Docker Hub。你从仓库拉取(pull)一个镜像,再用它启动容器(run)。
一个容易混淆的点:镜像和虚拟机镜像不是一回事。虚拟机镜像包含一个完整的操作系统,虚拟机管理程序(Hypervisor)直接虚拟化硬件;Docker镜像则共享宿主机的操作系统内核,只包含应用层以及它依赖的库和文件。所以Docker容器比虚拟机轻得多,启动通常是秒级,而不是分钟级。
1.3 什么时候不该用Docker
Docker不是万能的。这一点我想在前言就讲清楚,免得你走弯路。
需要图形界面的桌面应用,用容器跑会很别扭,虽然可以折腾X11转发、VNC方案,但远不如原生安装流畅。对硬件延时极其敏感的应用,比如高性能计算场景,Docker的网络和存储层会带来额外开销,需要谨慎评估。另外,如果你只是写个脚本,也不一定要容器化,直接跑可能更快。
适合用Docker的场景非常明确:Web应用、微服务、中间件(MySQL、Redis、Nginx这类有状态或无状态服务)、CI/CD构建环境、临时测试环境。现在你看到一个GitHub项目,如果它提供Dockerfile或者docker-compose.yml,基本可以确定这个项目想在几分钟内让你跑起来。
2. 安装Docker:两条主流路径和一个常见报错
安装Docker这件事,看起来简单,实际踩坑的人特别多。Windows和Linux环境差异巨大,我就分开说。
2.1 Linux环境安装:Ubuntu和CentOS都有各自的门道
如果是在云服务器或者自己的Linux机器上,Docker官方推荐用apt或yum直接安装。
Ubuntu / Debian系:
sudo apt update sudo apt install -y docker.io这条命令装完后,docker服务会自动启动并设置开机自启。部分Ubuntu版本里,docker.io这个包是系统自带的旧版本。如果你希望装最新版,需要添加Docker官方的APT仓库。我个人建议,生产环境用官方仓库安装;只是本地玩一下,系统源里的docker.io其实够用。
CentOS / RHEL系:
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 sudo systemctl start docker sudo systemctl enable dockerCentOS 7的坑比较多,主要在于内核版本和iptables的兼容性。老内核(3.10)跑新版Docker偶尔会有莫名其妙的问题,一些人会选择升级内核,但更稳妥的做法是用系统自带的docker包:
sudo yum install -y docker sudo systemctl start docker sudo systemctl enable docker在没有systemd的老系统上,用service docker start代替即可。安装完成后,关键验证命令是:
docker version能看到Client和Server两端信息,才算装成功。如果只看到Client而Server端报错,说明守护进程没起来,先看服务状态,再用journalctl -u docker查日志。
2.2 Windows下Docker Desktop与WSL2的配合
Windows上现在统一是用Docker Desktop,它默认依赖WSL2(Windows Subsystem for Linux)来运行Linux容器。现代Windows 10/11系统全新安装的话,一般缺的不是Docker Desktop本身,而是WSL2和虚拟化平台。
大致步骤是:
- 以管理员身份打开PowerShell,执行:
wsl --install重启电脑。
下载Docker Desktop安装包(docker.com/products/docker-desktop/),直接安装,一路下一步。
启动Docker Desktop,进入Settings -> General,确保Use the WSL 2 based engine是勾选状态。
还有一个很影响体验的操作:Docker Desktop默认镜像目录在C盘,用久了C盘会爆。可以在Settings -> Resources -> Advanced里把Disk image location改到D盘或别的数据盘,建议装完第一件事就做。
2.3 “Virtualization support not detected”这类报错到底怎么解
很多人在Windows上第一次启动Docker Desktop,会看到类似这样的报错:
Virtualization support was not detected. Docker Desktop failed to start because virtualization support wasn't detected.
看到这个先别慌。这句提示说的是:硬件虚拟化没有被系统正确识别。排查顺序是这样:
第一,进BIOS/UEFI找虚拟化开关。Intel处理器叫VT-x或Intel Virtualization Technology,AMD叫SVM Mode。把它从Disabled改成Enabled,保存重启。
第二,在Windows里打开“启用或关闭Windows功能”,确认Hyper-V和“虚拟机平台”两个项处于勾选状态。如果没Hyper-V选项,那多半是Windows版本是家庭版,家庭版默认没有Hyper-V,此时要么升级专业版,要么依赖WSL2模式跑容器。
第三,如果BIOS和Windows功能都开了还是报错,在管理员PowerShell里执行:
systeminfo看最下面Hyper-V要求那一栏。如果显示“检测到虚拟机监控程序。将不显示Hyper-V所需的功能”,说明Hyper-V还在跑;如果显示未检测,继续往上排查BIOS。
很多人的问题不在BIOS,而是电脑上装了VMware或VirtualBox这类虚拟机软件,它们和Hyper-V存在竞争关系。装了VMware的同时又想用Docker Desktop,两个平台对虚拟化的状态要求不同,会出现各种冲突。我遇到过的案例里,最后是把VMware的CPU虚拟化引擎设置改了一次设备开机自检才顺利。如果同时需要多个虚拟机平台,建议在Docker Desktop设置里切换为WSL2后端,别用Hyper-V后端,兼容性好很多。
3. 第一次跑起来:从一个镜像到运行中的容器
3.1 hello-world背后到底发生了什么
装好Docker,第一个命令几乎永远是:
docker run hello-world这条命令会经历这些步骤:
- 本地找不到名为hello-world的镜像;
- Docker自动去Docker Hub远程仓库拉取镜像;
- 下载完成后,根据镜像内容创建并启动一个容器;
- 容器运行程序,向终端输出一行问候信息;
- 程序执行完毕,容器退出,但容器本身还存在于磁盘上(状态为exited)。
这个流程虽然简单,但藏着Docker最核心的“客户端-守护进程”架构:你敲的docker命令是客户端,真正干活的是后台的docker daemon(守护进程)。客户端不直接跟内核打交道,它把所有指令发给daemon,daemon负责拉镜像、启动容器、管理网络和存储。
验证一下:跑完hello-world后,执行docker ps -a,会看到一个状态为exited的hello-world容器。它没有被自动清理,只是因为任务完成退出了。这种“退出但存在”的机制是理解Docker生命周期管理的起点。
3.2 镜像下载慢:配置镜像加速器
新装的Docker默认从Docker Hub拉镜像。在国内,直连Docker Hub的下载速度经常让人崩溃,几百MB的镜像能拖十几分钟。这时候需要配置镜像加速器。
我自己用的是阿里云容器镜像服务的加速器地址,需要登录阿里云容器服务控制台,在“镜像加速器”页面获取专属地址(每个人的地址不一样,格式是https://xxxxxx.mirror.aliyuncs.com)。拿到地址后配置:
Linux环境编辑/etc/docker/daemon.json:
{ "registry-mirrors": ["https://你的专属地址.mirror.aliyuncs.com"] }保存后重启Docker:
sudo systemctl daemon-reload sudo systemctl restart dockerDocker Desktop用户则是Settings -> Docker Engine,把同样的配置追加进JSON,点Apply & Restart。
除了阿里云,中科大、网易等也提供镜像加速服务,选一个稳定可用的就行。配置完成后再跑docker pull,速度会有天壤之别。
3.3 容器生命周期:启动、停止、删除的完整闭环
入门阶段必须搞清楚一组命令,它们对应容器的生命周期:
docker ps # 查看运行中的容器 docker ps -a # 查看所有容器(包括已退出的) docker stop 容器ID # 优雅停止容器 docker start 容器ID # 启动一个已存在的停止状态的容器 docker restart 容器ID # 重启容器 docker rm 容器ID # 删除容器(只能删已停止的) docker rm -f 容器ID # 强制删除运行中的容器一个常见的误区是:docker stop 容器后,容器并没有消失,它只是从running变成exited。下次docker start同一个容器ID,会恢复原来的配置再启动。而docker rm会把容器文件删除,这个操作不可逆。
容器ID可以不用写全,写前几位即可,比如docker stop 9d2e。如果容器很多,也可以给容器起名字:
docker run --name my-nginx -d nginx加上--name参数后,后续所有命令都可以用my-nginx代替容器ID,好记也好排查。
4. 新手最该掌握的五个核心操作
入门阶段,这几个操作占了我日常使用Docker的八成。每个都配合一个实际场景来讲解,顺带说明为什么这么用。
4.1 端口映射:让容器外的主机能访问容器内的服务
容器默认在一个独立的网络命名空间里,宿主机没法直接访问容器内的端口。比如容器里跑着一个Nginx监听80端口,你在宿主机浏览器打开http://localhost:80,大概率连不上。
需要做端口映射:
docker run -d -p 8080:80 nginx-p参数格式是“宿主机端口:容器端口”。上面命令把宿主机的8080端口映射到容器的80端口,访问宿主机8080就相当于访问容器的80。
理解这个方向很重要:左边是宿主机端口,右边是容器端口,不能反过来写。如果你宿主机端口写成了80:8080,会把宿主机80端口转发到容器8080,而容器8080上什么都没监听,请求就会失败。
多端口或者端口段也支持:
docker run -d -p 8080:80 -p 8443:443 nginx生产环境通常不会把端口映射暴露到外网,而是通过Nginx反向代理统一收口。本地开发和测试时,-p怎么方便怎么来。
4.2 数据持久化:容器删了,数据不能跟着删
容器一删,它内部写入的文件全部消失。对于MySQL、Redis这种数据库服务来说,这几乎是灾难。
解决方法是用卷(volume)或者绑定挂载(bind mount)。最简单的使用是绑定挂载,把宿主机的一个目录直接映射到容器的某个路径:
docker run -d -v /home/myuser/mysql-data:/var/lib/mysql mysql:8.0容器往/var/lib/mysql写入的数据,实际落在宿主机的/home/myuser/mysql-data里,即使容器被删除,数据还在。
另一种做法是命名卷:
docker volume create mysql-data docker run -d -v mysql-data:/var/lib/mysql mysql:8.0命名卷由Docker管理,实际存储在宿主机的一个特定目录下,不容易被误删,但不如绑定挂载直观,想进目录看文件需要docker volume inspect。
我自己的习惯是:本地开发用绑定挂载,好调试、好查看;生产环境用命名卷,可移植性更好。
4.3 环境变量:给容器传配置的标准方式
镜像通常是通用的,不同的应用场景靠环境变量区分。最典型的例子是MySQL镜像的root密码:
docker run -d -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:8.0-e参数可以重复使用,传多个变量。不同镜像需要哪些环境变量,在Docker Hub的镜像详情页都能看到。比如MySQL镜像,常用的还有MYSQL_DATABASE(创建数据库)、MYSQL_USER和MYSQL_PASSWORD(创建普通用户)。
如果你有大量需要传递的配置,把这些变量整理到.env文件里,然后:
docker run --env-file .env ...维护起来更干净。注意.env文件里不要有引号,格式类似KEY=value。
4.4 进入容器内部:docker exec -it的完整用法
容器不是黑盒,排查问题的时候需要进去看进程、看日志、跑命令。登录容器的命令是:
docker exec -it 容器ID /bin/bash如果容器里没有bash,用:
docker exec -it 容器ID /bin/sh-it是两个参数组合:-i让标准输入保持打开,-t分配一个伪终端。没有-t的话命令能执行但不会有交互式界面,没有-i的话一些需要输入的命令会直接退出。两个一起用,才能获得一个能交互的终端。
进去之后你看到的是一套精简的Linux文件系统。多数官方镜像基于Alpine或Debian Slim,里面可能没有vim、没有curl、没有ps,这是正常现象。需要装的话,Alpine系用apk add,Debian系用apt update && apt install。不过我更推荐的方式是尽量不在容器里额外装东西,需要什么命令直接在外面用docker exec配合宿主机工具执行,保持容器精简。
检查容器里运行的进程:
docker exec 容器ID ps aux看容器监听的端口:
docker exec 容器ID netstat -tlnp如果容器里连netstat都没有,也可以退出来用宿主机命令配合容器IP排查。
4.5 日志与资源占用:排错的两大利器
容器输出到标准输出的日志,Docker会统一捕获:
docker logs 容器ID持续跟踪新日志,类似tail -f:
docker logs -f 容器ID只看最后200行:
docker logs --tail 200 容器ID这些日志来自容器内主进程的stdout/stderr,直接跑在后台的程序如果写文件不写stdout,docker logs是看不到的,需要进容器查看日志文件。
查资源占用:
docker stats实时显示每个容器的CPU、内存、网络、磁盘I/O。我遇到过内存泄漏的应用,就是靠docker stats发现容器内存一直涨,才定位到问题的。
5. 实战:用Docker部署一个MySQL 8.0并跑通
光说不练假把式。我挑MySQL 8.0来演示,是因为它是数据类服务的典型代表,涉及端口、数据卷、环境变量、重启策略、验证连接几个关键点,一套流程跑通,Docker的核心套路基本就掌握了一半。
5.1 直接跑MySQL会遇到的坑
第一次有人让我看为什么MySQL容器起不来了,我拉日志一看:
[ERROR] [MY-010457] [Server] --initialize specified but the data directory has files in it.原因很简单,前一次运行没有指定数据目录,容器初始化后数据写在容器内部;第二次启动又用了同一个容器ID,Docker试图在已有数据的目录上重新初始化,直接报错。这是没有数据持久化意识导致的第一个坑。
第二个坑是密码设置。MySQL官方镜像强制要求设置环境变量MYSQL_ROOT_PASSWORD、MYSQL_ALLOW_EMPTY_PASSWORD和MYSQL_RANDOM_ROOT_PASSWORD三者之一,否则初始化失败。具体行为在不同版本里稍有差别,但无论如何,建议始终显式指定MYSQL_ROOT_PASSWORD。
第三个坑是编码。MySQL 8.0默认字符集是utf8mb4,其实已经比老版本好很多。但你用客户端连上去执行一些中文写入操作如果乱码,多半是连接层没指定charset,不是容器配置问题。这一点在写程序连接串时尤其注意。
5.2 一份完整的docker run命令逐段拆解
用下面这条命令部署MySQL 8.0:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourPassword0! \ -v mysql-data:/var/lib/mysql \ --restart=always \ mysql:8.0逐段解释:
- -d:后台运行,不在终端占住屏幕。不加-d会以当前终端绑定方式运行,Ctrl+C就会停掉容器。
- --name mysql8:给容器命名,之后所有操作都可以用mysql8代替容器ID。
- -p 3306:3306:宿主机3306映射容器3306。如果本机3306已被占用,把左边改成3307试试。
- -e MYSQL_ROOT_PASSWORD=YourPassword0!:初始化root用户的密码。密码里有特殊字符时,注意shell转义问题,建议密码里避免用到$和单引号。
- -v mysql-data:/var/lib/mysql:创建命名卷mysql-data并挂载到MySQL的数据目录。这一步保证了容器删了数据还在。
- --restart=always:容器异常退出或宿主机重启时,Docker会自动拉起这个容器。生产环境的服务几乎都会加这个参数。
- mysql:8.0:镜像名和标签(tag)。8.0不是指“8.0最新版”,而是一个浮动的标签,默认指向8.0系列的最新版本。需要精确定位时可以用mysql:8.0.36这种具体版本标签。
跑起来后验证:
docker ps看到mysql8容器状态是Up,就算启动成功。接着用宿主机上的mysql客户端或者任意GUI工具连接:
mysql -h127.0.0.1 -P3306 -uroot -p输入密码后能看到MySQL 8.0的命令行提示符。
5.3 容器启动失败时,先看日志而不是反复重启
我见过不少人,容器起不来就一遍遍docker restart,日志一眼不看。正确做法是:
docker logs mysql8常见错误无非几类:
- 端口占用:Host port 3306 is already in use。放弃映射或者换宿主机端口。
- 数据目录不匹配:挂载了空目录但指定了初始化参数,或者挂载了非空目录导致初始化失败,日志里会指出initialization errors。
- 权限问题:volume目录权限不对,MySQL进程无法写入,日志会提示Permission denied。
日志就是容器的病历,先看日志再看其他,能省下一大半排查时间。
5.4 更进一步:用docker compose管理一套环境
单容器用docker run没问题,但服务多了以后,依赖关系、网络配置、环境变量、启动顺序都变复杂。这时候用Docker Compose,用一个yaml文件描述整套服务。
下面是一个MySQL + Redis的compose文件示例:
version: "3.8" services: mysql: image: mysql:8.0 container_name: mysql8 ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: YourPassword0! volumes: - mysql-data:/var/lib/mysql restart: always redis: image: redis:7 container_name: redis7 ports: - "6379:6379" restart: always volumes: mysql-data:启动:
docker compose up -d停止并删除:
docker compose downCompose的好处是配置可读、可版本管理,同事之间共享一套环境定义文件,比口头传命令靠谱太多。项目里的基础设施,只要是小团队规模,我都是优先用Compose描述。
6. 入门之后必然遇到的几个问题排查清单
Docker本身不算复杂,但出问题的时候如果不懂原理,会感觉无从下手。我把入门阶段最高频的几个问题整理出来,分别说清原因和处理思路。
6.1 docker权限错误:Got permission denied while trying to connect
刚装完Docker,直接执行docker ps,如果遇到:
Got permission denied while trying to connect to the Docker daemon socket说明当前用户没有访问Docker守护进程套接字的权限。原因:Docker的客户端和守护进程通过/var/run/docker.sock通信,这个套接字默认属于root用户和docker组。
两种解决办法:
- 临时用sudo docker ps;
- 把用户加入docker组:
sudo usermod -aG docker $USER newgrp docker重新登录终端后,docker命令就能直接执行。注意,把普通用户加入docker组等同于授予了root级别的权限,因为docker组用户可以通过挂载宿主机目录等方式操作宿主机文件。个人开发机无所谓,共享的服务器上要谨慎管理加入docker组的用户名单。
6.2 Docker Desktop一直starting
Windows环境常见,Linux下如果服务一直启动不了,多半另有原因。
Docker Desktop长期卡在starting,优先排查路径:
- 确认WSL2可用:PowerShell里执行wsl --status,如果显示WSL未安装或内核过期,执行wsl --update。
- 检查Windows下的“虚拟机平台”功能有没有被第三方软件误关。
- 进入Docker Desktop的Troubleshoot选项卡点击Restart。
- 如果一直失败,查看日志:%LOCALAPPDATA%\Docker\log\。
一个很隐蔽的原因:装了旧版本的WSL内核,Windows更新后没有同步更新WSL。在管理员PowerShell执行wsl --shutdown,然后重新启动Docker Desktop,能解决不少问题。
6.3 镜像下载慢或者一直超时
配置了加速器但还慢,先验证加速器是否生效:
docker info看Registry Mirrors字段是否列出了你配置的加速地址。如果没生效,检查daemon.json格式是否正确、路径是否对(Linux是/etc/docker/daemon.json,不要放错位置)、修改后有没有重启Docker。
如果最终还是要直连Docker Hub,可以明显感受到文件分层的下载进度条,不过耐心一点也还能用。说白了镜像加速这件事,不同时段、不同网络环境差异很大,多备几个源没有坏处。另外,一些大型基础镜像(比如node:20、python:3.12)有alpine、slim这类精简变体,体积会小很多,网络条件不好的时候尽量选这类标签。
6.4 磁盘占用越来越大,怎么清理
Docker用久了,磁盘蹭蹭往上涨。常见占用来源是:镜像(含悬空镜像)、停止的容器、构建缓存、日志文件。
先看整体占用:
docker system df会列出IMAGES、CONTAINERS、LOCAL VOLUMES、BUILD CACHE各自的占用情况。
清理未使用的资源:
docker system prune加上-a清理所有未使用的镜像:
docker system prune -a这个命令非常激进,会删除所有没有被运行中容器引用的镜像和缓存,恢复能力很低,执行前确认环境确实不需要这些内容。
容器日志也可能悄悄占满磁盘。Docker默认的日志驱动不会自动轮转,日志文件会一直增长。对日志有要求的服务,需要配置json-file的max-size和max-file,或者接管到外部日志系统。
6.5 技术求助的正确姿势
遇到Docker问题,搜索引擎上能找到大量答案,但直接复制粘贴往往不能解决。正确做法是,把下面四样东西放到一起搜索:
- 你执行的那条命令;
- 完整的报错信息;
- Docker和操作系统的版本;
- 你正在使用的镜像名称。
比如,“docker run mysql 8.0 Host port is already in use”这样的组合,能精准定位到端口冲突问题。Stack Overflow上按时间排序看答案比按赞同数排序更靠谱,因为Docker发展快,老答案很可能已经过时。
Docker官方文档(docs.docker.com)里的reference章节质量很高,每个子命令的参数都列得很清楚。遇到不确定的参数,直接查reference比到处搜博客更准确。
用Docker这几年,我最大的感受是:这东西的上手曲线被很多人高估了。它不要求你具备多深的底层知识,核心概念就那么几个,常用命令不超过二十个。真正需要花时间理解的,不是命令本身,而是容器与宿主机的边界在哪里、数据放在哪里、出了问题到哪里看日志。这几个问题想明白了,Docker对你来说就不再是一个“会跑但不懂”的黑盒。
最后分享一个很多人容易忽略的小技巧:在Dockerfile里,每一层指令都会产生一层中间镜像缓存,调整Dockerfile中指令顺序会极大影响构建速度。把不常变的依赖安装放在前面,把频繁改动的源码COPY放在后面,能充分利用缓存,让每次构建只编译真正变化的层。这条经验在你写完第一个Dockerfile之后立刻就能用上。
如果你照着上面的过程装好了Docker,并且成功跑起来一个MySQL容器,恭喜你,Docker的核心流程你基本已经掌握了。接下来可以拿自己手头的项目试试写Dockerfile,从最简单的Python或Node应用开始,把它容器化跑起来,整个过程会比你想象的快很多。