3、容器化技术基础:Docker核心概念、Dockerfile编写、镜像构建与优化
好,咱们进入第三章。这一章我打算聊聊容器化技术,说白了就是Docker。很多刚接触微服务的同学,总觉得Docker就是个轻量级虚拟机,这么理解其实也没大毛病,但不够精准。我个人习惯把Docker看作一种「进程隔离与打包」的技术——它把你的应用和它需要的环境打包成一个标准单元,扔到哪都能跑。
3.1 Docker核心概念:镜像、容器、仓库
这三个概念,你面试时肯定被问过。我简单说说我的理解。
- 镜像(Image):一个只读的模板,包含了运行应用所需的代码、运行时、库、环境变量和配置文件。你可以把它想象成一个「类」——定义了应用长什么样。
- 容器(Container):镜像的运行实例。它是可写的,有生命周期。就像「对象」——从类实例化出来的具体东西。
- 仓库(Registry):存放镜像的地方。Docker Hub是公共的,企业一般用Harbor或AWS ECR搭私有的。
关键区别:镜像是不变的,容器是变化的。你启动一个容器,在里面写日志、改文件,这些改动不会影响原始镜像。除非你commit成一个新镜像。
我在项目中遇到过一个问题:有同事把容器当虚拟机用,在里面装了一堆调试工具,然后commit成一个新镜像。结果镜像体积从200MB膨胀到2GB。嗯,这里要注意——容器应该是「用完即弃」的,别在里面搞持久化操作。
3.2 Dockerfile编写:从入门到规范
Dockerfile就是构建镜像的「配方」。我见过太多人把Dockerfile写成「屎山」——一个RUN命令里塞了十几条apt-get install。咱们来聊聊怎么写才规范。
3.2.1 基础指令
| 指令 | 作用 | 常见用法 |
|---|---|---|
| FROM | 指定基础镜像 | FROM openjdk:11-jre-slim |
| WORKDIR | 设置工作目录 | WORKDIR /app |
| COPY | 复制本地文件到镜像 | COPY target/*.jar app.jar |
| RUN | 执行构建时的命令 | RUN apt-get update && apt-get install -y curl |
| CMD | 容器启动时的默认命令 | CMD ["java", "-jar", "app.jar"] |
| EXPOSE | 声明容器监听的端口 | EXPOSE 8080 |
举个例子,一个Java微服务的Dockerfile可以这样写:
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/order-service-1.0.0.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
你想想看,这个文件是不是很清晰?每一层都有明确的职责。
3.2.2 多阶段构建
这是我最喜欢的一个特性。以前构建Java应用,你得在镜像里装Maven、JDK,编译完再打包。结果镜像里全是构建工具,体积大得吓人。
多阶段构建的思路是:用第一个阶段做编译,第二个阶段只拿编译产物。这样最终镜像里只有运行时,没有构建工具。
# 第一阶段:编译
FROM maven:3.8.4-openjdk-11 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
我曾经把一个Spring Boot应用的镜像从1.2GB降到了180MB,靠的就是这个技巧。说白了,你只需要最终能跑起来的jar包,干嘛把Maven和源码都塞进去?
小技巧:COPY --from=builder 这个语法,可以从任意一个阶段复制文件,不限于前一个阶段。你可以定义多个builder阶段,然后从不同阶段拿不同的产物。
3.3 镜像构建与优化
构建镜像很简单,docker build -t myapp:1.0 . 就完了。但优化镜像,才是体现功力的地方。
3.3.1 减少镜像层数
Dockerfile里的每条指令都会生成一个层。层数越多,镜像越大,构建也越慢。我建议把相关的RUN命令合并成一条:
# 不推荐
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
# 推荐
RUN apt-get update && \
apt-get install -y curl vim && \
rm -rf /var/lib/apt/lists/*
注意最后那个 rm -rf,这是清理apt缓存。很多新手会忽略这一步,结果镜像里塞了一堆没用的缓存文件。
3.3.2 选择合适的基础镜像
基础镜像的选择直接影响最终镜像大小。拿Java来说:
openjdk:11-jre约 300MBopenjdk:11-jre-slim约 200MBopenjdk:11-jre-alpine约 80MB
我个人习惯用slim版本。Alpine虽然小,但用的是musl libc,有些Java应用会踩坑。比如某些依赖JNI的库,在Alpine上编译不过去。嗯,这里要注意——别为了省几十MB,搞出线上故障。
3.3.3 利用.dockerignore
这个文件的作用和.gitignore类似,告诉Docker在构建时忽略哪些文件。比如:
.git/
node_modules/
target/
*.log
我曾经见过一个项目,构建上下文有500MB,其中400MB是node_modules。加上.dockerignore后,构建速度从5分钟降到了30秒。你想想看,这个文件多重要。
避坑指南:我曾经把.dockerignore写成了.dockerignore.txt(Windows用户容易犯这个错),结果Docker根本不认。构建上下文里塞了一堆没用的文件,镜像体积直接翻倍。检查一下你的文件名,确保没有多余的后缀。
3.3.4 镜像安全扫描
镜像构建完,别急着部署。先扫一遍漏洞。我常用的工具是Trivy:
trivy image myapp:1.0
它会列出镜像里所有已知的CVE漏洞。如果发现高危漏洞,要么换基础镜像版本,要么在Dockerfile里打补丁。这个习惯,我建议你从一开始就养成。
3.4 实战:构建一个优化的Spring Boot镜像
咱们把前面讲的知识点串起来,写一个生产级的Dockerfile:
# 多阶段构建
FROM maven:3.8.4-openjdk-11 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
# 使用非root用户运行,提升安全性
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
CMD ["java", "-jar", "app.jar"]
这个文件里,我做了几件事:
- 多阶段构建,分离编译和运行环境
- 利用
dependency:go-offline缓存Maven依赖,加速后续构建 - 创建非root用户运行容器,降低安全风险
构建命令:
docker build -t order-service:1.0 .
运行命令:
docker run -d -p 8080:8080 --name order-service order-service:1.0
好了,这一章的内容就这些。Docker的核心概念、Dockerfile编写、镜像构建与优化,说白了就是「怎么把你的应用打包成一个干净、小巧、安全的盒子」。下一章咱们聊聊容器编排,到时候Kubernetes就该登场了。