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 约 300MB
  • openjdk:11-jre-slim 约 200MB
  • openjdk: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就该登场了。