Docker实战全攻略:从镜像构建到Compose编排,解决部署踩坑难题

发布时间:2026/9/15 13:52:01
Docker实战全攻略:从镜像构建到Compose编排,解决部署踩坑难题
docker 如今已经不只是运维工程师的工具它几乎成了后端开发者的标配。我接触 docker 是在跟着《狂神说 docker最全笔记》这份资料刷命令的那段时间。说实话那份笔记最大的价值不是命令清单而是把容器到底解决了什么问题讲清楚了。这次我想结合自己这几年的实际使用经历把笔记里的知识框架重新拆解一遍补上那些资料里没写透、但你在真实部署时几乎一定会遇到的坑安装失败、镜像拉不下来、MySQL 中文乱码、Redis 主从起不来、微服务打包慢、Compose 编排报错等等。无论你是刚把 docker 装好的新手还是部署过几个应用但老在某个环节卡住的开发者这篇文章都值得你从头到尾过一遍。1. Docker 到底是什么先把学习框架立起来1.1 一套笔记为什么能成为很多人入门 docker 的第一份资料《狂神说 docker》这套笔记的框架非常经典整个学习路径大概是概述、安装、常用命令、镜像原理、容器数据卷、Dockerfile、网络、Docker Compose、集群部署。这个顺序放在今天看依然合理因为它是按照是什么 - 怎么装 - 怎么用 - 怎么把多个服务编排起来这条线来走的。我当时照着敲完一遍之后最大的收获不是记住了几十条命令而是建立了容器化思维任何应用都可以被拆成镜像 容器 数据卷 网络这几个要素来思考。笔记里有一张特别经典的流程图描述的是docker run命令执行之后后台发生了什么。我用自己的话复述一遍客户端输入命令之后请求发给 docker 守护进程守护进程先检查本地有没有这个镜像有就直接用没有就去配置好的仓库拉取拉下来之后创建容器层启动进程。这个流程想通了后面看容器生命周期、镜像分层、数据卷全都顺理成章。1.2 容器和虚拟机到底差在哪很多人学 docker 之前先接触的是 VMware 或者 VirtualBox 这类虚拟机。虚拟机是虚拟出一整套硬件然后在上面装一个完整的操作系统所有软件都跑在这个虚拟电脑里所以体积大、启动慢、资源占用高。而容器不一样它直接共享宿主机的操作系统内核只对文件系统、网络、进程、用户做隔离。生活里做个类比虚拟机是把整个家搬走容器是把行李打包进标准集装箱运输工具还是同一辆卡车但每个集装箱之间互不干扰。这个区别带来的直接影响有两个。一是启动速度虚拟机启动可能按分钟算容器启动基本是按秒算。二是资源密度一台 8G 内存的服务器跑两个虚拟机可能就喘了但跑几十个轻量容器完全没问题。所以容器特别适合微服务这种需要大量独立进程的场景。当然容器也有短板因为共享内核所以不能像虚拟机那样跑一个和宿主机完全不同的操作系统比如不能在 Linux 宿主机上用容器跑一个 Windows 系统。1.3 把镜像、容器、仓库这三个概念刻进脑子镜像、容器、仓库是 docker 世界的三块基石。镜像是一个只读模板里面装好了程序运行所需的代码、运行时、系统工具、依赖库和配置容器是镜像运行时的实例有完整的生命周期可以启动、停止、删除仓库是存放镜像的地方最常用的是 Docker Hub企业里也会自建私有仓库。初学者最容易犯的错是把容器当成一台可以随便改的小机器来用今天进去装个软件明天改个配置后天又想手动删个文件。容器在设计上就是一次性的它应该可以被随时销毁、随时重建。所以一切需要持久保存的数据必须通过数据卷映射到宿主机上而不是留在容器内部。这个意识建立得越早后面踩的坑就越少。2. 安装环节最容易劝退先把平台差异讲透2.1 Windows 装 Docker Desktop 的完整流程和隐藏前提Windows 上安装 docker实际上装的是 Docker Desktop。它本身只是个管理界面真正的 docker 引擎跑在 WSL2 的 Linux 子系统里。所以安装前必须先确认两件事第一电脑是否开启了虚拟化第二Windows 功能里是否开启了虚拟机平台和适用于 Linux 的 Windows 子系统。虚拟化检测很容易任务管理器点性能标签页看 CPU 那一栏虚拟化显示已启用就没问题。如果显示未启用需要重启电脑进 BIOS打开 Intel VT-x 或者 AMD-V。很多品牌机出厂默认是关着的卡在这一步的人特别多。# 确认 WSL 版本docker desktop 需要 WSL2 wsl --status # 如果版本是 1升级到 2 并设置为默认 wsl --update wsl --set-default-version 2安装完成后如果启动报virtualization support not detected或者failed to connect to the docker api大概率是 WSL2 环境没就绪。这时候先去启用或关闭 Windows 功能确认三个选项都勾上了再去 BIOS 确认虚拟化最后重开 Docker Desktop。这三板斧能解决绝大多数 Windows 安装问题。2.2 Linux 下安装Ubuntu 和 CentOS 的推荐做法很多教程图省事直接让你apt install docker.io或者yum install docker。这种装法有个隐患系统自带源里的 docker 版本往往偏旧。旧版本在构建多阶段镜像、使用新网络特性时会踩各种兼容坑。所以我的建议是走官方源。Ubuntu 上大致是这样sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.ioCentOS 上要先把旧版本清干净。如果你之前装过 docker 或者 podman先卸载否则会有源冲突。然后配置 yum 仓库安装 docker-ce。装完记得启动服务并设置开机自启sudo systemctl enable --now docker验证是否装好一条命令就行sudo docker run hello-world能看到Hello from Docker!就说明整个链路通了。如果这一步卡在拉镜像说明网络访问 Docker Hub 不稳定直接进入下一节换源。2.3 权限、服务起不来这些高频问题的现场排查热词里有个出现频率极高的报错docker: permission denied while trying to connect to the Docker daemon socket。原因很简单docker 引擎的 socket 文件/var/run/docker.sock属于 root 用户和 docker 组普通用户没有访问权限。解决办法是把当前用户加到 docker 组sudo groupadd docker sudo usermod -aG docker $USER newgrp docker重新登录后执行docker ps就不会再报权限问题了。但这里必须提醒一句docker 组里的用户实际上拥有管理整个 docker 引擎的权限基本等同于 root。如果你管理的是一台生产服务器给用户加 docker 组要慎重最好还是用 sudo 来执行 docker 命令。服务启动失败的问题排查思路是固定的先看systemctl status docker再看日志journalctl -u docker -n 50。常见的坑有三个一是/etc/docker/daemon.json文件写错了比如镜像源格式不对二是磁盘空间满了docker 启动时会初始化空间三是 selinux 或者防火墙拦截了。配置文件改完之后最稳妥的检查方法是先手动执行一次dockerd它会有非常明确的报错输出比反复重启服务看状态高效得多。3. 镜像、仓库和下载慢的真正解法3.1 镜像为什么是一层一层的镜像分层的概念笔记里讲得很形象每一层都是只读的像千层蛋糕一样叠在一起。为什么要分层因为可以复用。比如你基于同一个基础镜像构建十个应用镜像这十个镜像可以共享底层几百 MB 的内容本地存储大幅节省pull 的时候也能增量下载本地已有的层直接跳过。docker 的存储驱动overlay2就是靠这种层级叠加来实现高效文件系统的。当你docker run启动容器时docker 会在镜像层之上加一个可写层后续对文件的修改都落在这一层里。这也就解释了为什么容器删除后数据会消失——因为可写层跟着容器一起被销毁了。明白了镜像分层的原理你就能理解为什么 Dockerfile 里每一行指令都可能生成新层也就能理解为什么构建镜像时要尽量合并 RUN 指令、把不常变的内容放在前面、把经常变的代码放在最后这样能最大化利用构建缓存。3.2 配置镜像源的具体姿势在国内网络环境下直接从 Docker Hub 拉镜像经常慢到怀疑人生。配置镜像源是必须做的一步。编辑/etc/docker/daemon.json{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://mirror.baidubce.com ] }改完执行sudo systemctl daemon-reload sudo systemctl restart docker然后docker info里能看到 Registry Mirrors 一栏已经加载了新配置。注意镜像源只对 Docker Hub 的镜像生效。如果你拉的是 ghcr.io、quay.io 这类第三方仓库的镜像还是要走对应的仓库地址。注意网上的镜像源地址经常变动可能今天能用明天就不通了。稳妥的做法是注册一个阿里云容器镜像服务在镜像加速器页面拿到自己专属的加速地址这个地址是相对稳定的配置方式一模一样。如果你的网络环境本身访问 Docker Hub 很顺畅甚至可以不配置镜像源减少一层代理反而更快。3.3 自建私有仓库与离线分发团队内部不想把镜像推到公网最简单的方案是自建一个 registry。一条命令就能起一个私有仓库docker run -d -p 5000:5000 --restartalways --name registry registry:2之后给镜像打标签、推送、拉取docker tag myapp:latest localhost:5000/myapp:latest docker push localhost:5000/myapp:latest docker pull localhost:5000/myapp:latest这里有个坑docker 默认只信任 HTTPS 的 registry。如果你在内网用 IP 加端口访问私有仓库需要在所有客户端的/etc/docker/daemon.json里加上{ insecure-registries: [192.168.1.10:5000] }否则 push 的时候会报http: server gave HTTP response to HTTPS client。离线环境更是离不开私有仓库把镜像包导出再导入也能用但多节点场景下还是 registry 最省事。3.4 下载断断续续、unexpected EOF 怎么处理实际拉镜像的时候很多人遇到过unexpected EOF、net/http: TLS handshake timeout这类报错。根源基本都是网络不稳定、连接被重置。应对策略分几层第一先换镜像源把慢的问题解掉一大半第二如果网络特别差可以给 docker 客户端设置更长的超时时间在/etc/docker/daemon.json里配置{ max-concurrent-downloads: 3, max-download-attempts: 5 }第三实在不行就换台网络更好的机器 pull 完再docker save -o image.tar image:tag导出传到目标机器docker load -i image.tar。这种离线导入方式在多架构镜像、内网环境里特别常用。4. 命令再多也别怕按用途分组记忆4.1 镜像命令和容器命令的速查思路docker 命令看一遍记不住很正常不用死记。我习惯把它们分成三组镜像操作、容器生命周期、容器交互。镜像操作就几个docker images看本地镜像docker search搜远程镜像docker pull拉取docker rmi删除docker build构建docker tag打标签。容器生命周期也很直白docker run创建并启动docker ps查看运行中的容器docker start/restart/stop/kill控制状态docker rm删除容器。交互组docker exec进容器执行命令docker logs看日志docker cp复制文件docker inspect看容器详细信息。表格整理如下方便快速翻查分类命令作用镜像docker images列出本地镜像镜像docker pull 镜像名:标签拉取镜像镜像docker rmi 镜像ID删除镜像镜像docker build -t 名字 .构建镜像容器docker run -d --name 名字 镜像后台启动容器容器docker ps -a列出所有容器容器docker exec -it 容器名 bash进入容器容器docker logs -f 容器名跟踪日志数据docker cp 容器:路径 本地路径复制文件数据docker volume ls查看数据卷docker run最常用的参数也固定-d后台运行-p端口映射-v数据卷挂载--name容器命名-e传入环境变量--restart设置重启策略。把这一组参数吃透90% 的基础部署都能拿下了。4.2 容器数据卷挂载目录、具名挂载、继承挂载数据卷是容器持久化的核心手段也是新手最容易迷糊的地方。挂载的方式分三种直接指定宿主机的目录叫绑定挂载用 docker 管理的数据卷叫具名挂载还有一种继承挂载。绑定挂载最直观冒号左边是宿主机目录右边是容器目录docker run -d -p 8080:80 -v /my/conf:/etc/nginx/conf.d --name nginx nginx:latest冒号右边是容器里的绝对路径左边如果写相对路径或者直接不写docker 会自动创建一个数据卷来托管这种就是具名挂载docker run -d -v mydata:/data --name app myimage多容器之间需要共享数据时可以用--volumes-from继承另一个容器的挂载集合。比如日志容器和业务容器共享同一个日志目录docker run -d --volumes-from app --name logger mylogger注意挂载目录之后容器内对应目录里的原始内容会被宿主机目录覆盖。这个问题在挂载 nginx 配置目录时特别常见很多人挂载完发现 404就是因为把容器里的默认配置也遮掉了。4.3 进入容器的正确姿势与常见误区进入容器有两个命令docker attach和docker exec -it。attach 是直接连接到容器的主进程也就是 PID 1 进程的标准输入输出。如果你 attach 进去的是一个长期运行的服务进程按 CtrlC 可能直接把容器给停了非常危险。所以我基本不用 attach。正确姿势是docker exec它是在容器里新起一个进程。比如docker exec -it mysql bash-it分开来看-i是保持标准输入打开-t是分配一个伪终端。进容器改文件、看进程、查日志都用这个。还有一种特殊情况容器启动后立刻退出了比如镜像里没有常驻进程这时候docker exec进不去可以先docker run -it 镜像 bash代替默认的启动命令进去排查。这个在调试自己的镜像时特别管用。5. 实战部署MySQL、Redis、GitLab、微服务一次说清楚5.1 安装 MySQL 8.0 并让它不乱码、能远程连MySQL 是容器化部署里最常碰到的第一个正式服务。拉镜像、起容器很简单docker pull mysql:8.0 docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -v /my/mysql/data:/var/lib/mysql \ -v /my/mysql/conf:/etc/mysql/conf.d \ -v /my/mysql/log:/var/log/mysql \ mysql:8.0参数逐个说MYSQL_ROOT_PASSWORD是初始化时设置 root 密码三个卷分别持久化数据、配置、日志。容器删了重建数据还在这才算合格。MySQL 8.0 有一个非常典型的坑默认认证插件从mysql_native_password改成了caching_sha2_password。老版本的客户端、某些旧连接池连接时会报Authentication plugin caching_sha2_password cannot be loaded。解决方法是进容器改用户的认证方式docker exec -it mysql8 mysql -uroot -p ALTER USER root% IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;中文乱码的问题也很常见。在挂载的配置目录下新建mysqld.cnf写入[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci重启容器即可。记住部署 MySQL 第一步就配上 utf8mb4后面能省一堆乱码的破事。5.2 Redis 主从部署从手动配置到 compose 编排Redis 主从在容器里的玩法和裸机安装不太一样。先跑一个主节点docker run -d --name redis-master \ -p 6379:6379 \ -v /my/redis/master:/data \ redis:7.0 redis-server --appendonly yes再跑一个从节点通过--slaveof指定主节点地址。注意容器之间互通不能用 localhost要用主节点的容器 IP 或者自定义网络里的服务名。这也是为什么部署集群时我强烈建议先把容器网络建好docker network create redis-net docker run -d --name redis-master --network redis-net \ -p 6379:6379 -v /my/redis/master:/data \ redis:7.0 redis-server --appendonly yes docker run -d --name redis-slave1 --network redis-net \ -p 6380:6379 -v /my/redis/slave1:/data \ redis:7.0 redis-server --appendonly yes --slaveof redis-master 6379进入从节点验证一下docker exec -it redis-slave1 redis-cli info replication看到role:slave和master_link_status:up就说明主从搭好了。一条龙多节点部署更推荐直接用 Docker Compose后面专门讲。5.3 GitLab 这种资源大户要怎么部署才不崩GitLab 是容器化部署里最考验耐心的一个。它一个容器里塞了 N 多个组件内存占用轻松上 4G。部署命令长但拆开看并不复杂docker run -d \ --name gitlab \ --hostname gitlab.example.com \ -p 443:443 -p 80:80 -p 2222:22 \ --restart always \ -v /srv/gitlab/config:/etc/gitlab \ -v /srv/gitlab/logs:/var/log/gitlab \ -v /srv/gitlab/data:/var/opt/gitlab \ gitlab/gitlab-ce:latest第一次启动非常慢可能要几分钟到十几分钟因为它在做初始化配置。这个期间别反复重启容器耐心等docker logs -f gitlab看到gitlab Reconfigured!再访问。内存不够的机器需要对 GitLab 做减配。改挂载目录下的gitlab.rbpuma[worker_processes] 2 sidekiq[max_concurrency] 5 postgresql[shared_buffers] 256MB然后执行docker exec gitlab gitlab-ctl reconfigure。实测这样配置之后内存可以从 4G 降到 2G 左右。如果你的服务器只有 2G 内存还是别硬跑 GitLab 了换 Gitea 或者轻量化的代码托管方案更现实。5.4 用 IDEA 开发并打包 Docker 镜像Java 后端开发最常用的一条链路是本地用 IDEA 写代码测试通过后打包成 docker 镜像。实现方式有两种。第一种在 pom.xml 里加 docker-maven-pluginmaven 构建时直接把 jar 包打进镜像。重点还是在 Dockerfile。一个典型的 Spring Boot 项目 Dockerfile 长这样FROM openjdk:17-jdk-slim WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]然后在项目根目录执行docker build -t myapp:1.0 .第二种方式用 IDEA 自带 Docker 插件。打开 Settings - Plugins装 Docker 插件然后在 Settings - Build - Docker 里配置连接到远程 Docker 守护进程。连接方式可以走 SSH也可以暴露 docker 的 TCP 端口。配置好之后Dockerfile 旁边会有绿色运行箭头点一下就能构建镜像并推送。这种方式胜在可视化构建日志、镜像列表、容器列表都能直接在 IDEA 里看很适合日常开发调试。5.5 其他常见部署场景速览kodbox可道云这类私有网盘部署很简单一条命令起一个容器挂载数据目录就能用。DVWA 这种用于学习 Web 安全测试的实验环境docker run一条命令起两个容器一个跑 PHP 环境一个跑 MySQL主要目的是给安全学习者提供一个本地靶场别在生产环境玩。Milvus 这种向量数据库官方推荐用 Docker Compose 部署单机版一条docker compose up -d就把 etcd、minio、standalone 三个组件全拉起来比手动起容器省太多事。人大金仓这类国产数据库也有官方镜像部署方式和 MySQL 类似注意它兼容的是 PostgreSQL 协议连接工具要选对。至于龙芯这类国产 CPU 环境最大的坑在于架构拉镜像时必须指定对应架构的标签或者使用支持多架构的镜像仓库否则会有exec format error本质是 CPU 指令集不匹配。微服务项目部署ruoyi 系这类前后端分离框架是典型代表。它有 mysql、redis、nacos、网关、多个业务服务如果不用编排工具单靠 docker run 一个一个启动依赖顺序和网络互通会让运维崩溃。这种场景就必须上 Docker Compose 了。6. Docker Compose 编排与依赖管理进阶6.1 Compose 到底解决了多容器部署的什么痛点当你需要同时启动 MySQL、Redis、Nacos、后端服务、前端服务时docker run一条条敲会非常痛苦。Docker Compose 的核心价值就是用一个docker-compose.yml文件把多个容器的配置统一管理起来一条命令完成创建、启动、网络联通。安装 compose 插件后项目目录下的docker-compose.yml定义了所有服务。docker compose up -d按配置启动docker compose down一键停止并清理。最关键的收益是容器网络compose 会自动为项目创建一个网络服务之间直接用服务名互相访问不再需要手动指定 IP。比如后端服务连接数据库配置里填jdbc:mysql://mysql:3306/dbname就行compose 会自动把mysql解析成数据库容器的地址。6.2 一个生产级 Redis 主从的 compose 长什么样我实际用过的一个 Redis 一主二从加密码认证的 compose 文件整理出来长这样version: 3.8 services: redis-master: image: redis:7.0 container_name: redis-master command: redis-server /usr/local/etc/redis/redis.conf ports: - 6379:6379 volumes: - ./master/redis.conf:/usr/local/etc/redis/redis.conf - ./master/data:/data networks: - redis-net redis-slave1: image: redis:7.0 container_name: redis-slave1 command: redis-server /usr/local/etc/redis/redis.conf --replicaof redis-master 6379 ports: - 6380:6379 volumes: - ./slave1/redis.conf:/usr/local/etc/redis/redis.conf - ./slave1/data:/data networks: - redis-net redis-slave2: image: redis:7.0 container_name: redis-slave2 command: redis-server /usr/local/etc/redis/redis.conf --replicaof redis-master 6379 ports: - 6381:6379 volumes: - ./slave2/redis.conf:/usr/local/etc/redis/redis.conf - ./slave2/data:/data networks: - redis-net networks: redis-net: driver: bridge这里 redis.conf 里至少要配置requirepass和masterauth两行主从之间才能带密码同步。--replicaof后面用的是服务名redis-master这就是 compose 网络内置 DNS 解析在起作用。启动后进入任意从节点执行redis-cli -a 密码 info replication能确认主从关系。6.3 容器内的依赖管理为什么重启就丢很多人在用面板类应用比如青龙这类定时任务管理工具时会遇到一个经典问题进入容器手动装了依赖容器一重启依赖全没了。原因前面已经说过容器被销毁重建后所有写在可写层的文件都会丢失。所以正确的做法有三种。第一把依赖安装写进 Dockerfile构建一个自带依赖的镜像第二把依赖安装命令写成一个脚本挂载进容器后每次启动时执行第三如果依赖会频繁变化用数据卷把依赖目录挂载到宿主机比如青龙的依赖目录就可以映射到宿主机上这样容器重建后依赖还在。第三种方案我用得最多因为它不需要重新构建镜像改起来最快。具体操作就是在 compose 文件里把对应的目录映射出来而不是把依赖装在容器内部。依赖管理的问题本质上就是搞清楚什么是无状态的、什么是有状态的代码和依赖尽量打进镜像数据必须落在数据卷里。6.4 编排生产级微服务时的依赖顺序微服务编排里另一个高频坑是服务启动顺序。比如 Nacos 没起来业务服务启动必然失败所以需要depends_on声明依赖关系。但depends_on只能保证容器启动顺序不能保证服务真正可用。Nacos 容器启动了不代表 Nacos 已经可以接受请求中间还有一段初始化时间。稳妥的做法是配合健康检查。compose 里可以这样配置services: nacos: image: nacos/nacos-server:v2.3.0 healthcheck: test: [CMD, curl, -f, http://localhost:8848/nacos] interval: 10s timeout: 5s retries: 12 business-service: image: myapp:1.0 depends_on: nacos: condition: service_healthy这样business-service会在 Nacos 健康检查通过后才启动大幅降低起晚了连不上的报错概率。实测下来这一套配置在部署若依、Spring Cloud 这类微服务时非常实用。我个人在实际操作中的体会是docker 的学习曲线并不是线性的它是一段段爬坡安装成功是一关跑通第一个容器是一关自己写出 Dockerfile 是一关能用一个 compose 把整套服务拉起来才算真正入门。那份最全笔记给你画好了地图但路还是要自己一步一步踩。环境不一致、版本不兼容、依赖丢失、网络不通……这些坑我全踩过一遍好在每个坑最后都能转化成一条可以在下次部署时用得上的经验。最后再分享一个小技巧命令记不住没关系docker --help就在那里但每个命令背后解决的是什么问题这个必须想明白。想明白了docker 就不再是一堆需要死记的指令而是一套关于如何打包、分发、运行应用的思维方式。