86、Docker容器化:Dockerfile编写、多阶段构建、Docker Compose、Kubernetes基础

说实话,容器化这件事,我入行前几年一直觉得是运维的活。直到有一次,我在本地跑得好好的Kotlin服务,部署到测试环境就崩了——环境不一致,JDK版本差一个小版本,依赖库路径不对,折腾了两天。从那以后,我老老实实学起了Docker。

这一章,咱们就聊聊Kotlin项目怎么容器化。我会把Dockerfile编写、多阶段构建、Docker Compose编排,还有Kubernetes入门,串起来讲。你跟着走一遍,基本就能上手了。

Kotlin 项目容器化知识体系 Docker 容器化 Dockerfile 编写 FROM / COPY / RUN / CMD 多阶段构建 构建阶段 → 运行阶段 Docker Compose 多容器编排 / YAML Kubernetes 基础 Pod / Deployment / Service 从单容器到集群编排,逐步演进

一、Dockerfile 编写:从零到镜像

Dockerfile 说白了就是一张「菜谱」。你告诉 Docker 要用什么基础镜像、装什么依赖、复制哪些文件、最后执行什么命令。我刚开始写的时候,总喜欢把所有东西塞到一个层里,结果镜像体积大得吓人。

1.1 基础结构

一个典型的 Kotlin 项目 Dockerfile 长这样:

# 使用官方 OpenJDK 17 作为基础镜像
FROM openjdk:17-jdk-slim AS builder

# 设置工作目录
WORKDIR /app

# 先复制构建文件,利用 Docker 缓存
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .

# 下载依赖(这一步会缓存,除非 build.gradle 变了)
RUN ./gradlew dependencies

# 复制源码并构建
COPY src src
RUN ./gradlew build -x test

# 第二阶段:运行阶段
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
💡 我的习惯:COPY gradlewCOPY src 分开写。这样只要不改构建脚本,依赖层就不会重新下载,构建速度快很多。

1.2 指令详解

指令 作用 注意事项
FROM 指定基础镜像 尽量用 slim 或 alpine 版本
WORKDIR 设置工作目录 相当于 cd,不存在会自动创建
COPY 复制文件到镜像 支持 --from 跨阶段复制
RUN 执行命令(构建时) 每个 RUN 会新增一层,尽量合并
EXPOSE 声明端口 只是文档作用,实际映射靠 -p
ENTRYPOINT 容器启动命令 推荐用 exec 格式,避免 shell 干扰
⚠️ 我曾经踩过的坑:CMD ["java", "-jar", "app.jar"] 但没指定 ENTRYPOINT,结果想传 JVM 参数时发现 CMD 会被覆盖。后来统一用 ENTRYPOINT + CMD 组合,前者固定,后者可覆盖。

二、多阶段构建:瘦身大法

你想想看,一个 Kotlin 项目编译需要 JDK、Gradle、各种依赖,但运行时只需要 JRE 和一个 jar 包。如果只用单阶段构建,镜像里会塞满编译工具,体积轻松上 500MB。

多阶段构建就是解决这个问题的。它允许你在一个 Dockerfile 里写多个 FROM,前面的阶段负责编译,后面的阶段只拿产物。

2.1 为什么需要多阶段?

  • 镜像体积小:从 500MB 降到 150MB 左右
  • 安全:运行镜像里没有源码、构建工具,攻击面小
  • 构建缓存友好:依赖层和源码层分离,改代码不重下依赖

2.2 实战对比

单阶段构建(不推荐):

FROM openjdk:17-jdk
WORKDIR /app
COPY . .
RUN ./gradlew build
EXPOSE 8080
CMD ["java", "-jar", "build/libs/app.jar"]
# 镜像大小:约 650MB

多阶段构建(推荐):

# 构建阶段
FROM gradle:7.6-jdk17 AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src src
RUN gradle build -x test

# 运行阶段
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# 镜像大小:约 180MB

核心思想: 构建环境用全功能镜像,运行环境用精简镜像。中间通过 COPY --from= 传递产物。

三、Docker Compose:多容器编排

实际项目中,你的 Kotlin 服务往往需要数据库、缓存、消息队列。一个个手动 docker run 太累了。Docker Compose 就是用来定义和运行多容器应用的。

3.1 docker-compose.yml 示例

version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb
      - SPRING_REDIS_HOST=redis
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:

3.2 常用命令

命令 作用
docker compose up -d 启动所有服务(后台)
docker compose down 停止并删除容器、网络
docker compose logs -f 实时查看日志
docker compose build 重新构建镜像
💡 我个人习惯: 开发环境用 docker compose up,生产环境用 Kubernetes。Compose 适合本地调试,K8s 适合集群管理。

四、Kubernetes 基础:从容器到集群

Docker Compose 解决了单机多容器的问题。但如果你有几十台服务器,需要自动扩缩容、滚动更新、服务发现……这时候就该 Kubernetes 上场了。

说实话,K8s 内容很多,咱们这里只讲最核心的三个概念:PodDeploymentService

4.1 Pod:最小部署单元

Pod 是 K8s 里最小的调度单位。一个 Pod 可以包含一个或多个容器,它们共享网络和存储。通常一个 Pod 只跑一个容器。

apiVersion: v1
kind: Pod
metadata:
  name: kotlin-app-pod
spec:
  containers:
  - name: app
    image: my-kotlin-app:latest
    ports:
    - containerPort: 8080

4.2 Deployment:声明式更新

直接创建 Pod 不太灵活——如果 Pod 挂了,没人帮你重启。Deployment 就是管这个的。你告诉它「我要跑 3 个副本」,它会自动维持这个状态。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kotlin-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: kotlin-app
  template:
    metadata:
      labels:
        app: kotlin-app
    spec:
      containers:
      - name: app
        image: my-kotlin-app:latest
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10

注意: 加上 livenessProbereadinessProbe,K8s 才能知道你的应用是否健康。我曾经没加探针,结果 Pod 启动失败但 K8s 以为它活着,流量全打过去了……

4.3 Service:稳定的访问入口

Pod 的 IP 是会变的。Deployment 扩容或重启后,IP 就变了。Service 提供一个固定的虚拟 IP 和 DNS 名称,把流量转发到后端的 Pod。

apiVersion: v1
kind: Service
metadata:
  name: kotlin-app-service
spec:
  selector:
    app: kotlin-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP

4.4 常用 kubectl 命令

命令 作用
kubectl get pods 查看所有 Pod
kubectl logs pod-name 查看 Pod 日志
kubectl apply -f deploy.yaml 应用配置
kubectl delete pod pod-name 删除 Pod(Deployment 会自动重建)
⚠️ 避坑指南: 我第一次用 K8s 时,忘了给 Deployment 设置 resources.requestsresources.limits。结果某个 Pod 内存泄漏,把整个节点的资源吃光了,其他 Pod 全被 OOM Kill。后来我学乖了,每个容器都加上资源限制。

五、总结

这一章咱们从 Dockerfile 写起,到多阶段构建瘦身,再到 Docker Compose 编排多服务,最后聊了 K8s 的核心概念。说白了,容器化就是让应用「一次构建,到处运行」。

我个人觉得,对于中小团队,Docker Compose 已经够用。如果业务量上来了,再上 K8s 也不迟。别一开始就追求大而全,先把基础打牢。

嗯,今天就到这儿。你动手写一个 Dockerfile,把之前的 Kotlin 项目容器化试试?


公众号:蓝海资料掘金营,微信deep3321