مقدمة: لماذا Docker و Kotlin هما الثنائي المثالي
التطوير الحديث لا يمكن تصوره بدون الحاويات. أصبح Docker المعيار الفعلي لتغليف التطبيقات وتسليمها وتشغيلها. Kotlin، بدوره، يكتسب شعبية بسرعة بفضل بناء الجملة الموجز، والتوافق الكامل مع Java، ودعم المنصات المتعددة. يتيح الجمع بين Docker و Kotlin إنشاء بيئات خفيفة الوزن وقابلة للتوسع ويمكن التنبؤ بها - من تطبيق خادم بسيط على JVM إلى ملف ثنائي أصلي لإنترنت الأشياء (IoT).
في هذه المقالة، سنشرح كيفية حاوِنة مشاريع Kotlin بشكل صحيح، وتحسين حجم الصور، وتجنب الأخطاء الشائعة. يمكنك تكييف جميع أمثلة الكود مع مشروعك الخاص.
1. الحاوِنة الأساسية لتطبيق Kotlin/JVM
لنبدأ بالسيناريو الكلاسيكي: تطبيق Kotlin يعمل على JVM. للبناء، سنستخدم Gradle - الأداة القياسية لمشاريع Kotlin.
1.1. هيكل المشروع
لنفترض أن لدينا خادم HTTP بسيط على Ktor. هيكل المشروع:
my-kotlin-app/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
└── main/
└── kotlin/
└── com/
└── example/
└── App.kt
1.2. Dockerfile للبناء متعدد المراحل
الممارسة الأساسية هي البناء متعدد المراحل (multi-stage build). المرحلة الأولى تقوم ببناء fat JAR باستخدام Gradle، والثانية تستخدم صورة JRE الحد الأدنى للتشغيل.
# المرحلة 1: البناء
FROM gradle:8.5-jdk21 AS build
WORKDIR /app
COPY --chown=gradle:gradle . .
RUN gradle build --no-daemon -x test
# المرحلة 2: التشغيل
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
1.3. التحسين: تخزين التبعيات مؤقتًا
لتجنب إعادة بناء التبعيات في كل مرة يتم فيها تغيير الكود، قم بتقسيم COPY إلى جزأين:
FROM gradle:8.5-jdk21 AS build
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
تستخدم هذه الحيلة آلية التخزين المؤقت لطبقات Docker: يتم إعادة بناء الصورة فقط عند تغيير build.gradle.kts، وليس عند كل تعديل في الكود.
2. حاوِنة تطبيقات Kotlin/Native
يسمح Kotlin/Native بترجمة الكود إلى ملفات ثنائية أصلية بدون JVM. هذا مثالي للخدمات المصغرة التي تحتاج إلى حجم صورة ضئيل وبدء تشغيل سريع.
2.1. Dockerfile لـ Native
لبناء الملف الثنائي الأصلي، نستخدم صورة مع مترجم Kotlin/Native. الصورة النهائية هي scratch (فارغة) أو distroless.
# مرحلة البناء
FROM jetbrains/kotlin-native:2.0.0 AS build
WORKDIR /app
COPY . .
RUN kotlinc-native src/main/kotlin/Main.kt -o app
# مرحلة التشغيل: صورة ضئيلة
FROM alpine:3.19
WORKDIR /app
COPY --from=build /app/app.kexe app
EXPOSE 8080
CMD ["./app"]
هام: قد يتطلب Alpine تثبيت libc6-compat إذا تم بناء الملف الثنائي باستخدام glibc. استخدم FROM busybox:glibc للحصول على حجم أصغر.
2.2. مقارنة أحجام الصور
- JVM (fat JAR + JRE): ~180-250 MB
- JVM (distroless + slim JRE): ~80-120 MB
- Native (Alpine): ~10-25 MB
- Native (scratch): ~5-10 MB
بالنسبة للخدمات المصغرة عالية التحميل، يوفر نهج Native توفيرًا كبيرًا في الموارد.
3. Docker Compose والمشاريع متعددة الوحدات
غالبًا ما تتكون تطبيقات Kotlin المعقدة من عدة وحدات: API، قاعدة بيانات، ذاكرة تخزين مؤقت. يعمل Docker Compose على تبسيط التنسيق.