
每个搞 Docker 的人基本都经历过这样的场景一条docker pull nginx:latest敲下去进度条在 2% 的位置卡了十分钟最后直接 timeout。我最早接触 Docker 的时候光是把一个 Ubuntu 镜像拉下来就折腾了一下午后来才慢慢摸索出镜像源配置和构建镜像的正确姿势。这篇就结合我做过的实际项目把 Docker 镜像源配置、加速原理和一次完整的自定义镜像构建过程掰开揉碎讲清楚希望对被拉镜像折磨过的朋友有点用。这篇文章适合三类人看刚装完 Docker Desktop 或者 Linux 版 Docker正为拉镜像慢发愁的新手已经在用 Docker 但每次build都很痛苦想优化构建流程的开发者以及只是想快速跑个 demo、又不想被网络问题劝退的朋友。全文不绕弯子直接讲怎么配源、怎么写 Dockerfile、怎么把构建好的镜像跑起来。1. 先搞清楚镜像源到底解决什么问题很多教程会直接甩给你一段daemon.json让你照着改但不解释为什么要改结果就是换了源之后问题依然在知其然不知其所以然。这里我先把原理部分讲明白后面实操的时候你心里就有底了。1.1 Docker Hub 为什么在国内基本拉不动Docker 默认的镜像仓库是 Docker Hub服务器节点主要部署在国外。拉镜像本质上是走 HTTPS 从远端 registry 拉取分层压缩包网络链路过长、国际出口带宽受限再加上部分节点对 Docker Hub 的连接并不友好慢就成了必然。这里有个关键机制要理解Docker 的镜像拉取支持registry mirror也就是镜像加速器。它不是把你本地的请求原封不动转发到 Docker Hub而是先访问一个中间层 registry这个 registry 缓存了常用镜像的分层数据你请求的时候直接从缓存里返回。这就像你下载软件时先用镜像站而不是直接访问官网原理完全一样。1.2 镜像源机制和常见的坑配置镜像源之后docker info里会多出一行 Registry MirrorsDocker 拉镜像时会先尝试从这些 mirror 拉取拉不到再回源到 Docker Hub。需要提醒的是镜像源不是万能药。你在daemon.json里配置的都是 Docker Hub 的加速镜像只对 Docker Hub 官方仓库的镜像有效。如果你docker pull的是某个私有仓库比如registry.example.com/nginx或者第三方仓库比如gcr.io/xxx镜像源完全不会介入。这个区分很重要否则你配完源去拉一个非官方镜像发现还是慢就容易误判“配置没生效”。另一个坑是镜像源本身的稳定性。国内公开的加速镜像站经历了多轮变动早期能用的地址现在可能已经关闭或者只对内部网开放。所以配置镜像源时建议同时配多个地址Docker 会按顺序尝试某个挂了还能用下一个兜底。2. 镜像源配置实操与选型我自己在不同的机器上试过多种配置方式下面按使用场景拆开说。不管你是 Windows 上的 Docker Desktop 还是 Linux 上的命令行 Docker原理都是同一套只是入口不同。2.1 可用镜像源横向对比截至我最近一次实测下面几个源在多数网络环境下是可用的速度和稳定性各有差异镜像源地址适用场景备注https://docker.m.daocloud.ioDocker Hub 全量加速比较稳定我主力使用的源https://dockerproxy.comDocker Hub 全量加速偶尔会有波动https://hub-mirror.c.163.comDocker Hub 全量加速网易老牌镜像速度中等https://mirror.ccs.tencentyun.com腾讯云内网机器专用云服务器上表现很好https://registry.cn-hangzhou.aliyuncs.com阿里云个人版加速器需登录阿里云控制台获取专属地址建议大家在配置之前先写一条命令实测一下哪些源是可用的因为网络环境变化很快curl -I https://docker.m.daocloud.io/v2/如果返回了 HTTP 200 或者 401说明这个源是活的。如果 curl 直接超时或者 404那这个地址大概率已经失效换下一个。2.2 命令行手动配置Linux 推荐这是最通用的方式在/etc/docker/daemon.json里写入配置然后重启 Dockersudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json -EOF { registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://hub-mirror.c.163.com ] } EOF sudo systemctl daemon-reload sudo systemctl restart docker验证是否生效docker info | grep -A 5 Registry Mirrors看到列出的 Registry Mirrors 里有刚才配置的地址就说明生效了。这里有个细节daemon.json一旦存在Docker 会把里面的配置项和默认配置做合并。如果你之前配置过其他参数比如>docker pull nginx:latest --registry-mirror https://docker.m.daocloud.io这个命令在部分 Docker 版本上可能不支持--registry-mirror参数具体取决于版本更通用的做法是直接用带源地址的完整镜像名比如把镜像源地址当作 registry 前缀来拉取。不过这种方式比较绕日常还是建议把源配进 daemon.json一劳永逸。3. 从零构建一个可运行的自定义镜像 demo配置好镜像源、解决了拉取问题之后就可以动手构建自己的镜像了。我下面用一个非常简单的 Node.js 应用作为 demo完整走一遍 Dockerfile 编写、构建、启动的过程。选 Node.js 是因为它够简单、依赖少构建出来的镜像小方便观察每一层的变化适合拿来做教学。先交代一下环境Ubuntu 22.04 服务器或者本地 Linux 虚拟机已安装 Docker Engine 24.x已配置好上文镜像源。如果你是 Docker Desktop for Windows/macOS命令基本一致。3.1 初始化 demo 项目先建一个目录注意不要用中文路径容易出问题然后创建两个文件docker-demo/ ├── app.js └── Dockerfileapp.js内容很简单创建一个 HTTP 服务监听 3000 端口返回当前容器的主机名const http require(http); const os require(os); const server http.createServer((req, res) { res.writeHead(200, { Content-Type: text/html; charsetutf-8 }); res.end(h1Hello from Docker/h1pHostname: ${os.hostname()}/p); }); server.listen(3000, () { console.log(Server is running on port 3000); });这个 demo 有讲究返回主机名是一个很实用的技巧做负载均衡或者多容器部署时你一眼就能看出请求命中了哪个容器实例排查问题特别好使。3.2 编写 Dockerfile 并理解每一行Dockerfile内容如下# 指定基础镜像这里用 Node.js 20 的 Alpine 版本 FROM node:20-alpine # 设置工作目录后续命令都在这个目录下执行 WORKDIR /app # 拷贝 package.json这里用不到依赖但为了演示保留这个习惯 COPY package.json ./ # 如果用到 npm 依赖就执行 npm install没有依赖就跳过 RUN npm install --registryhttps://registry.npmmirror.com || true # 拷贝源码到镜像中 COPY app.js . # 暴露端口 EXPOSE 3000 # 设置启动命令 CMD [node, app.js]然后随便创建一个package.json保持合法 JSON 即可{ name: docker-demo, version: 1.0.0, main: app.js, dependencies: {} }这个 Dockerfile 里每个指令都有讲究FROM node:20-alpine选择 Alpine 变体是因为它体积小基础镜像只有几十 MB而完整版 Node 镜像动辄上 GB。具体做生产镜像时你要在“体积小”和“兼容性”之间权衡Alpine 用了 musl libc有些原生依赖可能需要额外编译。COPY package.json ./先把依赖清单单独拷进去是为了利用 Docker 的层缓存。之后你改了源码但没改依赖重新构建时npm install这层会命中缓存省掉重复下载。但如果把源码和 package.json 一起拷进去改一行代码就要重新跑一遍依赖安装构建效率低很多。RUN npm install --registry... || true这个写法有取巧成分。真实项目里不应该加|| true因为它会掩盖安装失败的错误。我这里加是为了一方面演示如何在内网环境里给 npm 也配上国内源另一方面保证整个 demo 在任何环境下都能构建成功。生产环境请去掉|| true让错误直接暴露出来。EXPOSE 3000这只是一个“声明”告诉 Docker 容器里的应用监听了哪个端口。它本身不会自动做端口映射真正映射是在docker run的时候用-p参数完成的。CMD [node, app.js]指定容器启动后执行的命令。JSON 数组格式是 exec 形式这是推荐的写法。如果你写成CMD node app.jsshell 形式命令会以/bin/sh -c node app.js执行信号处理和进程管理会有细微差异。3.3 Dockerfile 编写中的两个核心概念很多人第一次写 Dockerfile 最大的困惑是为什么不能把全部文件COPY进去就完事为什么要分这么多层这涉及 Docker 镜像的分层存储机制。每条指令FROM、COPY、RUN都会生成一个新的只读层Docker 在构建时会检查每一层是否能复用本地已有的缓存层。判断依据是指令字符串没变且涉及的文件没变。所以最好的做法是把“不常变的依赖文件”放在指令前面把“频繁变更的源码”放在指令后面最大化利用缓存。另一个经常踩坑的是.dockerignore文件。如果没有它COPY . /app会把当前目录下的所有文件——包括node_modules、.git、日志文件——全部打包进构建上下文然后发送给 Docker 守护进程。构建上下文大镜像也臃肿。我建议在项目根目录创建一个.dockerignorenode_modules .git *.log这样构建时这些目录和文件就直接被排除了构建速度提升明显镜像也会干净很多。3.4 执行构建并查看分层在docker-demo目录下执行docker build -t docker-demo:v1 .注意最后的.不是可有可无的它表示构建上下文路径。构建过程会看到每一层的输出类似STEP 1/7: FROM node:20-alpine STEP 2/7: WORKDIR /app STEP 3/7: COPY package.json ./ ... COMMIT docker-demo:v1构建完成后查看镜像docker images | grep docker-demo你会看到docker-demo镜像大小大概 180MB 左右。如果用完整版node:20基础镜像这个体积会翻倍甚至更多。这就是 Alpine 的价值。4. 镜像构建完成之后怎么启动标题里的热词有一个“使用 dockerfile 构建的镜像怎么启动”这确实是很多新手卡住的地方。构建完镜像后启动方式并不复杂但有一个核心原则必须先建立一个容器通常只跑一个主进程监控、初始化、业务进程都是通过脚本或进程管理器来协同而不是在容器里塞一个 systemd。4.1 一条 docker run 命令完成启动docker run -d --name demo-app -p 8080:3000 docker-demo:v1参数拆解一下-d后台运行不加的话会一直占用当前终端输出日志CtrlC 退出时容器也会停掉。--name demo-app给容器取个有意义的名称之后docker logs demo-app、docker stop demo-app都靠它。-p 8080:3000端口映射宿主机 8080 端口转发到容器 3000 端口。注意这是宿主机在前、容器在后的顺序不要写反。你访问http://localhost:8080就能看到页面。docker-demo:v1指定镜像名和标签。启动后验证curl http://localhost:8080能看到Hello from Docker和容器主机名。这个主机名每次启动容器都会变默认是随机生成的一串 ID这正是容器隔离性的一个体现。4.2 进容器看日志与排查容器跑起来之后最常用的三个命令docker ps # 查看运行中的容器 docker logs demo-app # 查看容器日志 docker exec -it demo-app sh # 进入容器内部Alpine 没有 bash用 sh进入容器后可以查看进程、查看环境变量、查看当前目录文件ps aux env ls -la /app这套“启动—日志—进入容器”的流程是日常排查问题的基础。镜像构建成功只是第一步运行时能不能跑起来、前置条件是否满足都得靠这几个命令去验证。4.3 docker compose 管理多个容器如果你的 demo 不再只是一个 Node 服务而是像 MySQL Redis 后端应用一套组合用docker run一条条启动会非常痛苦。这时候可以用 docker compose 来统一管理。项目里加一个docker-compose.ymlservices: app: image: docker-demo:v1 ports: - 8080:3000 restart: unless-stopped redis: image: redis:7-alpine ports: - 6379:6379然后在目录下执行docker compose up -ddocker compose 的优势是描述式管理所有服务的配置都集中在 YAML 文件里换机器部署时只需要把整个目录拷贝过去一条命令就能拉起全套环境。热词里的 “docker compose”、“docker安装mysql8.0并使用”、“docker安装redis主从” 实际上都是这个模式先用镜像源拉取官方镜像再通过 docker run 或者 compose 启动、映射端口、挂载数据卷。挂载数据卷是容器使用中必须掌握的一个点。MySQL、Redis 这类有状态的中间件数据都必须持久化到宿主机否则容器删掉数据就全没了。启动 MySQL 时常见的命令长这样docker run -d --name mysql8 \ -p 3306:3306 \ -v /data/mysql:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORDroot123 \ mysql:8.0-v /data/mysql:/var/lib/mysql就是把宿主机目录挂载到容器内的数据目录实现数据持久化。这个知识点建议实践一下因为不做数据卷挂载的容器删除重建之后数据就清空回初始状态了这种丢了数据的教训一次就够疼。5. 构建与运行中的常见问题排查这一节我把实际碰到过的高频问题整理成速查表后面又挑两个典型场景详细展开。5.1 问题速查表现象可能原因解决方法docker pull超时镜像源没配好或源失效配置 registry-mirrors 并验证生效daemon.json修改后 Docker 起不来JSON 格式错误、配置项冲突用docker run hello-world测试先备份原文件再修改构建时网络卡在npm install/apt-get容器内访问外网受限给对应的包管理器配置国内源npm 用 npmmirrorapt 用清华源容器一启动就退出主进程执行失败、命令路径错误等用docker logs查看退出日志确认 CMD 写的命令在镜像内确实存在端口访问不到没有端口映射或容器内进程没监听确认docker ps里端口映射正常进入容器curl localhost:3000自测构建时提示 no matching manifestCPU 架构不匹配拉取对应架构的镜像或者开启 buildx 做多架构构建docker run时找不到镜像镜像名或标签写错docker images先确认镜像名和 tag 完全一致5.2 daemon.json 出错导致 Docker 起不来这个坑出现频率极高单独拿出来说。修改完/etc/docker/daemon.json之后执行systemctl restart docker结果卡了很久然后systemctl status docker显示 failed。排查步骤很简单先看 Docker 日志journalctl -u docker --no-pager | tail -50日志里会明确提示配置文件哪里写错了常见的错误是 JSON 最后一个对象后面多了一个逗号或者用了一些非标准的转义字符。预防办法修改任何配置之前先备份sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak然后改完之后先做 JSON 语法校验再重启python3 -m json.tool /etc/docker/daemon.json有输出就说明语法正确没有报错再重启。养成这个习惯之后这类低级失误基本能避免。5.3 基础镜像拉不下来和架构不匹配基础镜像拉不下来的原因绝大多数是默认 Docker Hub 访问不稳定。解决方案就是第一部分说的——配置镜像源。如果你换了几个源还是拉不下来那可能是你的机器在特殊网络环境这种情况通常不建议绕过方案去折腾其他路径而是应该检查源地址是否可用、网络策略是否允许访问 HTTPS 443 端口等。架构不匹配是另一个容易忽略的问题。你在 Apple Silicon MacARM64 架构上构建了一个镜像推到远程仓库然后去一台 x86_64 的 Linux 服务器上docker pull并运行会直接报exec format error。因为镜像里的二进制文件是 ARM 架构的无法在 Intel 架构的 CPU 上运行。解决办法是构建多架构镜像或者拉取时显式指定平台docker pull --platform linux/amd64 node:20-alpine本地调试时可以这么指定。如果要推送镜像给不同架构的机器使用用 buildx 一次性构建多平台镜像这才是生产环境的做法。5.4 镜像源配置了但没生效怎么自查如果你配置完镜像源docker pull速度还是没变化先按顺序查三个地方第一用docker info看 Registry Mirrors 是否真的出现了你配置的地址。没有出现就说明配置没加载重启 Docker 或者检查 daemon.json 路径是否错误。第二确认拉取的是 Docker Hub 官方镜像。如果你拉的是 gcr.io、quay.io 或者私服镜像镜像源默认不生效这点前面讲过。第三看镜像源是否本身已经失效。很多公开镜像源的生命周期不长今天能用明天可能就 404。换一个源再测。我见过一种特别隐蔽的情况daemon.json 配置没问题、也生效了但docker pull的镜像正好在镜像源里没有缓存源会回源到 Docker Hub 拉取这个回源过程依然很慢。这类镜像尤其是冷门的旧版本 tag第一次拉的时候就是会卡需要一点耐心或者换一个源试试。5.5 构建上下文过大构建很慢如果你docker build的时候发现每次执行都特别慢而且输出里有一段类似Sending build context to Docker daemon 500MB的日志说明构建上下文太大了。解决办法就是前面提到的.dockerignore文件。在项目根目录创建.dockerignore把不需要的文件明确排除掉。大多数人构建镜像都是把整个项目目录传给 Docker daemon里面如果有 node_modules 或者 .git体积轻松突破几百 MB传到 docker 内置的虚拟化环境都是耗时构建缓存也容易失效。另外构建时的输出日志里其实会显示每层的耗时哪一层耗时特别长那一层就是你的优化目标。一般apt-get install或npm install属于高频耗时层如果数据变化不频繁可以把这层尽量挪到 Dockerfile 靠前的位置让缓存发挥最大作用。最后再说几句体己话镜像源和 Dockerfile 构建这两件事看起来简单但确实是日常使用 Docker 时最影响体感的部分。源配好了拉镜像从“看运气”变成“秒开”Dockerfile 写顺了构建镜像从“玄学”变成“可控”。我把常用的镜像源地址和排查清单整理在下面方便你直接抄作业主力源用 daocloud 和网易那两个配 daemon.json 之前一定记得备份构建镜像时把 .dockerignore 加上、依赖层写在前面。希望这篇分享能帮你少踩几个坑早点把时间花在真正重要的事情上。