最近频繁有朋友问我Docker的问题:新电脑装了Docker Desktop起不来,Linux上装好了却总报权限错误,MySQL容器跑了几天数据丢了,项目里要用Compose搭Redis主从却一直连不上……这些问题我基本都踩过一遍。今天干脆把手里的经验整理一下,从安装到部署、再到网络和数据卷的常见坑,用一套能直接照做的流程串起来。文章面向刚接触Docker的开发者,也适合那些已经会用docker run、但遇到问题不知道怎么排查的运维同学。
1. 先搞清楚Docker到底帮你解决了什么问题
1.1 镜像和容器:你只需要记住两个词
很多人学了几天Docker,还是分不清镜像和容器,其实用做饭来类比就特别直白:镜像就是“预制菜包”,里面包含运行某个程序需要的全部东西——代码、运行时、系统库、配置文件,全都打包到一个只读文件里。而容器就是你把这份预制菜下锅炒出来的那盘菜,是一个可以读写、可以运行的实例。同一个镜像可以同时炒出很多盘一模一样的菜,每个容器互相独立,互不干扰。
再往深处说一层:Docker利用的是Linux内核的命名空间和cgroups机制,这也解释了为什么它比虚拟机轻那么多。虚拟机需要在宿主机里再模拟一套完整操作系统,跑起来动辄几个GB的内存占用;而Docker容器直接共享宿主机的内核,启动一个普通容器往往只需要一两秒。你把它理解成给进程做了一层“隔离舱”,而不是给整台机器做“仿真”,就完全不会走偏。
1.2 为什么现在几乎所有团队都在用Docker
早期开发最大的痛点就是“在我电脑上能跑”。Java项目JDK版本不一样可能编译报错,Python项目依赖冲突能折腾一天,更别提Redis、MySQL、Nginx这些中间件的版本兼容问题。Docker的出现把这层混乱直接统一了:Dockerfile把环境固化成了代码,任何人拉下来同一份镜像,跑出来的行为完全一致。
除了环境一致性,Docker在前后端联调、CI/CD流水线、弹性扩容上的价值也很明显。比如你写微服务,本地起一堆服务依赖,机器风扇转得飞起;用Docker Compose直接一条命令全部拉起,资源占用可控,关掉也特别干净。对于运维,一个镜像可以同时跑在不同规格的宿主机上,不再需要为每台机器单独配置环境。换句话说,Docker已经不只是“部署工具”,而是现代软件交付的基本单元。
2. 环境准备:不同系统安装Docker的完整流程
2.1 Linux装Docker:Ubuntu和CentOS两条命令路线
Linux下安装Docker最常见的是两条路径:使用发行版自带的软件源,或者用官方提供的安装脚本。我这里更推荐前一种,可控性更强,也方便定制版本。
Ubuntu/Debian系列,先更新索引再安装:
sudo apt update sudo apt install docker.io -y sudo systemctl enable --now dockerCentOS/RHEL系列,先配置Docker官方仓库:
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 enable --now docker装完之后用docker version验证一下,能看到Client和Server两段信息就说明服务正常。如果执行docker version只显示Client、没有Server,大概率是docker服务没起来,用systemctl status docker查一下原因。在较老的CentOS 7内核上偶尔会遇到overlayfs兼容性问题,升级内核或者把存储驱动切到vfs能暂时解决,但我的建议是生产环境尽量用较新的内核版本。
2.2 Docker Desktop在Windows/macOS上的安装与常见失败
Windows用户装Docker Desktop之前,建议先把WSL2准备好。Docker Desktop在Windows下默认使用WSL2做后端,没有启用WSL2的话,安装过程即便顺利,启动时也会遇到各式各样的问题。
遇到最常见的错误提示Docker Desktop failed to start because virtualisation support wasn't detected,基本能定位到以下几个原因:
- BIOS里没开启虚拟化。开机进BIOS,找到“Intel VT-x”或“AMD SVM”这一项,把它打开。
- Hyper-V没有启用。在Windows功能里勾选“Hyper-V”和“虚拟机平台”,重启后再试。
- WSL2没有真正安装。以管理员身份运行
wsl --update,然后wsl --set-default-version 2。 - 电脑上装了第三方安全软件,拦截了Docker的管理员服务。这类情况可以暂时把安全软件退出来再启动。
注意,即使能启动Docker Desktop,如果某个镜像运行异常,也要先检查Windows的虚拟化是否被占用(比如同机器在跑其他Android模拟器或者VMware),这类软件通常会抢占虚拟化能力,导致Docker Desktop起不来。
2.3 拉不动镜像?给Docker配一个可用的镜像加速源
国内云服务器或家庭宽带拉取Docker官方镜像经常超时,镜像加速基本是唯一实用的解法。Linux上修改/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.net" ] }然后重启Docker:sudo systemctl restart docker。Windows的Docker Desktop在Settings-Docker Engine里直接改JSON,保存后会自动重启引擎。
换加速源之后,可以用docker pull mysql:8.0测试速度。如果还是慢,建议直接搜索那些当前可用的公共加速器,或者自行搭建镜像缓存服务,这个我后面讲私有仓库时还会提到。
3. 日常用得最多的核心操作:镜像、容器、数据卷、日志
3.1 镜像拉取、容器生命周期和常用参数解析
平时操作Docker,最绕不开的就是docker run。我用一个MySQL容器来拆解常用参数,示例命令:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v mysql_data:/var/lib/mysql \ mysql:8.0-d表示后台运行,不加这个参数容器会直接在前台占用终端。--name给容器起名字,后续管理、查看都会方便很多,一般都要带上。-p 3306:3306把宿主机的3306端口映射到容器的3306端口。宿主机端口冲突时,可以把左边改成一个不冲突的端口,比如-p 3307:3306。-e用来传入环境变量,镜像内程序会读取这些变量做初始化,比如MySQL会用它设置root密码。-v mysql_data:/var/lib/mysql是数据卷挂载,把容器内的数据目录映射到一个由Docker管理的卷中,防止容器删除后数据丢失。
容器生命周期管理我用这套命令就够了:
docker ps -a # 查看所有容器 docker start mysql8 # 启动已存在的容器 docker stop mysql8 # 停止容器 docker rm mysql8 # 删除容器 docker images # 查看本机镜像列表 docker rmi mysql:8.0 # 删除镜像删除容器前想保留数据的话,只要不删数据卷即可;如果连着数据卷一起删,容器里的数据就真没了。养成习惯:生产环境不要用docker rm -v去清理一个不确定是否还有用的容器。
容器生命周期里还有一个隐藏考点:ENTRYPOINT和CMD的区别。简单说,ENTRYPOINT是镜像的“主程序入口”,CMD只是给这个入口传的默认参数。比如docker run ubuntu echo hello会执行echo hello,而如果镜像里定义了ENTRYPOINT ["nginx", "-g"],你追加的命令行参数会拼到nginx后面。想强制覆盖ENTRYPOINT时可以在命令行加--entrypoint参数。
3.2 进入容器、执行命令、查看日志的基本用法
调试时最常见的需求是进到容器内部看看进程、配置文件或者网络状态。命令很简单:
docker exec -it mysql8 bash-it的意思是把当前终端绑定到容器里的bash进程,这样就能像在一台瘦身Linux里一样敲命令。如果容器里连bash都没有(很多精简镜像只有sh),就把bash换成sh。
查看日志是排查问题的第一抓手:
docker logs -f mysql8-f是持续跟踪输出,跟tail -f一样。日志默认只显示最近一段历史,想从头看可以加--tail 100或者直接在日志文件路径找。容器与宿主机之间拷贝文件的命令是:
docker cp 宿主机文件 容器ID:/容器内路径 docker cp 容器ID:/容器内文件 宿主机路径我之前调试一个Nginx镜像时,就是靠这个命令把宿主机里的配置直接拷进容器,省掉了重新构建镜像的过程。
3.3 权限错误:docker: permission denied的两步修复
刚装完Docker的Linux用户执行docker ps大概率会看到permission denied。原因很简单:docker socket文件的权限默认只给root用户和docker用户组,普通用户没权限访问。
解决办法分两步:
sudo usermod -aG docker $USER newgrp docker执行完newgrp docker可以让当前终端立即加入用户组,不用退出重新登录。如果已经远程ssh登录,也可以直接断开重连。注意,这只适用于可信的本机用户,不建议在共享服务器上给所有人都加docker组,因为加入docker组基本等同拥有root权限,可以通过挂载宿主机目录来提权。
4. 高频实战:MySQL、Redis主从、GitLab,再到微服务部署
4.1 用Docker安装MySQL 8.0并配置持久化与字符集
MySQL是Docker使用频率最高的软件之一。直接docker run跑一个MySQL其实很容易,但很多人后续遇到两个坑:一是容器删了数据全没了,二是有中文写入报错。这里给出一个生产可用的启动方案:
docker run -d \ --name mysql8 \ --restart=always \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=MyStrongPwd \ -e TZ=Asia/Shanghai \ -v /data/mysql/conf:/etc/mysql/conf.d \ -v /data/mysql/data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci有几个细节值得说清楚。--restart=always让Docker守护进程在容器异常退出时自动拉起它,服务器重启后MySQL也会跟着起,省心很多。字符集方面,MySQL 8.0默认字符集已经不是latin1,但显式指定utf8mb4能保证表情符号和生僻字不出乱码。宿主机上的/data/mysql/conf会自动创建,想要自定义my.cnf,直接往这个目录丢一个配置文件再重启容器即可。
如果启动失败,常见原因有三个:3306端口被宿主机程序占用,用ss -lntp检查一下;容器内MySQL初始化日志没看完就误判失败,用docker logs mysql8看真正的报错;服务器内存低于2GB,MySQL 8.0初始化时会比较吃力,这种情况可以把--restart=always暂时去掉,先查日志解决资源问题。
4.2 Docker Compose一键拉起Redis主从架构
实际项目中,一个服务往往依赖多个中间件。每次都手动写一长串docker run太痛苦,用Docker Compose把架构描述成YAML文件才是长期方案。以Redis主从为例,我常用的编排如下(docker-compose.yml):
version: "3.8" services: redis-master: image: redis:7 container_name: redis-master command: ["redis-server", "--appendonly", "yes"] ports: - "6379:6379" volumes: - master-data:/data redis-slave: image: redis:7 container_name: redis-slave command: ["redis-server", "--slaveof", "redis-master", "6379"] depends_on: - redis-master ports: - "6380:6379" volumes: - slave-data:/data volumes: master-data: slave-data:在文件所在目录执行docker compose up -d,两个容器就会一起生成并启动。这里重点讲一个很多人忽略的问题:容器间通信。
主从配置里我写的是--slaveof redis-master 6379,而不是--slaveof 127.0.0.1 6379。原因是在Compose默认的bridge网络里,每个服务名本身就是一个可解析的DNS名,redis-master会被解析成该容器在Docker内部网络里的IP。如果你在容器内部用127.0.0.1,那指向的是这个slave容器自己,自然无法连到主节点。
验证主从状态的办法是进入容器执行Redis命令:
docker exec -it redis-slave redis-cli info replication看到role:slave和master_host:redis-master基本就正常了。从节点要连不上,优先检查主节点的bind配置是否限制了内网访问,以及Compose里是否遗漏了自定义网络。
4.3 GitLab社区版容器部署与资源控制
团队内部搭Git仓库,很多人图省事直接GitLab官方包安装,结果依赖环境一堆问题。用Docker部署GitLab社区版其实更清爽,这里给一个最小可用配置:
docker run -d \ --name gitlab \ --restart=always \ -p 80:80 \ -p 443:443 \ -p 2222:22 \ -v /data/gitlab/etc:/etc/gitlab \ -v /data/gitlab/log:/var/log/gitlab \ -v /data/gitlab/data:/var/opt/gitlab \ gitlab/gitlab-ce:latest启动后GitLab初始化要等上一阵子,可以用docker logs -f gitlab观察,看到 “GitLab is ready” 提示再去浏览器访问。注意-p 2222:22是把宿主机的2222映射到容器的22端口,因为宿主机22常常被SSH占用,GitLab里克隆仓库时就要写ssh://git@宿主机IP:2222/用户/项目.git。如果你不想处理这个端口映射,可以直接-p 22:22,前提是宿主机22没被占用。
GitLab镜像比较大,运行也吃内存,官方建议至少4GB可用内存。如果服务器内存有限,可以在环境变量里禁用Prometheus和Grafana来省资源:
-e prometheus_enabled=false -e grafana_enabled=false我第一次在2GB内存的小机器上跑GitLab,结果系统卡死,之后加了这个参数才勉强流畅运行。
4.4 IDEA一键打包Docker镜像并部署微服务
微服务开发中,用IDEA的Docker插件来打包镜像非常顺手。项目根目录写一个Dockerfile:
FROM openjdk:17-jdk-slim LABEL maintainer="yourname" WORKDIR /app COPY target/xxx.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]然后在IDEA的Run Configuration里添加Dockerfile运行方式,指定Dockerfile路径和镜像名称(比如registry.example.com/demo/xxx-service:1.0.0),点击运行就会自动完成docker build。注意首次使用需要让IDEA能连接Docker:在Settings-Build Tools-Docker里配置连接本机Docker Desktop或者在远程服务器上暴露的tcp端口。
构建完成之后推送镜像:
docker push registry.example.com/demo/xxx-service:1.0.0生产环境拉取该镜像并运行:
docker run -d --name xxx-service -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=prod \ --network your_app_network \ registry.example.com/demo/xxx-service:1.0.0如果微服务之间有服务发现需求,记得把它们放到同一个自定义bridge网络里,否则服务名无法互相解析。还有一点:Docker构建镜像时,COPY target/xxx.jar这一步要求先执行过Maven打包,否则会直接报文件不存在。我经常犯这个错,于是习惯于在构建之前先跑一次mvn clean package或让IDEA执行前置构建步骤。
5. 网络与数据存储:最容易翻车的两个环节
5.1 Docker网络模型与网络不通排查思路
Docker最常见的网络不解问题,基本绕不开几种模型。默认bridge网络是容器与宿主机之间有NAT,容器有独立IP,宿主机端口映射才能从外部访问。host网络模式下容器直接使用宿主机的网络栈,端口不需要映射,性能好,但隔离性差。none模式适合完全不依赖网络的容器,一般不常用。自定义bridge网络则支持容器间通过服务名通信。
排查网络不通时,我有一套固定顺序:
- 先看两个容器是否在同一个自定义网络里:
docker network inspect 网络名。 - 从容器A里ping容器B的服务名,确认DNS解析是否正常。
- 检查目标容器的端口是否真的在监听:
docker exec 容器B ss -lntp。 - 检查服务是否绑定到了非loopback地址。很多中间件默认只监听127.0.0.1,在容器里这样绑定的话外部容器就无法访问,需要修改配置让它监听0.0.0.0。
另外,宿主机外部要访问容器服务必须做端口映射,这一步忘了是新手最容易犯的错误:-p 宿主机端口:容器端口。
5.2 数据卷:三种挂载方式的选型与避坑
持久化是容器化应用的高频问题。Docker里数据持久化主要有三种方式:
- bind mount:把宿主机某个目录挂给容器。如
-v /data/mysql:/var/lib/mysql,好处是数据文件直接可见,备份方便,适合数据库、配置等场景。 - 命名卷:
-v myvolume:/var/lib/mysql,数据存放在/var/lib/docker/volumes/myvolume下,Docker管理,适合日志、临时文件等。 - tmpfs:数据只存在内存中,适合跑临时任务,容器停止后数据就没了。
我自己的选型原则是:数据库和有状态服务用bind mount,日志和缓存用命名卷,临时计算任务直接用tmpfs。bind mount时要特别注意权限问题,容器内外uid不一致可能导致容器没权限读写挂载目录,这时要么改宿主机目录的属主,要么在run命令里加-u指定用户。
5.3 私有镜像仓库:一条命令搭建自己的Registry
镜像下载慢、团队内部分发镜像不方便,一个轻量级私有仓库能解决很多问题。Docker官方提供了registry镜像,部署方式极简:
docker run -d --name registry \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ --restart=always \ registry:2推送镜像时,先把镜像打上仓库地址标签:
docker tag myapp:latest 宿主机IP:5000/myapp:latest docker push 宿主机IP:5000/myapp:latest拉取时也要带上仓库地址:
docker pull 宿主机IP:5000/myapp:latest注意,默认的registry是明文HTTP传输。如果远程服务器要访问私有仓库,需要在每台客户端的 daemon.json 里把该地址加进insecure-registries,否则Docker会拒绝推送和拉取。想上HTTPS就得挂证书和Nginx做反向代理,这个按需配置即可。
6. 进阶场景:青龙依赖管理、GPU容器与其他热门用法
6.1 容器化部署青龙面板时的依赖安装方法
青龙面板这类定时任务管理工具,容器化部署后经常出现“脚本依赖安装失败”的反馈。依赖问题是容器环境与宿主机环境隔离导致的,不能指望宿主机的Node.js和Python装上之后容器内也有。标准做法是进入容器后安装:
docker exec -it qinglong bash cd /ql/scripts npm install pnpm -g pip install requests或者在宿主机上用docker exec直接调容器内的Shell:
docker exec -it qinglong bash -c "cd /ql/scripts && npm install"更推荐的方式是把它固化进自定义镜像:以青龙官方镜像为基础,写Dockerfile里提前安装好项目需要的Node模块和Python包,这样每次创建容器都自带依赖,不需要反复手工安装。
说到青龙,切记不要把它用于违反服务条款的自动化脚本,尤其是涉及灰产的场景。我这里只讲技术层面的依赖管理思路。
6.2 用GPU跑容器:NVIDIA Container Toolkit配置与验证
容器里跑大模型、深度学习推理已经非常普遍。默认情况下Docker容器无法读取宿主机GPU,需要NVIDIA Container Toolkit来打通这层。Ubuntu上的安装步骤:
sudo apt install nvidia-container-toolkit sudo systemctl restart docker运行容器时增加GPU参数:
docker run --rm --gpus all \ -v /models:/models \ vllm/vllm-openai:v0.27.1 \ --model /models/qwen3-embedding-0.6b如果--gpus all报权限或找不到设备,用docker run --rm -it --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi验证一下容器内能否看到GPU信息。看到显卡型号和显存就说明NVIDIA软链是通的。在宿主机单独执行nvidia-smi正常不代表容器内正常,这个区别很多人没搞清楚。
6.3 从DVWA靶场到Hadoop:Docker在学习和实验环境中的价值
Docker轻量、可快速销毁的特性,天然适合搭学习实验环境。比如做安全测试学习时,用Docker拉起DVWA靶场:
docker run -d -p 8080:80 vulnerables/web-dvwa访问宿主机8080端口就能开始练习,测完直接docker rm销毁,不留残留。同样地,Hadoop这类复杂的分布式系统,官方或社区维护的镜像能让你在单机上快速起一个多容器集群,学习核心原理的时候非常方便。ROS2等机器人方向的环境配置一直比较繁琐,但容器里跑ROS2 humble和micro-ROS agent正在成为新趋势,让环境复现和设备验证变得简洁很多。
这类场景让我愿意向大家推荐Docker为“学习利器”,因为它能让你放得开手去试错,试错了删除容器重来,成本几乎为零。
7. 常见问题排查速查表
把这几类问得最多的问题整理成一张速查表,遇到类似情况直接对照:
| 问题现象 | 最可能的根因 | 快速解决办法 |
|---|---|---|
| Docker Desktop启动失败,提示virtualisation support not detected | BIOS虚拟化没开,或Hyper-V/WSL2未启用 | 开BIOS的VT-x/SVM,启用Windows可选功能,更新WSL2 |
docker ps提示permission denied | 当前用户不在docker用户组 | sudo usermod -aG docker $USER后重新登录 |
| 拉取镜像超时或极慢 | 未配置镜像加速源 | 修改daemon.json配置registry-mirrors并重启Docker |
| 容器删除后MySQL数据丢失 | 未挂载数据卷 | 启动时加-v 宿主机目录:/var/lib/mysql |
| Redis主从连不上 | 从节点配置了127.0.0.1 | 使用主节点容器名作为可解析地址 |
| 容器外部访问不到服务 | 未映射宿主机端口 | 加-p 宿主机端口:容器端口 |
| 容器之间ping不通 | 不在同一自定义网络 | docker network create并在run时指定--network |
| gitlab容器内存占用过高 | 默认监控组件过多 | 添加环境变量关闭prometheus和grafana |
| 容器内读不了挂载目录 | uid权限不一致 | 修改宿主机目录权限,或容器启动时指定-u |
排查问题的通用心法就三条:先看日志、再验网络、最后查权限。道理很简单,但照着这个顺序执行,能省下大量盲目试错的成本。
一位前辈跟我说过一句话我至今记得:Docker学得好不好,不看你记得多少命令,而是看你能不能准确地描述“哪个进程在哪个隔离环境里,监听了哪个端口,数据写到了哪里”。把这条主线想透,绝大部分Docker疑问都能自己找到答案。我自己操作中也确实如此——最初的坑都是因为没搞清网络命名空间和数据卷的边界在哪里。多拿几个容器练手,拆掉重建几次,顺手之后你也会觉得它就是一套逻辑严谨的“装鸡蛋的盒子”,只是一旦真正理解了它的边界,你往里面装什么都心里有数。