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"]
这个版本能用,但不够好。为什么?因为每次改代码都要重新构建整个镜像,而且镜像体积偏大。
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 倍。
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
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"]
21.6 知识体系总览
下面这张图把 Docker 容器化的核心流程串起来了。你跟着这个思路走,基本不会出错。
21.7 避坑指南
最后分享几个我踩过的坑:
- 时区问题:Alpine 镜像默认 UTC 时区,记得加
ENV TZ=Asia/Shanghai - 日志持久化:容器重启日志就丢了,用
docker logs只能看最近的。建议挂载 volume 或接入 ELK - 健康检查:加上
HEALTHCHECK指令,K8s 和 docker-compose 都能自动检测服务状态 - 资源限制:生产环境一定要加
--memory和--cpus,防止某个容器把宿主机资源吃光
我曾经有一次线上事故,就是因为没限制内存,一个内存泄漏的容器把整台机器搞挂了。从那以后,我每个容器都加资源限制,再也没出过类似问题。