1. Docker到底是什么,为什么要学它
1.1 先搞清楚镜像、容器、仓库三个概念
很多人第一次接触 Docker 时容易被“镜像”“容器”“仓库”这几个词绕晕,我用一个最朴素的理解方式:镜像就是安装包,容器就是运行中的程序,仓库就是存放安装包的应用商店。
镜像是一个只读的模板文件,里面包含了你运行一个应用所需要的全部东西——代码、运行时、系统库、配置、环境变量。它被打包成一个标准化的单元,所以只要是装了 Docker 的机器,无论 Windows、Linux 还是 macOS,拿过来就能直接跑,不会出现“在我电脑上明明好好的,到你电脑上就挂了”这种经典事故。
容器则是镜像运行起来之后的状态。你可以同时从同一个镜像启动多个容器,互相之间隔离,就像同一套安装包装到多台电脑上运行,互不干扰。仓库负责存储和分发镜像,最常用的就是 Docker Hub,也可以自己搭私有的 Registry。
1.2 容器为什么这么轻
很多人容易把容器和虚拟机混为一谈,这俩在隔离层面的实现方式是完全不同的。
虚拟机会虚拟化出一整套硬件,然后在上面跑一个完整的操作系统,所以占用空间大、启动速度慢,通常要几十秒甚至几分钟。而容器是直接复用宿主机内核,只是通过 Linux 的 namespace 机制把进程的视图隔离出来,让它以为自己独占了一个系统;再用 cgroup 限制 CPU、内存、网络等资源的使用上限;最后通过 overlayfs 这样的联合文件系统实现分层镜像存储。
说白了,虚拟机是“硬件层面的虚拟化”,容器是“操作系统层面的虚拟化”。所以容器启动往往只需要几百毫秒,一个镜像可能只有几十兆,同一个物理机上可以跑几十个容器,这密度虚拟机完全比不了。
注意:因为容器共享宿主机内核,所以没办法在 Windows 内核上原生跑 Linux 容器。Windows Docker Desktop 的做法是借助 WSL2 或 Hyper-V 创建一个轻量级 Linux 虚拟机,然后在这个虚拟机上跑 Linux 容器,只是整个过程被 Docker 隐藏得很彻底,用起来和原生 Linux 没区别。
1.3 这套机制解决了什么问题
如果你只在本地写代码,Docker 的价值可能还没那么明显。一旦涉及团队协作、多环境部署、微服务拆分,痛点立刻出来了:
- 开发环境、测试环境、生产环境配置不一致,每次上线都像开盲盒。
- 新同事入职,配环境配一天,各种依赖版本冲突。
- 微服务一拆十几个模块,光启动顺序和依赖就够喝一壶的。
Docker 把这些环境问题直接锁进镜像里,代码走到哪,环境就带到哪。这也是为什么 Docker 几乎成了后端开发、运维、测试、甚至是前端工程化绕不开的基础工具。
2. Docker环境安装与启动失败排查
2.1 Windows下安装Docker Desktop
Windows 上装 Docker 基本只有一条路线:Docker Desktop。它现在是 WSL2 优先的实现方式,我强烈建议你默认就走 WSL2,Hyper-V 方案在资源占用和磁盘性能上都不如 WSL2。
安装前先确认三件事,少了任何一个后面都可能报错:
- CPU 虚拟化已经开启。打开任务管理器,切到“性能”标签页,看底部“虚拟化”那项,必须是“已启用”。如果显示“已禁用”,需要进 BIOS 把 Intel VT-x 或 AMD-V 打开。
- Windows 功能里启用了“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。可以管理员身份打开 PowerShell 执行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /restart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /restart - WSL 内核更新到 WSL2 版本,并且把默认版本设为 2:
wsl --set-default-version 2
装完 Docker Desktop 后,界面直接可以用,命令行里敲docker version能看到 Server 信息就说明引擎起来了。
2.2 最头疼的启动失败:虚拟化支持检测不到
很多人安装完 Docker Desktop 一点启动,立刻报错:
Docker Desktop failed to start because virtualisation support wasn't detected.或者:
Virtualization support not detected.这基本是两个原因。
第一种是 BIOS 里真的没开虚拟化。这个对于品牌机和笔记本尤其常见,原因五花八门——有的是出厂默认关的,有的是装了某些虚拟化安全软件后把 VT-x 占用了。解决方法是重启按 Del/F2/F10 进 BIOS,找到Intel Virtualization Technology或SVM Mode,设为 Enabled 保存退出。
第二种是系统层面 Hyper-V 组件处于混乱状态。有时候之前装过旧版 Docker Toolbox 或别的虚拟化软件,残留配置会导致冲突。这种情况先确认 Windows 功能列表里的 Hyper-V 状态,然后管理员身份运行命令行,手动设置 Hypervisor 启动方式:
bcdedit /set hypervisorlaunchtype auto执行完重启再开 Docker Desktop。
2.3 连接引擎失败:npipe 管道报错
另一个高频报错长这样:
failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine这个报错的意思是 Docker CLI 想连接 Docker 引擎,但引擎根本没起来。排查思路按下面顺序来:
- 看系统托盘里的 Docker Desktop 图标,如果一直是启动动画或鲸鱼标是“starting”,说明引擎还在启动中,等一会儿就好。
- 如果一直起不来,打开 Docker Desktop 的 Troubleshoot 面板,点 Restart 一次。
- 如果还不行,直接重置 WSL2 相关组件。在 PowerShell 里执行
wsl --shutdown,然后重新打开 Docker Desktop。 - 最后的大招是重置 Docker Desktop 到出厂状态:Settings -> Troubleshoot -> Clean / Purge data。这会删掉已有的容器和镜像,操作前先确认里面有没有重要数据。
2.4 Ubuntu和CentOS安装
Linux 装 Docker 不建议用发行版自带的软件源,版本太旧,很多新特性用不了。我一般用官方提供的安装脚本,一条命令搞定:
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun装完把当前用户加进 docker 组,省得每次 sudo:
sudo usermod -aG docker $USER newgrp dockerCentOS 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 sudo systemctl start docker sudo systemctl enable docker如果你的 CentOS 7 上是旧版本想升级,直接sudo yum install -y docker-ce,yum 会帮你升级到最新版,不用先卸载,但注意旧容器和镜像数据可能不兼容,升级前最好备份。
3. 镜像加速与高频命令速查
3.1 镜像下载慢,先配置registry-mirrors
学习 Docker 过程中第一个劝退点就是镜像下载慢。Docker Hub 的仓库大多部署在海外,受网络链路和高峰时段带宽影响,拉一个 Ubuntu 镜像都可能卡半天。
解决办法是给 Docker 配置镜像加速器。修改/etc/docker/daemon.json(Windows 是在 Docker Desktop 的 Settings -> Docker Engine 里改):
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.nju.edu.cn" ] }改完保存,执行sudo systemctl restart docker。
注意:这些公共加速源都是社区或机构维护的,稳定性好一阵坏一阵,哪天不给用了就换一批。加速的本质是让 Docker 从国内的缓存节点拉取公共镜像,如果你遇到某个加速源突然失效,换个源或者多配几个兜底就可以了。
另外一个实用经验是:如果某个镜像特别大,拉取总是超时,不要反复删了重拉同一整个镜像,异常中断时可以先执行docker pull重试,Docker 有分层缓存机制,已下载的分层不会重复下载。实在不行就换一台网络环境更好的机器 push 到自己的 Registry,再到目标机上 pull。
3.2 核心命令查得最多的那几张表
高频命令不需要背,多敲两遍自然就记住了。我把平时工作里最常用的整理成几组:
镜像操作
| 命令 | 作用 |
|---|---|
docker pull nginx:latest | 拉取镜像,tag 不写默认 latest |
docker images | 查看本地镜像列表 |
docker rmi nginx | 删除镜像 |
docker build -t myapp:v1 . | 用当前目录 Dockerfile 构建镜像 |
docker push myrepo/app:v1 | 推送镜像到仓库 |
docker tag nginx myrepo/nginx:v1 | 给镜像打标签 |
容器生命周期
| 命令 | 作用 |
|---|---|
docker run -d --name nginx -p 8080:80 nginx | 后台运行容器 |
docker ps | 查看运行中的容器 |
docker ps -a | 查看所有容器,包含已停止的 |
docker start/stop/restart nginx | 启停容器 |
docker rm nginx | 删除容器 |
docker exec -it nginx bash | 进入容器内部 |
docker logs -f nginx | 跟随查看容器日志 |
数据卷与网络
docker volume create mydata docker run -d -v mydata:/data nginx docker network create mynet docker run -d --network mynet --name app nginx3.3 端口映射和数据卷的底层逻辑
玩过几天 Docker 的人都知道-p 8080:80是把宿主机 8080 端口映射到容器 80 端口,但很多人不明白为什么需要这一步。
因为容器有自己的网络命名空间,和别人隔离的。你在宿主机上访问localhost:80时,流量进的是宿主机的网络协议栈,不是容器的。如果想让外部访问容器里跑的服务,就得显式地把宿主机的某个端口“桥接”到容器端口上。这个桥接是 iptables 规则实现的,所以如果你在宿主机上开了防火墙,忘了放行宿主机那个映射端口,外部一样访问不了。
数据卷-v解决的是另一个问题:容器默认是易失的。删除容器时,容器里所有写入的数据就一起没了。把宿主机的目录或数据卷挂载进容器,读写就会落到宿主机磁盘上,容器没了数据还在。
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v /data/mysql:/var/lib/mysql \ mysql:8.0这样/data/mysql目录里就是 MySQL 的物理数据文件,哪怕容器删了重建,数据也完好无损。这是生产环境使用 Docker 的底线要求:任何有状态的数据,必须落到数据卷上。
4. 真实场景部署与Compose编排
4.1 MySQL 8.0 部署:三分钟跑起来一个数据库
光说理论没感觉,直接上手部署几个真实服务。MySQL 8.0 是平时咨询量最大的一个。
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e TZ=Asia/Shanghai \ -v /data/mysql:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci这里几个关键点拆开讲:
-e MYSQL_ROOT_PASSWORD是容器首次初始化时创建 root 用户的密码。-v /data/mysql挂载数据目录,这是必须的。后面的--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci是 MySQL 服务端启动参数,把默认字符集改成 utf8mb4,避免出现中文乱码问题。最后那两行command都会拼到 mysqld 启动命令后面。
跑起来之后,命令行验证一下:
docker exec -it mysql8 mysql -uroot -p如果本机装了 MySQL 客户端,也可以直接mysql -h127.0.0.1 -P3306 -uroot -p连上去,相当于 DBA 日常操作的一个平滑迁移。
4.2 Redis 主从复制:一条命令搭建一主一从
Redis 用 Docker 跑主从,最大的好处是不用在本机装多个 Redis 进程,也不用担心端口冲突和数据目录混乱。先用 Docker 网络把两个容器打通:
docker network create redis-net启动主节点:
docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7 \ redis-server --requirepass 123456启动从节点:
docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7 \ redis-server --masterauth 123456 --replicaof redis-master 6379这里最容易踩的坑是:从节点的replicaof后面必须写主节点的“容器名”而不是127.0.0.1。因为在 Docker 自定义网络里,容器之间是通过容器名进行 DNS 解析的,如果你写127.0.0.1,从节点连的是它自己,这叫自欺欺人。
验证主从状态:
docker exec -it redis-slave redis-cli -a 123456 info replication能看到role:slave和master_link_status:up基本就成了。这套玩法还能继续扩展,热词里有人提到“Redis 生产环境部署”,我的建议是生产至少三主三从集群,用官方的 redis-cli --cluster 命令或者直接上云厂商的托管版,别自己折腾太简单的拓扑。
4.3 GitLab为例:docker-compose 一键部署全家桶
如果部署的服务多了,单个docker run命令不仅长,还容易漏参数。这时候用 Docker Compose 把编排配置全部写进一个 YAML 文件里,所有环境变量、端口映射、数据卷一目了然,一条命令完成整套服务的启停。
以 GitLab 为例,新建一个docker-compose.yml:
version: "3.8" services: gitlab: image: gitlab/gitlab-ce:latest container_name: gitlab restart: always hostname: gitlab.example.com ports: - "8083:80" - "8022:22" - "8443:443" volumes: - /data/gitlab/config:/etc/gitlab - /data/gitlab/logs:/var/log/gitlab - /data/gitlab/data:/var/opt/gitlab shm_size: "256m"启动:
docker compose up -d查看状态:
docker compose psGitLab 是出了名的吃内存,官方推荐至少 4G,实测下来低于 2G 很容易启动失败或者运行期间 OOM。所以生产环境部署前先free -h看一下可用内存。
Compose 在数据卷上有个细节容易搞混:/data/gitlab/config:/etc/gitlab这种是“路径挂载”,宿主机路径必须提前建好;而如果写的是gitlab_config:/etc/gitlab这种,就是卷挂载,由 Docker 管理存储目录。路径挂载适合你想直接备份文件的情况,卷挂载更省心但文件位置不好找。这里用的是路径挂载,方便做备份和迁移。
4.4 开源项目的Compose模板直接抄
不只是 GitLab,现在绝大多数开源项目都默认提供了 docker-compose 模板。我记得第一次搭建 Dify 这类应用的时候,下载源代码后,进入主项目目录的 docker 文件夹里,按照文档操作:
cp .env.example .env docker compose up -d一两分钟整个服务栈就起来了。里面的依赖服务,包括数据库、缓存、向量数据库、网关,全都由 compose 编排好了,这种项目天生就是为 Docker 设计的。
如果你要部署的是本地知识库、自动化工具这类需要依赖管理的项目,Docker 还有一个好处:依赖全部固化在镜像里,不会因为你手动在宿主机装依赖导致环境越搞越乱。我之前为了省事,在宿主机上直接装依赖跑服务,半年后各种版本冲突到崩溃,后来全部收敛进容器,一了百了。
5. 镜像构建与打包部署
5.1 用Dockerfile构建你自己的镜像
别人提供现成镜像可以直接拉,但业务系统就得自己写 Dockerfile 构建了。一个 Java Spring Boot 应用最简的 Dockerfile 长这样:
FROM openjdk:11-jre-slim WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 CMD ["java", "-jar", "app.jar"]构建:
docker build -t myapp:v1 .注意.是构建上下文目录,Docker 会把整个目录发给守护进程作为上下文。所以一定要在项目里加.dockerignore,把target/、.git/、node_modules/这些大目录排除掉,否则每次构建都慢得想砸电脑。
5.2 多阶段构建,镜像瘦身利器
如果你用 Java,基础的openjdk镜像动辄几百兆,推送到私有仓库、拉取到生产服务器都费劲。这时候多用多阶段构建。
需求是:先用一个包含完整编译工具链的镜像来编译项目,再把编译产物拷贝到精简运行时镜像里。以 Go 应用为例:
FROM golang:1.21 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /app/app main.go FROM alpine:3.18 WORKDIR / COPY --from=builder /app/app . EXPOSE 8080 CMD ["./app"]最终镜像只有十几兆。当年我第一次看到镜像体积从 800MB 降到 97MB 的时候,确实感觉打开了新世界。多阶段构建还能顺手解决编译环境与运行环境不一致的问题,因为最终镜像里只保留了运行必需的东西,攻击面小很多。
5.3 IDEA里直接打包镜像
日常开发中,IDE 里点点就能打包镜像是最省事的。IDEA 装 Docker 插件后,Dockerfile 右键就能 Build。
更主流的方式是结合 Maven 插件,比如 Google 的 Jib:
<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>3.4.0</version> <configuration> <to> <image>registry.example.com/myapp:${project.version}</image> </to> </configuration> </plugin>执行:
mvn compile jib:build它会自动推导基础镜像,把项目打成镜像并推送到远程仓库,全程不需要你写 Dockerfile。微服务项目里,十几个模块用这种方式批量打包太爽了。不过需要注意 Jib 默认会从远端的 Google Container Registry 拉基础镜像,在国内网络环境下建议配置jib.from.image为镜像加速器可访问的镜像地址。
5.4 国产化场景下的Docker
前面聊的多是 x86 架构,但热词里有“龙芯 docker”“人大金仓数据库 docker”,说明国产化环境里 Docker 的需求在起来。
龙芯的 LoongArch 架构跟 x86 完全不同,直接拉 x86 的镜像跑肯定起不来。目前的方案主要有两类:一是用支持 LoongArch 的镜像,随着生态完善,越来越多的官方仓库开始提供 loong64 版本;二是靠 QEMU 之类的模拟层跑 x86 镜像,但性能损失很明显,不建议用在线上。国产数据库如人大金仓、达梦等也都陆续发布了官方 Docker 镜像,很多厂家已经默认把 Docker 作为交付方式了,运维体验确实在变好。
6. 常见问题与排查技巧实录
6.1 权限报错:docker: permission denied
刚在 Linux 上装完 Docker 的用户,敲命令大概率会遇到:
docker: permission denied while trying to connect to the Docker daemon socket原因是 Docker CLI 和引擎通信时需要访问/var/run/docker.sock,只有 root 用户和 docker 组的成员才有权限。解决办法就是把当前用户加进 docker 组,重新登录:
sudo usermod -aG docker $USER sudo reboot注意:加入 docker 组就等于拿到了 root 级别的权限,因为 docker 组用户可以控制宿主机上的任意容器。生产服务器上别随便给人加,这点安全洁癖得有。
6.2 镜像拉取异常:unexpected EOF
还有一类高频报错是:
unexpected EOF这个基本都是镜像下载过程中网络中断或者连接被重置导致的。Docker 守护进程在拉取大型镜像时,和远端仓库的连接保持时间较长,一旦链路不稳就会断。解决方案按顺序尝试:
- 直接重试几次
docker pull ...,有时候就是波动。 - 换一个镜像加速器,多配置几个 mirror,一个失败会自动切换。
- 把镜像分割成多个小的,在 Dockerfile 里用多阶段构建减少单层体积。
- 实在不行,在稳定的机器上先构建好镜像,导出成 tar 再传输到目标机器。
6.3 常见问题速查表
| 问题现象 | 可能原因 | 排查命令 / 解决方法 |
|---|---|---|
| 容器启动后立刻退出,看不到任何日志 | 前台进程结束 | docker logs 容器名看最后几行日志 |
| 端口映射不生效 | 防火墙拦截 | 检查宿主机防火墙,确认放行映射端口 |
| 容器内部时间不对 | 时区未设置 | run 时加-e TZ=Asia/Shanghai |
| docker命令需要sudo才能用 | 用户不在docker组 | usermod -aG docker $USER |
| 容器数据删没了 | 没挂数据卷 | 确认是否需要挂载-v |
| 镜像越来越多,磁盘满了 | 无自动清理 | docker system prune -a |
| 容器之间互相访问不通 | 没在同一网络 | docker network create mynet,run 时加--network mynet |
| Docker Desktop连不上引擎 | WSL2异常 | wsl --shutdown后重启 Docker Desktop |
6.4 一个检查优先级的经验
排障的时候我有一套固定的优先级,这套思路是踩了多少次坑总结出来的:先看容器状态,再看日志,最后猜配置。
很多人一遇到问题就翻配置文件,其实 90% 的问题都会在日志里告诉你答案。docker logs -f 容器名永远是你排查的第一站。日志没信息再考虑看环境变量、网络配置这些。
之前有个同事 MySQL 连不上,折腾半天防火墙端口映射,最后发现是启动时忘了设置 root 密码的环境变量,MySQL 容器初始化完连不上,日志里清清楚楚写着。所以,先日志,后配置,这个顺序错不了。
写在后面的一些心得
我自己从最早用docker run一条命令跑各种中间件,到后来整个微服务环境全用 Compose 编排,再到自己写 Dockerfile、搭私有 Registry、搞 CI/CD 自动打包,一步步走过来。最大的体会是:Docker 入门不难,但它背后那一套“一切皆镜像、环境即代码”的思想,才是真正值钱的东西。学命令只是表象,理解镜像的不可变性、容器的易失性、数据卷的持久性,再回过头看那些部署问题,思路会清晰很多。如果你刚开始学,不用贪多,按这篇的顺序,先把环境装好,把 MySQL、Redis 两个容器跑起来,再试着写一个最简单项目的 Dockerfile,整个链路走通一次,后面再深入就有底气了。