21、Docker容器化:Dockerfile编写、多阶段构建、docker-compose编排、镜像优化

说实话,我见过太多后端项目死在「环境不一致」这个坑里了。

本地跑得好好的,一上测试环境就崩。Ktor 项目也不例外。你想想看,你的开发机是 macOS,CI 是 Ubuntu,生产是 CentOS,光一个 JDK 版本差异就能折腾半天。

Docker 就是来解决这个问题的。它把你的应用和所有依赖打包成一个「集装箱」,走到哪都能跑。

21.1 为什么 Ktor 项目需要 Docker?

我个人习惯,从项目第一天就把 Docker 加进来。原因很简单:

  • 环境一致性:开发、测试、生产用同一个镜像
  • 快速部署:docker run 一条命令搞定
  • 资源隔离:每个服务跑在独立容器里
  • 弹性伸缩:配合 K8s 可以轻松扩缩容

我在项目中遇到过最典型的场景:团队新来了个同事,配环境配了两天。后来我把 Dockerfile 扔给他,十分钟就跑起来了。

21.2 Dockerfile 编写:从零开始

先看一个最基础的 Dockerfile。嗯,这里要注意,Ktor 项目最终打出来的是 fat JAR,所以我们需要一个能运行 JAR 的基础镜像。

# 基础镜像,选 Alpine 版本,体积小
FROM openjdk:17-jdk-alpine

# 维护者信息
LABEL maintainer="your-email@example.com"

# 暴露端口,跟 application.conf 里保持一致
EXPOSE 8080

# 创建应用目录
WORKDIR /app

# 复制构建产物
COPY build/libs/ktor-app-1.0.0-all.jar app.jar

# 启动命令
ENTRYPOINT ["java", "-jar", "app.jar"]

这个版本能用,但不够好。为什么?因为每次改代码都要重新构建整个镜像,而且镜像体积偏大。

注意: 不要直接把整个项目复制进镜像。用 .dockerignore 排除掉不需要的文件,比如 .gradle、build 目录里的临时文件。

21.3 多阶段构建:瘦身利器

多阶段构建是我最常用的技巧。说白了,就是用一个镜像来编译,用另一个镜像来运行。

编译阶段需要 Gradle、JDK、依赖库,这些东西加起来可能 1GB 以上。但运行阶段只需要 JRE 和你的 JAR 包。

# 第一阶段:编译
FROM gradle:7.6-jdk17 AS builder

WORKDIR /app

# 先复制依赖配置文件,利用 Docker 缓存
COPY build.gradle.kts settings.gradle.kts ./
COPY gradle gradle

# 下载依赖(这一步会被缓存)
RUN gradle dependencies --no-daemon

# 复制源码并编译
COPY src src
RUN gradle build --no-daemon -x test

# 第二阶段:运行
FROM openjdk:17-jre-alpine

WORKDIR /app

# 从第一阶段复制 JAR
COPY --from=builder /app/build/libs/ktor-app-1.0.0-all.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

你看,最终镜像只有 JRE + JAR,体积从 1.2GB 降到了 180MB 左右。我在项目中用这个方案,部署速度提升了 3 倍。

小技巧: 把 gradle dependencies 放在复制源码之前,这样只要 build.gradle.kts 没变,依赖下载步骤就会命中缓存,每次构建能省 2-3 分钟。

21.4 docker-compose 编排:多服务协同

一个完整的后端系统,通常不止一个服务。Ktor 应用可能需要数据库、缓存、消息队列。docker-compose 就是用来管理这些服务的。

我曾经接手过一个项目,启动文档写了 20 页,光配置数据库就要手动建表。用 docker-compose 之后,一条命令全搞定。

version: '3.8'

services:
  # Ktor 应用
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_URL=jdbc:postgresql://db:5432/ktor_db
      - DB_USER=ktor_user
      - DB_PASSWORD=secret123
      - REDIS_HOST=redis
    depends_on:
      - db
      - redis
    networks:
      - backend

  # PostgreSQL 数据库
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=ktor_db
      - POSTGRES_USER=ktor_user
      - POSTGRES_PASSWORD=secret123
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - backend

  # Redis 缓存
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - backend

volumes:
  pgdata:

networks:
  backend:

启动只需要一条命令:

docker-compose up -d

停掉也简单:

docker-compose down
核心要点: 服务之间通过服务名通信,比如 Ktor 连接数据库用 jdbc:postgresql://db:5432/ktor_db,这里的 db 就是 docker-compose 里定义的服务名。

21.5 镜像优化:从 1GB 到 100MB

镜像大小直接影响部署速度。我见过一个项目,镜像 2.3GB,每次部署要等 10 分钟。优化后降到 120MB,部署只要 30 秒。

下面是我总结的优化清单:

优化手段 效果 说明
多阶段构建 减少 70-80% 编译和运行分离
选择 Alpine 基础镜像 减少 30-50% 比 Ubuntu 小很多
合并 RUN 命令 减少层数 每层都会增加体积
清理临时文件 减少 5-10% apt 缓存、临时文件
使用 .dockerignore 避免无效文件 排除 .git、node_modules 等

来看一个优化后的完整示例:

# 多阶段构建 + Alpine + 清理缓存
FROM gradle:7.6-jdk17-alpine AS builder

WORKDIR /app

COPY build.gradle.kts settings.gradle.kts ./
COPY gradle gradle
RUN gradle dependencies --no-daemon

COPY src src
RUN gradle build --no-daemon -x test && \
    rm -rf ~/.gradle/caches/*

FROM openjdk:17-jre-alpine

WORKDIR /app

COPY --from=builder /app/build/libs/ktor-app-1.0.0-all.jar app.jar

RUN addgroup -S appgroup && adduser -S appuser -G appgroup && \
    chown -R appuser:appgroup /app

USER appuser

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
安全提示: 不要用 root 用户运行容器。上面示例里我创建了 appuser 用户,这样即使容器被攻破,攻击者也拿不到 root 权限。

21.6 知识体系总览

下面这张图把 Docker 容器化的核心流程串起来了。你跟着这个思路走,基本不会出错。

Docker 容器化核心流程 Dockerfile 编写 基础镜像 + 复制 JAR 多阶段构建 编译 → 运行 分离 镜像优化 Alpine + 清理 docker-compose 编排 Ktor + PostgreSQL + Redis 最终产出 轻量镜像(~150MB) + 一键部署 一条 docker-compose up -d 搞定所有服务

21.7 避坑指南

最后分享几个我踩过的坑:

  • 时区问题:Alpine 镜像默认 UTC 时区,记得加 ENV TZ=Asia/Shanghai
  • 日志持久化:容器重启日志就丢了,用 docker logs 只能看最近的。建议挂载 volume 或接入 ELK
  • 健康检查:加上 HEALTHCHECK 指令,K8s 和 docker-compose 都能自动检测服务状态
  • 资源限制:生产环境一定要加 --memory--cpus,防止某个容器把宿主机资源吃光

我曾经有一次线上事故,就是因为没限制内存,一个内存泄漏的容器把整台机器搞挂了。从那以后,我每个容器都加资源限制,再也没出过类似问题。

一句话总结: Docker 容器化不是把应用塞进容器就完事了。Dockerfile 要精简、镜像要瘦身、编排要合理、资源要限制。每一步都做到位,你的 Ktor 应用才能在生产环境稳定运行。

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