Docker 入门:是什么、怎么用、和虚拟机的区别
一、Docker 是什么
Docker 是一个开源的容器化平台,用于开发、交付和运行应用程序。它的核心价值在于:把应用及其全部依赖(运行时、库、配置)打包成一个轻量、可移植的容器,确保应用在任何支持 Docker 的环境中都以完全相同的方式运行。
Docker 平台由几个关键部分组成:
- 镜像:一个只读模板,包含运行应用所需的全部文件、依赖和配置,可以理解为容器的"蓝图"或"安装包"。
- 容器:镜像的运行实例。容器之间相互隔离,拥有自己的文件系统和网络空间,但共享宿主机内核。
- 仓库:集中存放和分发镜像的地方,最著名的是公共仓库 Docker Hub,也可以搭建私有仓库。
- Docker 引擎 :客户端-服务器架构。你通过
docker命令行发送指令,由后台守护进程负责构建、运行和分发容器。
二、Docker 的主要作用
Docker 主要解决以下几类问题:
- 环境一致性:消除"在我电脑上能跑"的问题,让开发、测试、生产环境保持一致。
- 快速交付与部署:配合 CI/CD 流程,加速应用的测试和上线。
- 提高资源利用率:相比虚拟机,同样的硬件能运行更多工作负载。
- 隔离运行时依赖:宿主机不需要安装 JDK、数据库客户端等应用依赖,依赖被封装在镜像里。
需要澄清一个常见误解:Docker 并不是"到目标机器上自动安装依赖",而是把运行环境预先打包成镜像。目标机器只需要有 Docker,依赖在镜像里已经就位。
三、Docker 和传统虚拟机的区别
两者在"安装环境"这件事上的根本不同在于:虚拟机安装的是一个完整的操作系统,而 Docker 安装的只是应用运行所需的代码和依赖,共享宿主机内核。
传统虚拟机:模拟一台完整的电脑
典型流程是:先安装操作系统(如 CentOS/Ubuntu),更新系统,再用包管理器逐个安装应用所需的依赖(如 Java 运行环境、数据库、Web 服务器),配置防火墙,最后上传应用代码并启动。这相当于手动配置一台全新的电脑,每换一个环境就要重复一遍,容易出现遗漏或版本不一致。
Docker:打包一个"即开即用"的盒子
应用的环境在构建阶段就被写进 Dockerfile,打包成一个只读镜像。部署时,目标机器只需拉取镜像并运行容器,应用的依赖已经全部就位,无需再执行"安装环境"的步骤。
关键对比
| 维度 | 传统虚拟机 | Docker 容器 |
|---|---|---|
| 部署流程 | 装 OS → 更新 → 手动装依赖 → 上传代码 → 启动,步骤繁琐 | 编写 Dockerfile(一次性)→ 构建镜像 → 运行容器,部署时无需安装环境 |
| 资源占用 | 需运行完整客户 OS,内存磁盘占用以 GB 计 | 共享宿主机内核,镜像通常几十到几百 MB |
| 启动速度 | 需引导完整 OS,通常以分钟计 | 本质是启动隔离进程,通常秒级就绪 |
| 隔离强度 | 硬件虚拟化,强隔离,安全性更高 | 进程级隔离,共享内核,隔离性弱于虚拟机 |
| 适用场景 | 需要不同内核的 OS,或对安全隔离有硬性要求 | 追求部署确定性、速度和资源效率的应用部署 |
在实际生产环境中,两者也常结合使用:在虚拟机里运行 Docker,兼顾隔离与效率。
四、Docker 的核心优势
- 环境一致性:镜像把应用和依赖绑定在一起,开发、测试、生产环境完全一致。
- 部署快、资源省:镜像轻量、启动秒级,一台机器可运行大量容器。
- 跨平台部署:在 Windows 上开发、构建 Linux 镜像,部署到 Linux 服务器完全可行。Windows 上的 Docker Desktop 默认使用轻量 Linux 虚拟机运行 Docker 引擎,构建出的镜像天然是 Linux 镜像。
- 依赖与宿主机解耦 :宿主机不需要装 JDK、不需要配
JAVA_HOME、不需要担心版本冲突,运行环境由镜像保证。
五、核心原理:Docker 如何从镜像仓库拉取预置环境
FROM maven:3.8-openjdk-17-slim AS build 这行命令,实质是让 Docker 去镜像仓库拿到一个已经装好 Maven 和 JDK 的环境。这背后是 Docker 镜像的分层存储和拉取机制。
镜像不是"一个包",而是一叠"层"
Docker 镜像并不是一个巨大的压缩文件。它由多个只读层 叠加而成,每一层代表一次文件系统变更(安装一个软件、复制一批文件、修改一个配置)。maven:3.8-openjdk-17-slim 这个镜像,底层可能是一个精简的 Debian 系统层,上面叠加了 OpenJDK 17 的安装层,再上面是 Maven 的安装层。每一层都有自己独立的、基于内容的哈希值作为标识。
拉取时,Docker 只拿"本地没有的层"
当你执行 docker build,Docker 发现本地没有 maven:3.8-openjdk-17-slim 时,会向镜像仓库(默认 Docker Hub)发起拉取请求。关键机制在于:Docker 会先检查每一层在本地是否已经存在。如果某个基础层(比如 Debian slim 层)你之前拉取其他镜像时已经下载过,Docker 就跳过它,只下载缺失的层。
这就是为什么拉取一个"新"镜像有时会很快:它和本地已有镜像共享了大量底层。Docker 官方文档明确说明,层可以被镜像复用,拉取 debian:bookworm 时如果 debian:latest 的层已经在本地,就"只拉取元数据,不拉取层"。
拉取完成后,层被"叠"起来用
下载下来的各个层被解压到本地存储目录中。当你启动容器时,Docker 通过联合文件系统(union filesystem)把这些只读层按顺序叠加,对外呈现为一个完整的、统一的可读文件系统。容器看到的 java 命令、mvn 工具,就是叠加后的结果。此外,Docker 会在最上面再加一个薄的可写层给容器专用,所有运行时产生的修改都写在这里,底下的只读层保持不变,因此多个容器可以安全地共享同一份镜像层。
所以,FROM 那行到底发生了什么
FROM maven:3.8-openjdk-17-slim AS build 的实质是:Docker 向仓库请求这个镜像的清单 ,清单里列出了构成这个镜像的所有层的哈希值。Docker 逐层检查本地缓存,只下载缺失的层,然后把它们叠加起来,作为后续 COPY、RUN 等指令的执行基础。你不需要写任何安装 Maven 或 JDK 的命令,因为这些层在镜像制作者构建时就已经装好并固化了,你只是把它们"拉"下来复用。
理解了这一点,也就理解了 Docker 镜像高效的原因:层被共享,存储不重复,传输只传增量。这也是为什么你在 Windows 上构建的镜像,拿到 Linux 上运行时,不需要 Linux 宿主机再装一遍 JDK------JDK 所在的层已经跟着镜像一起过去了。
六、Docker 一般怎么用
以 Java 应用为例,典型流程如下。
1. 编写 Dockerfile
推荐使用多阶段构建:在 Docker 内部完成编译,再把编译产物复制到只含运行时的轻量镜像里。
dockerfile
# 第一阶段:构建
FROM maven:3.8-openjdk-17-slim AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
如果已经有打好的 JAR 包,Dockerfile 可以非常简短:
dockerfile
FROM openjdk:8-jre-slim
WORKDIR /app
COPY target/your-app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
FROM 指令的含义是让 Docker 去镜像仓库拉取一个已经预装好相应环境的现成镜像(如包含 JDK 8 的基础镜像),而不是在构建时自己安装。这些工具是基础镜像的制作者提前装好并打包进去的,你直接继承这个结果。具体拉取机制见上一节。
2. 构建镜像
在 Linux 测试环境(或任意有 Docker 的机器)上执行:
bash
docker build -t myapp .
Docker 会拉取基础镜像,把你的 JAR 复制进去,生成 myapp 镜像。
3. 运行容器
bash
docker run -p 8080:8080 myapp
容器启动,内部执行 java -jar app.jar。注意:启动方式从"在宿主机上直接敲 java -jar"变成了"用 docker run 启动容器,容器内再执行 Java 命令"。
4. 几个让 Dockerfile 更好用的习惯
- 先复制依赖文件,再复制源码 :先
COPY pom.xml再COPY src,只改代码不改依赖时可利用缓存,跳过下载依赖。 - 用 ENTRYPOINT 而不是 CMD :主启动命令更明确,不易被
docker run附加命令覆盖。 - 加 .dockerignore :排除
target/、.git/、*.log等,加快构建、减小镜像。 - 考虑非 root 用户运行:生产环境可创建普通用户运行应用,作为安全加固。
七、一个完整的部署场景
场景:Windows 上开发,JDK 8 + Maven,打成 JAR 包,部署到只有 Docker 的 Linux 测试环境。
- Windows 开发环境用 JDK 8 + Maven 打出 JAR 包。
- 编写 Dockerfile:
FROM openjdk:8-jre-slim,COPYJAR 进去,ENTRYPOINT ["java","-jar","app.jar"]。 - 把 JAR 和 Dockerfile 传到 Linux(或在 Windows 上构建镜像后传到 Linux)。
- Linux 上执行
docker build -t myapp .:Docker 拉取 JDK 8 基础镜像,复制 JAR,生成镜像。 - Linux 上执行
docker run -p 8080:8080 myapp:容器启动,内部执行java -jar app.jar。
整个过程中,Linux 宿主机不需要安装 JDK 8,不需要配 JAVA_HOME,不需要担心宿主机装的是 JDK 11 还是 17。JDK 8 被封装在镜像里,与应用绑定,到哪台有 Docker 的机器上运行环境都一样。
八、总结
Docker 的本质是把运行环境预先打包成镜像,让应用带着自己的环境到处运行。它不负责在目标机器上"自动安装依赖",而是让目标机器根本不需要安装这些依赖。相比虚拟机,它更轻、更快、部署更确定;它的核心优势是环境一致性、跨平台部署和依赖与宿主机解耦。镜像的分层存储和按层拉取机制,保证了镜像的高效共享与增量传输。对于"开发环境 Windows、部署环境 Linux、测试机只有 Docker"这类场景,Docker 是非常合适的解决方案。