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 gradlew 和 COPY 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 内容很多,咱们这里只讲最核心的三个概念:Pod、Deployment、Service。

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

注意: 加上 livenessProbe 和 readinessProbe,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.requests 和 resources.limits。结果某个 Pod 内存泄漏,把整个节点的资源吃光了,其他 Pod 全被 OOM Kill。后来我学乖了,每个容器都加上资源限制。

五、总结

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

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

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


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