1. 安装Docker和Docker Desktop:大多数人卡住的地方,其实在启动之前
我最早接触Docker,是在一个需要同时跑MySQL、Redis、Nginx和两个Java服务的老项目上。当时机器环境乱得离谱,Redis版本不兼容、MySQL权限混乱、Nginx配置被改得面目全非,每次处理环境问题都要花掉半天时间。后来我直接在机器上装了Docker Desktop,把所有中间件全部容器化,从此环境问题基本在一分钟内解决。这也是我为什么一直建议身边的朋友:只要你是做开发的,无论后端、前端还是运维,Docker都值得尽早用起来。
不过,很多人在安装这一步就栽了跟头。尤其是Windows用户,最常见的就是启动Docker Desktop时报错,提示virtualization support not detected,或者是docker desktop failed to start because virtualization support is not enabled。这个报错看起来吓人,其实原因非常集中——Docker Desktop依赖虚拟化技术来跑Linux容器,你的机器没开虚拟化,或者没装好相应的Windows功能。
1.1 Windows下必须先确认的三件事
先说结论:在Windows上安装Docker Desktop,本质上不是在"安装一个软件",而是在给Linux容器运行准备好完整的虚拟化环境。Docker Desktop底层借助WSL2或者Hyper-V来运行Linux内核,这三样东西缺一不可。
第一件事,确认BIOS里的虚拟化开关。
打开任务管理器,切到"性能"标签,看CPU那一栏右下角有没有"虚拟化:已启用"。如果是"已禁用",就需要重启电脑,进BIOS设置。不同的主板叫法不一样,Intel平台一般叫Intel Virtualization Technology,也叫VT-x,AMD平台叫SVM Mode,有的华为、联想、戴尔笔记本甚至会在默认状态下关闭。
提示:进BIOS的方法在台式机和笔记本上略有不同,开机时反复按F2、Del或F10都能试。找不到选项的话,直接搜自己主板型号加"开启虚拟化"。
开启之后,重启进系统,再确认一下任务管理器里的虚拟化状态确实变成了"已启用",这一步经常有人漏掉。
第二件事,启用Windows功能。
在Windows搜索框里输入"启用或关闭Windows功能",然后勾选这两项:
- 适用于Linux的Windows子系统(也就是WSL2)
- 虚拟机平台(Virtual Machine Platform)
勾完之后,系统会提示你重启。注意,这一步不是可选项。虽然Docker Desktop也支持用Hyper-V来跑,但WSL2模式在启动速度和内存占用上明显更好,而且已经是官方默认推荐的方式。我见过很多人只勾了WSL没勾虚拟机平台,启动时依然报错,所以两项都要勾。
第三件事,更新WSL内核。
这一步很容易被忽略。Docker Desktop装好之后,如果启动时始终卡在Engine starting,十有八九是WSL内核太旧。在PowerShell里执行:
wsl --update更新完之后再重启Docker Desktop。如果系统里从来没用过WSL,可以先执行:
wsl --status看看输出里有没有报错,比如没有已安装的分发版。有报错的话,执行:
wsl --install装一个默认的Ubuntu发行版,再回来启动Docker Desktop。
1.2 启动卡住、界面无反应时的排查思路
我遇到过一个比较典型的场景:一位同事装Docker Desktop,双击图标之后,界面长时间停留在Docker Engine starting,左下角一直转圈。按照网上的说法,他重新装了几遍都没用。后来我让他看了一下Docker Desktop的设置页面,发现"Use WSL 2 based engine"处于开启状态,但WSL里根本没有可用的发行版,等于引擎的底层不存在。这种时候启动当然会失败。
排查顺序很简单:
- 先确认WSL状态:
wsl --list --verbose,看看有没有正在运行的发行版。 - 没有的话,去Microsoft Store装一个Ubuntu,然后重新启动Docker Desktop。
- 如果WSL状态正常但启动依然卡住,打开任务管理器看内存占用,Docker Desktop初始化时比较吃内存,Memory不足也会一直卡在Starting状态。
另外,某些第三方安全软件会拦截Docker Desktop创建虚拟网卡,表现也是启动失败或启动后镜像拉不下来。这个没有太好的办法,只能尝试临时退出安全软件再启动。我自己用的做法是给Docker Desktop的核心进程加白名单。
1.3 不想用Desktop的话,命令行安装方式也很稳
如果你用的是纯Linux服务器,或者本机配置比较有限,Desktop并不是唯一选择。Linux下的安装方式其实更干净:
curl -fsSL https://get.docker.com | bash这条命令会自动检测系统版本,配置Docker官方源并安装。安装完成后:
sudo systemctl enable --now docker国内网络环境下,如果这一步拉镜像特别慢,需要配置镜像加速器。编辑/etc/docker/daemon.json,写入:
{ "registry-mirrors": ["https://docker.m.daocloud.io"] }然后重启Docker:
sudo systemctl restart dockermacOS用户如果不想装Desktop,也可以直接用Homebrew:
brew install docker brew install colima colima startColima是一个轻量级的Docker运行时,口碑一直不错。它的用法和Desktop基本一致,命令行完全兼容。
2. 跑通第一个容器之前,得先把这几个概念理顺
装好Docker之后,很多人的第一个动作就是去看教程,复制粘贴跑一个nginx容器。跑完发现能访问,然后就没然后了。过了几天,想跑MySQL的时候,照着网上的命令敲,结果各种报错。问题出在哪里?出在最基础的那几个概念没理顺——镜像和容器是什么关系,端口映射是怎么工作的,数据卷为什么必须挂。
2.1 镜像是模板,容器是运行实例
把镜像类比成ISO安装盘,容器就是装完系统跑起来的机器。你可以从同一个ISO安装出很多台机器,每台机器互不影响;同样,你可以从一个镜像启动很多个容器,它们之间彼此隔离。但和虚拟机不一样的是,容器的开销小得多,因为它不是完整模拟硬件,而是直接复用宿主机的内核。
实际操作中,你会发现一条命令就够了:
docker run -d --name my-nginx -p 8080:80 nginx这条命令的拆解是这样的:
docker run:从镜像创建并启动容器。-d:后台运行,不占用当前终端。--name my-nginx:给容器起个名字,之后操作都用这个名字。-p 8080:80:把宿主机的8080端口映射到容器内的80端口。nginx:要使用的镜像名。
跑起来之后,浏览器访问http://localhost:8080,就能看到nginx的欢迎页。如果没看到,先别急着怀疑端口映射,90%的情况是容器压根没启动成功。执行docker ps -a看看状态,再执行docker logs my-nginx看日志。
2.2 端口映射的本质:宿主机端口和容器端口是两回事
很多新手在-p参数上犯迷糊,觉得既然容器有端口,为什么还要再映射一次。这里要明白一个关键点:容器默认运行在一个独立的网络隔离环境里,宿主机访问不到容器的IP。所以你必须告诉Docker:帮我开一个宿主机端口,把流量转发到容器的某个端口上。
格式是-p 宿主机端口:容器端口。左边的端口如果被占用,启动时Docker会直接报错,提示端口已经被绑定。我经常看到有人问"我的3306端口被本机MySQL占用了怎么办",解决办法很简单,左边换一个端口就行,比如-p 3307:3306。容器内部依然是3306,外部通过宿主机的3307访问,两边互不干扰。
2.3 数据卷:不挂载的话,删容器等于丢数据
这是Docker使用里最惨痛的教训,我身边至少三个人踩过。做个演示你就明白了:
docker run -d --name test-redis -p 6379:6379 redis docker exec -it test-redis redis-cli set name "hello" docker rm -f test-redis再重新docker run一个redis,你会发现name这个键根本不存在。因为默认情况下,容器内部的文件系统是临时的,容器删除后,里面所有数据一并消失。对于数据库类的容器,这绝对是灾难。
解决办法是挂载数据卷。常见做法是把宿主机目录映射到容器内目录:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=my-secret-pw \ -v /data/mysql:/var/lib/mysql \ mysql:8.0这样MySQL的数据文件会落到宿主机的/data/mysql目录里,以后无论容器怎么重建,数据都还在。挂载目录这个动作,在Docker里叫"数据卷",核心思想就是:容器本身可丢弃,但数据要留在宿主机上。我个人的习惯是,凡是跑数据库、缓存、队列这类有状态的服务,一律先把数据卷挂好再启动。
2.4 进容器、看日志、拷文件的基础操作
日常维护中,这三条命令的使用频率最高:
# 进入容器的交互式shell docker exec -it my-nginx bash # 持续查看容器的实时日志 docker logs -f my-nginx # 从宿主机拷贝文件进容器 docker cp /local/file.txt my-nginx:/tmp/file.txtexec这条命令很重要,排查容器内部的问题时,几乎每次都靠它。比如容器能启动但服务报错,你可以直接进去看配置文件、手动执行命令。需要注意的是,有些容器基于Alpine系统,里面没有bash,只有sh,那你就用docker exec -it my-container sh。
3. 两个实战级例子:MySQL 8.0和Redis主从的容器化搭建
理论说再多,不如亲手跑一遍。这一节我用两个最常见的使用场景——MySQL 8.0和Redis主从——来做实战演示。你如果在搜索引擎上搜过"docker安装mysql8.0并使用"和"docker安装redis主从",那你正在找的可能就是下边这些内容。
3.1 用Docker跑起MySQL 8.0,并顺利完成初始化
先拉镜像:
docker pull mysql:8.0然后启动:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=你的密码 \ -e MYSQL_DATABASE=app_db \ -e MYSQL_USER=app \ -e MYSQL_PASSWORD=app密码 \ -v /data/mysql:/var/lib/mysql \ -v /data/mysql-conf:/etc/mysql/conf.d \ mysql:8.0这里有两个容易翻车的点。
第一个点:字符集。默认情况下,MySQL 8.0拉起来后,字符集可能还是latin1,存中文容易乱码。建议在启动前先建好自定义配置文件。在宿主机的/data/mysql-conf目录下新建my.cnf,写入:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci [client] default-character-set=utf8mb4然后重启容器:
docker exec mysql8 mysql -uroot -p你的密码 -e "SHOW VARIABLES LIKE 'character%';"看到character_set_server的值是utf8mb4,说明配置生效了。
第二个点:端口冲突。你本机如果已经装了MySQL,3306端口就被占用了,docker run会直接报错。这时候左边换一个宿主机端口就行,比如-p 3307:3306。客户端连接时连3307,完全不受影响。
排查容器内MySQL状态的命令:
docker exec -it mysql8 mysql -uroot -p进去之后正常执行SQL。如果容器启动后两三秒就退出,基本是配置文件写错或者挂载目录权限不对,看docker logs mysql8的输出,问题基本一目了然。
3.2 搭建Redis主从:两行命令搞定,但容器间通信要先想清楚
Redis主从的用处不用多说,读写分离和高可用都靠它。用Docker搭建比手工装两个实例方便太多了。
先建一个自定义网络,让两个容器可以通过名字互相访问:
docker network create redis-net启动主节点:
docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7 redis-server --appendonly yes启动从节点,注意用--replicaof指定主节点的地址。在同一个自定义网络里,容器名redis-master就是它的主机名,Docker内置的DNS会自动解析:
docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7 redis-server --replicaof redis-master 6379测试验证一下:
docker exec -it redis-master redis-cli set foo bar docker exec -it redis-slave redis-cli get foo从节点返回bar,说明主从同步正常。
这里要提一个非常容易踩的坑:如果你不自定义网络,而是分别在默认bridge网络上跑主从,然后从节点启动时填了主节点的IP,看起来也能通,但一旦容器重启,IP变了,从节点就再也连不上主节点了。而用自定义网络之后,容器名解析是固定的,重建容器只要同名,从节点就能重新找到主节点。所以我建议,凡是容器间需要通信的场景,一律先docker network create建一个网络,再用--network启动容器。
4. Docker网络不通?按这四层链路排查,基本都能定位
Docker相关的问题里,网络问题大概占了四成。症状五花八门:容器之间ping不通、宿主机访问不了容器端口、容器能启动但访问超时。很多人遇到这种问题就慌了,开始盲目重启Docker和容器,往往浪费大量时间。其实Docker的网络链路是层层叠加的,只要按顺序排查,大多数问题都能快速定位。
4.1 第一层:容器内的服务到底有没有在监听
先问自己一个问题:你访问不通的那个端口,容器内部真的有服务在监听吗?
我见过很多次"镜像拉下来了,容器也起来了,但访问不了"的求助。进去一看,容器里的进程压根没起来,或者起来以后崩了。所以排查第一步不是查网络,而是查容器状态:
docker ps -a docker logs 容器名如果容器处于Exited状态,看日志找崩溃原因。如果还在运行,可以进去手动验证:
docker exec -it 容器名 bash curl localhost:端口容器里自己访问自己通,宿主机访问不通,才进入第二层。容器里都不通,那问题出在服务本身的配置上,比如绑定地址是127.0.0.1还是0.0.0.0。不少应用默认只监听127.0.0.1,这样即使端口映射配了,外部也访问不到。配置文件里改成0.0.0.0即可。
4.2 第二层:端口映射是否正确
容器内部通、宿主机外部不通,优先检查映射关系。
docker port 容器名这个命令会打印当前容器的端口映射列表。看到类似3306/tcp -> 0.0.0.0:3306,说明映射是存在的。如果映射正常但宿主机访问还是不通,再看看是否有多个容器戴了帽子,比如两个容器都映射了宿主机的3306端口,后启动的那个其实没能绑定成功,这也会导致访问异常。
还有一个隐蔽问题:Docker Desktop在Windows/macOS上,端口映射和宿主机的回环地址绑定是比较特殊的。访问时应该用localhost而不是局域网IP,如果非要用局域网IP访问,需要在Docker Desktop设置里开启"Allow user defined networks"之类的相关选项,不同版本名字略有差异。
4.3 第三层:容器之间的互通靠网络,别用宿主机IP
前面提到过,容器和容器之间通信,最好在同一自定义网络里用容器名访问。如果容器A访问容器B的宿主机映射端口,有可能会通,但这种依赖宿主机的转发链路很不稳定。更常见的问题是:容器A ping 不通 容器B的IP,但服务器上的防火墙没有拦,安全组也放行了,最后还是不通。
这种时候要检查两个容器是不是在同一个网络里:
docker inspect 容器A | grep NetworkMode docker inspect 容器B | grep NetworkMode两边都是同一个自定义网络的话,才能用容器名互访。如果一个是默认bridge、一个是自定义网络,它们之间天然隔离开,必须通过IP或端口映射才能通。
注意:
docker inspect的输出很长,建议用docker inspect --format='{{.HostConfig.NetworkMode}}' 容器名只提取网络模式,看起来更清楚。
4.4 第四层:防火墙、云安全组和Docker的"自作主张"
这一层的坑最隐蔽。现象是:在宿主机本机用curl localhost:8080能通,但用另一台机器访问服务器IP的8080就不通。一般有两个原因。
第一个是云服务器的安全组策略没有放行该端口,去云控制台看安全组规则,添加入站规则。
第二个是宿主机防火墙没放行Docker的端口:
sudo firewall-cmd --add-port=8080/tcp --permanent sudo firewall-cmd --reload有些发行版还需要额外注意iptables的问题。Docker在启动时会修改宿主机的iptables规则,如果你手动改过防火墙或iptables,可能导致Docker的转发规则被清掉,表现就是容器之间或者外部的流量进不来、出不去。遇到这种"说不清原因"的网络问题,我试过最好的方法是重启Docker守护进程:
sudo systemctl restart docker这会重建Docker维护的网络规则,不少临时性的网络异常能够直接恢复。
5. docker compose:把一堆命令变成一笔可复用的配置文件
如果你只用docker run管理两三个中间件,倒还好。一旦服务数量多起来,比如MySQL、Redis、Nginx、后端、前端一起跑,每次重启都要敲一长串命令,而且容易记混参数。这就是docker compose存在的意义——把容器的启动配置写成YAML文件,一条docker compose up全部搞定。
5.1 为什么Compose值得专门学一下
Docker Compose做的事情,本质上就是把docker run的参数结构化,写进一个docker-compose.yml文件里。好处有三点:
- 所有配置都可见、可维护、可提交到Git仓库。
- 新同事接手项目时,不用再翻文档猜端口,看配置文件就一目了然。
- 一组服务可以用一条命令统一启动、统一停止、统一查看状态。
举一个实实在在的例子:一个普通的Web项目,需要MySQL和Redis做支撑。文件内容大致长这样:
version: "3.8" services: mysql8: image: mysql:8.0 container_name: app-mysql restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: app_db volumes: - /data/mysql:/var/lib/mysql - ./mysql-conf:/etc/mysql/conf.d redis: image: redis:7 container_name: app-redis restart: always ports: - "6379:6379" command: redis-server --appendonly yes volumes: - /data/redis:/data然后在同目录下执行:
docker compose up -d两条命令启动两个服务,比写两条docker run加维护各自参数的体验好太多了。之后查看状态:
docker compose ps看日志:
docker compose logs -f进入某个服务:
docker compose exec mysql8 bash停止所有服务:
docker compose down注意,down会删除容器,但不会删除数据卷。如果你连数据卷一起清理,需要加-v参数。生产环境中慎用down -v,这命令会连库带文件一起清空。
5.2 depends_on的坑:它只管启动顺序,不管服务是否就绪
新手配置Compose时,经常听到depends_on这个词,以为用上它就能保证服务依赖可靠。实际上depends_on只保证容器启动的先后顺序,不保证依赖服务"已经可以接受请求"。
举个例子,你定义一个后端服务依赖MySQL:
services: backend: image: my_backend depends_on: - mysql8系统会先启动mysql8,再启动backend,但不会等MySQL初始化完成。MySQL初始化通常需要几秒到几十秒,如果后端启动时立即去连数据库,就会报连接失败。
解决办法通常是在应用层做重试,或者在启动脚本里等待端口可用。很多项目里,后端代码自带数据库连接重试逻辑,所以这种情况不算致命,但你要心里清楚:depends_on迁移的是启动顺序,而不是就绪状态。
5.3 把Compose文件拆成多个环境的技巧
实际项目里,开发环境和生产环境的配置往往不同。比较常见的做法是用.env文件保存差异变量,然后在Compose文件里引用:
services: mysql8: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}在项目根目录放一个.env文件:
MYSQL_ROOT_PASSWORD=dev_password_123这样同一个Compose文件,开发环境用自己的.env,生产环境用另外一份。注意.env文件不应该提交到Git仓库,应该加入.gitignore,数据库密码这种敏感信息留在仓库里是隐患。
6. 日常使用中真正能提升效率的若干细节
到了这个阶段,基础的镜像、容器、网络、Compose你都能用起来了,接下来要聊的是我在日常使用中积累的一些"润物细无声"的细节。每一个单看都不是惊天动地的大功能,但组合起来,能让你的Docker使用体验从及格变成舒服。
6.1 定期清理垃圾,别等磁盘满了再折腾
容器跑久了,宿主机上会积累大量无用的镜像层、停止状态的容器和悬空的匿名卷。看磁盘空间:
docker system df这个命令能直观展示你当前占用了多少空间。然后:
# 清理停止的容器、悬空网络镜像 docker system prune # 更彻底一些,清理所有未使用的镜像和构建缓存 docker system prune -a --volumessystem prune是安全操作,一般不会误删正在使用的东西。但-a --volumes会清理所有未被容器引用的镜像、数据卷和构建缓存,执行前想清楚。
6.2 给日志加上限,防止日志文件把磁盘撑满
容器日志默认会无限增长,尤其是那种高频打印日志的应用,几天就能吃掉几十GB。从源头控制更稳妥。启动容器时加上日志轮转参数:
docker run -d --name app \ --log-opt max-size=10m \ --log-opt max-file=3 \ my_app_image意思是最多保留3个日志文件,每个最大10MB。Compose里对应:
services: app: image: my_app_image logging: driver: json-file options: max-size: "10m" max-file: "3"如果是已经跑起来的容器,修改日志参数需要重建容器,但你可以先通过truncate -s 0把当前日志文件清空,再用docker logs确认应用没有因为日志文件被清空而报错。大多数容器日志驱动是json-file,截断日志文件不会影响容器运行。
6.3 调试容器内部环境时,用临时的初始化命令
排查问题时,经常需要临时起一个容器做网络连通性测试或者环境检查,比如看看特定端口能不能通。我常用的方式是:
docker run --rm -it --network app-network nicolaka/netshoot \ ping app-mysql--rm让容器在退出后自动删除,不留垃圾。nicolaka/netshoot这个镜像里预装了tcpdump、ping、curl、nc等网络调试工具,比在业务容器里现场装工具方便太多。这种"用完即焚"的临时容器,是排查容器网络问题的利器。
6.4 镜像体积越小,拉取和启动越快
这是老生常谈的话题,但值得再次强调。同样的应用,Java基础镜像可能上百MB,Alpine版本可能只有几十MB;多阶段构建可以把编译工具链只留在构建阶段,最终镜像里只保留运行时的产物。构建镜像时的习惯直接影响日常使用体验,尤其在网络环境一般的情况下,小镜像带来的加速是立竿见影的。
大致思路是:
# 构建阶段 FROM golang:1.22 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED=0 go build -o app . # 运行阶段 FROM alpine:latest COPY --from=builder /app/app /usr/local/bin/app CMD ["app"]最后跑出来的镜像不包含golang工具链,基础层也很薄,推拉速度会快很多。
6.5 容器重建的通用套路
无论改了什么环境变量、更换了镜像版本还是调整了挂载卷,最终你都会面临容器重建。我的标准操作是五步,经过长期验证,基本不会出问题:
- 先备份:如果是数据库容器,先做一下数据导出或者确认数据卷里有完整数据。
- 停旧容器:
docker stop 容器名。 - 删除旧容器:
docker rm 容器名。注意,这一步和上一步是两回事,只知道stop不删除,直接再跑同名的会报名字冲突。 - 用新参数重新启动。
- 确认状态:
docker ps看是否在运行,docker logs --tail 50 容器名看是否有报错。
这套流程看起来没有技术含量,但比直接docker rm -f安全得多,因为你有机会在删除前备份,也有时间观察新容器是否正常。
顺手分享一个我个人的习惯:给每一个容器都固定名称,不要用随机生成的容器名。有名字的容器在排错时可以直接看见业务语义,日志、exec、inspect都不需要先查ID。
用Docker时间久了,会发现它的核心价值其实不是"虚拟化"或者"隔离",而是"环境的可复制性"——昨天在一台机器上验证过的环境,今天在另一台机器上一模一样地跑起来。这种确定性,省掉的环境折腾时间远超学习Docker本身花掉的时间。希望这篇文章里写到的安装细节、中间件搭建、网络排查和Compose使用,能让你少走一些弯路。