Docker零基础入门:从镜像容器到Compose多容器编排实战
1. 从零上手Docker前,先把这几个概念嚼碎
很多人第一次接触Docker,脑子里蹦出来的第一个问题就是:这东西到底跟虚拟机有啥区别?我当年也是这么想的,甚至一度觉得“不就是个轻量级虚拟机嘛”。结果真上手之后才发现,这个理解偏差会直接导致后面镜像、容器、仓库这几个概念全乱套。
Docker本质上是一套应用打包与运行的标准。它把应用本身和它依赖的运行环境(库文件、配置、目录结构)一起打包成一个镜像,这个镜像可以在任何装了Docker的机器上跑起来,跑起来的实例就叫容器。你可以把镜像理解成“安装光盘的ISO文件”,容器就是“用这张光盘装出来并正在运行的系统”。光盘是只读的,装出来的系统可以随便改,改坏了删掉重新装一次就行——这就是容器最核心的价值:一次构建,到处运行,随时重建。
跟传统虚拟机比,虚拟机是把整台硬件虚拟出来,每台虚拟机里跑一个完整的操作系统,资源开销大、启动慢。Docker容器共享宿主机的内核,只隔离进程、网络、文件系统这些层面,所以启动基本是秒级,一台普通笔记本同时跑十几个容器毫无压力。这个差异决定了Docker特别适合微服务、开发环境统一、CI/CD流水线这些场景。
那为什么零基础的人容易卡住?我观察下来,卡点集中在三个地方:一是镜像从哪来(国内网络环境下拉取镜像经常超时),二是容器里的数据怎么持久化(容器一删数据就没了),三是多个容器怎么协同(一个应用往往要配数据库、缓存、消息队列)。这三个问题分别对应镜像仓库、数据卷、Docker Compose,也是这篇笔记要重点拆解的内容。
下面这张表先帮你把几个高频名词一次性理清楚,后面看到就不会懵:
| 名词 | 一句话解释 | 类比 |
|---|---|---|
| 镜像 Image | 只读的应用模板 | 光盘ISO |
| 容器 Container | 镜像运行起来的实例 | 装好的系统 |
| 仓库 Registry | 存放镜像的地方 | 应用商店 |
| Dockerfile | 构建镜像的说明书 | 装机脚本 |
| 数据卷 Volume | 容器外持久化存储 | 外接硬盘 |
| Compose | 多容器编排工具 | 一键启动脚本 |
提示:初学阶段不要一上来就啃Docker的网络模型和存储驱动,先把“拉镜像、跑容器、进容器、看日志、删容器”这条最小闭环走通,信心就建立起来了。
2. 装Docker这件事,选对方式比装得快更重要
2.1 Windows和macOS用户:Docker Desktop是唯一省心解
Windows和macOS上没有原生Linux内核,所以Docker官方提供了Docker Desktop,它内部跑了一个轻量级Linux虚拟机来承载容器。安装过程本身不复杂,去官网下载安装包一路下一步就行,但有几个坑我必须提前说。
Windows用户第一个坑是虚拟化没开。如果你装完启动报“virtualization support not detected”或者“Docker Desktop failed to start”,八成是BIOS里的虚拟化技术(Intel VT-x或AMD-V)没启用,或者跟Hyper-V、WSL2的配置冲突了。Win11用户建议直接用WSL2后端,性能和兼容性都比老的Hyper-V方案好。开启方法是在“启用或关闭Windows功能”里勾选“适用于Linux的Windows子系统”和“虚拟机平台”,然后重启。
第二个坑是WSL2内核没更新。Docker Desktop依赖WSL2,如果系统里的WSL2内核版本太老,会提示你手动下载更新包。这个更新包微软官网有,装完再启动Docker Desktop就正常了。
macOS用户相对省心,Apple Silicon芯片(M系列)要选ARM64版本的安装包,Intel芯片选x86_64版本。装完第一次启动会要求授权,输入系统密码即可。
2.2 Linux用户:别用系统自带的旧版本
Linux是Docker的原生平台,但很多发行版的软件源里带的Docker版本非常老,比如Ubuntu自带的可能还是几年前的版本,功能缺失、bug也多。正确做法是添加Docker官方仓库再安装。
以Ubuntu为例,完整流程是这样的:
装完之后有个必做步骤:把当前用户加入docker组,否则每次敲docker命令都要加sudo,烦得很。
国产操作系统比如麒麟V10(x86_64架构)也支持在线安装Docker 26和Compose插件,思路跟Ubuntu一致,只是仓库地址要换成对应发行版的。麒麟系统基于CentOS系的话,用yum源配置方式即可。
2.3 国内网络环境下拉镜像慢怎么办
这是零基础用户遇到最多的拦路虎。默认的Docker Hub在国内访问经常超时,拉一个几百兆的镜像能等到天荒地老。解决办法是配置镜像加速器,也就是把拉取请求转发到国内的镜像源。
配置方法很简单,编辑/etc/docker/daemon.json(Linux)或在Docker Desktop的设置里找到Docker Engine配置项,加入:
改完重启Docker服务生效。具体用哪个加速地址,建议找当前可用的公共镜像源,因为这类服务时有变动,我这里不写死某个地址,你搜一下“Docker镜像加速器”就能找到最新的可用列表。
注意:镜像加速器只对Docker Hub的官方镜像生效,如果你拉的是其他仓库的镜像,需要单独配置。另外企业环境里如果对镜像安全有要求,建议搭建私有仓库(Registry),所有镜像先同步到内网再分发,既快又可控。
3. 镜像和容器:把“拉、跑、进、删”这条链路走顺
3.1 拉取镜像时到底发生了什么
当你执行docker pull nginx的时候,Docker客户端会去配置的仓库地址请求这个镜像。镜像在仓库里是分层存储的,一个镜像由多个层(layer)叠加而成,每一层对应Dockerfile里的一条指令。比如nginx镜像可能分成基础系统层、安装nginx层、配置文件层、启动脚本层。
分层的好处是复用。如果你本地已经有ubuntu基础镜像,再拉一个基于ubuntu构建的其他镜像时,基础层不用重新下载,只拉差异层就行。这也是为什么Docker镜像看起来很大,但实际下载往往比想象中快。
拉取完成后可以用docker images查看本地镜像列表,会显示仓库名、标签、镜像ID、创建时间和大小。标签(tag)默认是latest,但生产环境强烈建议指定明确版本号,比如nginx:1.25而不是nginx:latest,否则哪天latest更新了,你的环境可能就跑不起来了。
3.2 启动容器的常用姿势和参数含义
最基础的启动命令是:
逐个拆解这些参数:
-d:后台运行,不加的话容器会占着当前终端--name:给容器起个名字,方便后续操作,不起名的话Docker会随机分配-p 8080:80:端口映射,把宿主机的8080端口映射到容器内的80端口。格式是宿主机端口:容器端口- 最后的
nginx:1.25:指定用哪个镜像
启动后用docker ps能看到正在运行的容器,加-a能看到包括已停止的所有容器。想看容器日志用docker logs 容器名,想进容器内部用docker exec -it 容器名 bash(有些精简镜像没有bash,用sh)。
这里有个新手常犯的错误:容器启动后马上退出了。原因通常是容器的主进程执行完就结束了,比如你跑一个ubuntu镜像但没给它任何持续运行的命令,它启动后执行完默认命令就退出了。解决办法是启动时加个持续运行的命令,比如docker run -d ubuntu sleep infinity,或者用-it交互模式进去。
3.3 数据持久化:别让容器一删数据全没
容器本身是无状态的,删掉容器,里面写入的数据就没了。但实际应用比如MySQL,数据必须保留。这时候就要用数据卷(Volume)或绑定挂载(Bind Mount)。
数据卷由Docker管理,存在宿主机的特定目录下,用-v 卷名:容器内路径挂载:
绑定挂载则是直接把宿主机的某个目录挂进容器,用-v /宿主机/绝对路径:/容器内路径。开发环境常用绑定挂载,因为改宿主机文件容器里立刻生效;生产环境更推荐数据卷,由Docker统一管理,权限和备份都更规范。
实操心得:MySQL容器第一次启动时,如果挂载的目录是空的,它会自动初始化数据库;如果目录里已经有数据,它会直接用现有数据启动。所以千万别把空目录和有数据的目录搞混,否则可能出现“密码怎么都不对”的情况——因为用的是旧数据里的密码。
3.4 容器资源隔离与权限问题
Docker通过namespace做资源隔离,通过cgroups做资源限制。你可以用--memory和--cpus限制容器能用的资源:
这在多容器共存时特别重要,防止某个容器把宿主机资源吃光。
权限方面,容器默认以root用户运行,但有些场景下会碰到“权限设置并未向在应用程序容器中运行的地址授予”这类报错,本质是Windows的权限模型和容器隔离机制之间的冲突。遇到这类问题,优先检查挂载目录的权限,Linux下用chmod或chown调整,Windows下检查共享驱动器的权限设置。
还有一种情况是容器需要访问宿主机网络,比如宝塔面板里某个容器想直接用宿主机的网络环境,启动时加--network host即可。但要注意host模式下端口映射参数-p会失效,因为容器直接用了宿主机网络栈。
4. Dockerfile:把“怎么装”写成可复现的脚本
4.1 Dockerfile的基本结构和每条指令的作用
Dockerfile是一个文本文件,里面是一条条构建指令,Docker按顺序执行,每执行一条就生成一个镜像层。核心指令有这么几个:
FROM:指定基础镜像,必须是第一条有效指令WORKDIR:设置后续命令的工作目录COPY/ADD:把宿主机文件复制进镜像,ADD还支持解压和远程URLRUN:在构建阶段执行命令,比如装依赖ENV:设置环境变量EXPOSE:声明容器运行时监听的端口(只是声明,不实际映射)CMD/ENTRYPOINT:容器启动时执行的命令
一个典型的Java应用Dockerfile长这样:
4.2 CMD和ENTRYPOINT的区别,别再搞混了
这是面试和实操都高频的考点。简单说:ENTRYPOINT是容器的入口命令,CMD是默认参数。如果Dockerfile里同时有这两个,CMD的内容会作为参数传给ENTRYPOINT。如果只有CMD,那CMD就是启动命令,但docker run时可以在镜像名后面追加命令覆盖CMD。
举个例子,docker run myimage echo hello,如果镜像的CMD是["java","-jar","app.jar"],那echo hello会覆盖掉CMD,容器就执行echo了。但如果用的是ENTRYPOINT,追加的参数会拼在ENTRYPOINT后面,而不是覆盖。
实践中的建议:需要固定入口用ENTRYPOINT,需要灵活传参用CMD。两者结合是最佳实践,ENTRYPOINT写死可执行程序,CMD给默认参数。
4.3 构建镜像时的缓存机制和优化技巧
Docker构建镜像时会逐层缓存,如果某一层没变,就直接用缓存,不重新执行。所以把不常变的指令放前面,常变的放后面能大幅加快构建速度。比如先COPY依赖文件装依赖,再COPY源码,这样改代码时依赖层还能用缓存。
另一个优化点是减小镜像体积。用slim或alpine基础镜像、多阶段构建(multi-stage build)、清理构建产生的临时文件,都能显著瘦身。多阶段构建的思路是在一个阶段编译,只把产物复制到最终镜像里,编译工具链不进入最终镜像。
这样最终镜像里只有JRE和jar包,没有Maven和源码,体积能小一大半。
踩坑提醒:
COPY和ADD的源路径是相对于构建上下文(build context)的,不是Dockerfile所在目录。如果你在Dockerfile里写COPY ../something,大概率会报错,因为构建上下文之外的文件访问不到。构建时用docker build -t 镜像名 .,那个点就是上下文目录,所有需要的文件都得在这个目录范围内。
5. Docker Compose:多容器协同的正确打开方式
5.1 为什么需要Compose
一个真实的应用很少是单容器。比如一个Web服务,后面要配MySQL、Redis、可能还有消息队列。如果每个容器都手动docker run,参数一大堆,启动顺序还得自己控制,网络还要手动连,维护起来是灾难。
Compose用一个YAML文件把所有这些容器的定义写在一起,一条docker compose up全部启动,自动创建网络让它们互相能通,还能控制依赖顺序。这就是它存在的意义。
5.2 compose.yaml的核心字段
一个典型的Compose文件结构:
关键字段说明:
services:定义所有容器build:指定Dockerfile所在目录,用来自动构建镜像image:直接用现成镜像ports:端口映射depends_on:声明依赖关系,控制启动顺序environment:环境变量volumes:数据卷- 顶层
volumes:声明具名卷
Compose会自动创建一个默认网络,所有service在同一个网络里,可以用service名当主机名互相访问。比如web里连数据库,主机名直接写db就行,不用管IP。
5.3 部署MySQL主从和Redis主从的Compose思路
以MySQL主从为例,Compose里定义两个service,一个master一个slave,分别挂载不同的配置文件和data目录。master的my.cnf里开启binlog并设置server-id,slave的配置里指定server-id和relay-log,启动后进slave容器执行CHANGE MASTER TO指向master。
Redis主从更简单,slave的启动命令加--replicaof master 6379即可,Compose里写:
Compose会自动解析redis-master这个主机名。
5.4 Compose常见报错排查
“cannot start docker compose application. reason: compose [start] exit status”这类报错信息很笼统,实际原因可能有很多。排查顺序建议是:
- 先
docker compose config检查YAML语法有没有问题 - 再
docker compose up不加-d,看前台输出的具体错误 - 检查端口是否被占用,
netstat -tlnp | grep 端口号 - 检查挂载目录权限
- 检查镜像是否拉取成功
还有一个高频问题是容器启动顺序。depends_on只保证启动顺序,不保证依赖的服务已经“就绪”。比如web依赖db,db容器启动了但MySQL还没初始化完,web连过去就会失败。解决办法是在应用里做重试,或者用healthcheck配合depends_on的condition字段。
6. 镜像安全与容器安全,别等出事才重视
6.1 镜像来源要可控
用公开镜像很方便,但风险也实在。你不知道镜像里有没有植入恶意脚本、有没有已知漏洞、有没有多余的账号。生产环境的基本原则是:只用可信来源的镜像,能自己构建就自己构建。
具体做法包括:优先用官方镜像(official image),它们有明确的维护者和更新记录;用特定版本标签而不是latest;定期用漏洞扫描工具扫镜像,比如Trivy、Clair这类;企业环境搭建私有仓库,镜像先同步再分发。
6.2 容器运行时的最小权限原则
容器默认以root运行,这在多租户环境里是隐患。可以在Dockerfile里创建普通用户并切换:
启动容器时也可以加--read-only让文件系统只读,加--cap-drop去掉不必要的内核能力,加--security-opt no-new-privileges防止提权。这些措施单独看作用有限,组合起来能显著降低风险。
6.3 常见安全误区和纠正
一个典型误区是“容器隔离了所以很安全”。实际上容器共享宿主机内核,内核漏洞可能影响所有容器。另一个误区是“镜像里不放敏感信息就行”,但环境变量、构建参数里也可能泄露密钥。正确做法是用Docker secret或外部密钥管理服务,不要把密码硬编码在Dockerfile或Compose文件里。
还有一点容易被忽略:容器日志和临时文件。容器运行久了,日志可能撑满磁盘,临时文件可能包含敏感数据。建议配置日志轮转,限制单个日志文件大小和保留数量。
7. 我踩过的那些坑和压箱底的实操技巧
7.1 容器启动失败但日志没信息怎么办
有时候docker logs看不到任何有用信息,因为容器在极早期就挂了。这时候用docker inspect 容器名看State字段,里面有ExitCode和Error。ExitCode 137通常是OOM被杀,ExitCode 1是应用报错,ExitCode 126/127是命令找不到或没权限执行。
还可以用docker run -it 镜像名 sh直接进镜像看环境,确认命令路径、依赖库是否齐全。
7.2 镜像构建慢的排查思路
构建慢一般三个原因:网络慢(拉基础镜像和装依赖)、层缓存失效、构建上下文太大。第一个换镜像源,第二个调整Dockerfile指令顺序,第三个加.dockerignore文件排除不需要的文件(比如node_modules、.git、日志文件)。
.dockerignore的写法和.gitignore类似,能显著减少发送给Docker守护进程的数据量。
7.3 磁盘空间被Docker吃光
Docker用久了,停止的容器、悬空镜像、未使用的卷会占大量空间。定期清理:
但--volumes要慎用,会删掉没在用的数据卷,如果里面有重要数据就麻烦了。建议先docker volume ls看看有哪些卷,确认后再清。
7.4 几个提升效率的小习惯
第一,给常用命令设别名,比如alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"',输出清爽很多。
第二,用docker compose logs -f 服务名跟踪特定服务日志,比一个个docker logs高效。
第三,开发环境用绑定挂载加热重载,改代码不用重新构建镜像。
第四,把常用的Compose配置存成模板,新项目直接改改就能用,省去重复劳动。
第五,养成看docker stats的习惯,实时监控各容器CPU、内存、网络占用,资源异常能第一时间发现。
这些习惯看起来小,但日积月累能省下大量时间。Docker这东西,入门门槛不高,但真正用顺、用稳,靠的就是这些细节上的积累。我到现在也还在不断踩新坑,每次解决完记一笔,慢慢就形成自己的知识库了。