Tampilan
Kenapa penting
Sepanjang modul M12 kamu mempelajari potongan-potongan yang membuat aplikasi layak production: image Docker, Compose, konfigurasi dan secret, Actuator, metrics, logging dan tracing, serta CI/CD. Di proyek nyata, potongan itu tidak berdiri sendiri — mereka harus bekerja bersama. Proyek mini ini merangkai semuanya pada API pesanan yang kamu bangun di M08 (JPA + Flyway) dan amankan di M09 (JWT). Hasil akhirnya adalah satu repositori yang bisa di-clone siapa pun, dijalankan dengan satu perintah lengkap dengan dashboard dan tracing, lalu dirilis otomatis ke server lewat pipeline. Inilah bentuk portofolio yang membuat pewawancara yakin kamu tidak hanya bisa menulis kode, tetapi juga mengoperasikannya.
Konsep inti
Gambaran target
| Komponen | Peran | Port lokal |
|---|---|---|
app | API pesanan Spring Boot 4 (Java 25) | 8080 |
postgres | Database pesanan, skema dikelola Flyway | 15432 |
redis | Cache katalog produk | 16379 |
| Prometheus | Men-scrape /actuator/prometheus setiap 15 detik | 9090 |
| Grafana | Dashboard latency, error rate, JVM, pool koneksi | 3000 |
| Jaeger | Menerima trace OTLP dan menampilkannya per request | 16686 |
Langkah kerjanya berurutan: (1) image, (2) konfigurasi observability di aplikasi, (3) Compose, (4) pipeline, (5) checklist. Selesaikan dan verifikasi satu langkah sebelum lanjut — debugging lima komponen sekaligus jauh lebih sulit.
Langkah 1 — Dockerfile multi-stage
Pakai pola dari bab Docker: stage build dengan JDK untuk mengompilasi, stage runtime dengan JRE saja, layer Spring Boot diekstrak agar perubahan kode tidak membuat ulang layer dependency, dan proses berjalan sebagai user non-root.
text
# Dockerfile — Potongan untuk API pesanan
FROM eclipse-temurin:25-jdk AS build
WORKDIR /workspace
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw -q dependency:go-offline
COPY src src
RUN ./mvnw -q -DskipTests package \
&& cp target/*.jar application.jar \
&& java -Djarmode=tools -jar application.jar extract --layers --destination extracted
FROM eclipse-temurin:25-jre
RUN useradd --system --uid 10001 spring
WORKDIR /application
COPY --from=build /workspace/extracted/dependencies/ ./
COPY --from=build /workspace/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/extracted/application/ ./
USER spring
EXPOSE 8080
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75"
ENTRYPOINT ["java", "-jar", "application.jar"]ENTRYPOINT berbentuk array (exec form) penting untuk graceful shutdown: Java menjadi proses PID 1 dan menerima SIGTERM langsung, bukan ditelan shell perantara.
Langkah 2 — Actuator, metrics, tracing, log, dan shutdown
Tambahkan starter observability ke pom.xml. Versinya dikelola oleh parent Spring Boot, jadi tidak perlu menulis <version>.
xml
<!-- pom.xml — Potongan -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>Lalu satukan konfigurasinya di profil prod. Semua nilai yang berbeda per environment dibaca dari environment variable, sesuai prinsip 12-factor di bab konfigurasi.
yaml
# src/main/resources/application-prod.yaml — Potongan
server:
shutdown: graceful # selesaikan request aktif sebelum berhenti
spring:
lifecycle:
timeout-per-shutdown-phase: 20s
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
data:
redis:
host: ${REDIS_HOST}
logging:
structured:
format:
console: logstash # log JSON satu baris per event; MDC traceId/spanId ikut tercetak
management:
endpoints:
web:
exposure:
include: health,prometheus
endpoint:
health:
probes:
enabled: true # /actuator/health/liveness & /readiness
tracing:
sampling:
probability: ${TRACING_SAMPLING:0.1}
opentelemetry:
tracing:
export:
otlp:
endpoint: ${OTLP_TRACING_ENDPOINT:http://localhost:4318/v1/traces}Verifikasi langkah ini sebelum lanjut: jalankan aplikasi, panggil POST /api/orders, lalu pastikan /actuator/health/readiness mengembalikan UP, /actuator/prometheus memuat http_server_requests_seconds_count, dan baris log berbentuk JSON dengan field traceId.
Sampling di production
Sampling 100% nyaman di lokal, tetapi di production menambah beban dan biaya penyimpanan. Mulai dari 10% lewat TRACING_SAMPLING, lalu sesuaikan dengan volume trafik.
Langkah 3 — compose.yaml lengkap
Compose menyatukan seluruh stack. Perhatikan healthcheck + condition: service_healthy, stop_grace_period yang lebih panjang dari timeout shutdown, dan secret yang dibaca dari file .env yang tidak di-commit.
yaml
# compose.yaml — Potongan
name: order-service
services:
app:
image: ghcr.io/kamu/order-service:${APP_TAG:-local}
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DB_URL: jdbc:postgresql://postgres:5432/orders
DB_USERNAME: app
DB_PASSWORD: ${DB_PASSWORD:?isi DB_PASSWORD di .env}
REDIS_HOST: redis
OTLP_TRACING_ENDPOINT: http://jaeger:4318/v1/traces
TRACING_SAMPLING: "1.0"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
stop_grace_period: 30s
restart: unless-stopped
postgres:
image: postgres:17
environment:
POSTGRES_DB: orders
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?isi DB_PASSWORD di .env}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d orders"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
prometheus:
image: prom/prometheus
volumes:
- ./ops/prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
jaeger:
image: jaegertracing/all-in-one
ports:
- "16686:16686" # UI; port OTLP 4318 cukup di network internal
volumes:
pgdata:
grafana-data:yaml
# ops/prometheus.yml
scrape_configs:
- job_name: order-service
metrics_path: /actuator/prometheus
scrape_interval: 15s
static_configs:
- targets: ["app:8080"]Jaeger dipilih karena satu image sudah memuat penerima OTLP, penyimpanan, dan UI. Bila timmu memakai Grafana Tempo, cukup ganti service jaeger dengan Tempo dan arahkan OTLP_TRACING_ENDPOINT ke sana — aplikasi tidak perlu diubah karena protokolnya sama-sama OTLP. Untuk production, pin setiap image ke versi spesifik (bukan tag default latest) agar stack tidak berubah diam-diam.
Langkah 4 — Pipeline: test → image → deploy via SSH
Lanjutkan workflow dari bab CI/CD. Job test dan image sama persis; tambahan di proyek ini adalah job deploy ke VPS. Di server, siapkan folder /opt/order-service berisi compose.yaml, ops/, dan .env (berisi DB_PASSWORD, dibuat manual sekali, tidak pernah lewat Git), serta user deploy yang tergabung di grup docker.
yaml
# .github/workflows/release.yml — Potongan job deploy
deploy:
needs: image
runs-on: ubuntu-latest
environment: production # butuh persetujuan reviewer
concurrency: production
steps:
- name: Siapkan SSH
run: |
install -m 700 -d ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Deploy SHA ini
run: |
ssh deploy@${{ vars.DEPLOY_HOST }} \
"cd /opt/order-service && \
export APP_TAG=${{ github.sha }} && \
docker compose pull app && \
docker compose up -d --wait app"
- name: Smoke test
run: |
curl --fail --retry 10 --retry-delay 5 \
"https://${{ vars.DEPLOY_HOST }}/actuator/health/readiness"DEPLOY_KNOWN_HOSTS berisi fingerprint server (hasil ssh-keyscan yang sudah kamu verifikasi), sehingga pipeline tidak memakai StrictHostKeyChecking=no yang rawan serangan man-in-the-middle. Bila smoke test gagal, jalankan ulang job yang sama dengan SHA rilis sebelumnya — itulah rollback-mu. Karena VPS menarik image privat dari GHCR, login sekali di server dengan token yang hanya punya izin read:packages.
Satu VPS bukan zero-downtime
docker compose up -d mengganti container app dengan jeda singkat. Untuk belajar, ini cukup. Zero-downtime sejati butuh lebih dari satu instance di belakang reverse proxy (rolling atau blue-green) — atau orkestrator seperti Kubernetes, yang dibahas di level Microservices.
Langkah 5 — Production readiness checklist
Sebelum menganggap proyek "siap production", centang setiap butir dengan bukti, bukan perasaan.
| Area | Butir |
|---|---|
| Build | ./mvnw verify hijau di CI; image ditandai SHA; base image JRE, user non-root |
| Konfigurasi | Tidak ada secret di Git; semua nilai per-environment lewat env var |
| Database | Migrasi Flyway kompatibel mundur; backup terjadwal dan pernah dicoba restore |
| Health | Liveness & readiness benar; readiness DOWN bila DB tidak terjangkau |
| Observability | Dashboard latency p95, error rate, JVM heap, pool HikariCP; trace & traceId di log |
| Shutdown | Graceful shutdown aktif; stop_grace_period > timeout shutdown |
| Keamanan | Hanya health & prometheus diekspos; Prometheus/Grafana/Jaeger tidak publik |
| Rilis | Branch protection + required checks; rollback ke SHA sebelumnya sudah dilatih |
Contoh di dunia backend
Bayangkan hari pertama setelah rilis: tim CS melaporkan checkout "kadang lama". Dengan stack ini, alurnya terukur. Di Grafana, panel latency p95 POST /api/orders naik dari 120 ms ke 2 detik sejak deploy SHA a1b2c3d, sementara error rate tetap rendah. Kamu mengambil satu traceId dari log JSON request yang lambat:
json
{
"@timestamp": "2026-10-23T14:41:12.384+07:00",
"@version": "1",
"message": "Order ORD-10293 created in 2140 ms",
"logger_name": "id.jalurjava.orders.OrderService",
"thread_name": "http-nio-8080-exec-7",
"level": "WARN",
"level_value": 30000,
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7"
}Membuka trace itu di Jaeger menunjukkan span JDBC SELECT ... FROM products WHERE sku = ? dipanggil 40 kali dalam satu request — N+1 query yang masuk bersama rilis terakhir, karena cache Redis untuk katalog tidak lagi dipakai. Keputusan cepatnya: jalankan rollback ke SHA sebelumnya lewat pipeline (dua menit, migrasinya kompatibel mundur), lalu perbaiki query dengan tenang di branch baru, lengkap dengan test yang menghitung jumlah query. Tanpa metrics, trace, dan rollback berbasis SHA, investigasi yang sama bisa memakan waktu berjam-jam.
Latihan menyusul
Latihan kode untuk proyek mini ini akan tersedia di folder exercises/ modul M12. Sementara itu, terapkan langkah 1–5 pada repositori API pesananmu sendiri.
Pertanyaan interview
1. Bagaimana kamu memastikan deploy tidak memutus request yang sedang berjalan?
Aktifkan graceful shutdown di Spring Boot (server.shutdown: graceful) dengan batas waktu fase shutdown, pastikan Java menerima SIGTERM langsung (exec form ENTRYPOINT), dan beri orkestrator waktu tunggu lebih panjang (stop_grace_period di Compose atau terminationGracePeriodSeconds di Kubernetes). Di setup multi-instance, readiness probe menahan trafik ke instance baru sampai siap.
2. Apa beda metrics, log, dan trace, dan kapan kamu memakai masing-masing?
Metrics adalah angka agregat murah untuk alert dan tren (latency p95, error rate). Log adalah catatan event detail per kejadian. Trace merekam perjalanan satu request lintas komponen beserta durasi tiap langkah. Biasanya alert dari metrics memicu investigasi, trace menunjukkan bagian yang lambat, dan log dengan traceId yang sama memberi konteks detail.
3. Apa yang kamu periksa sebelum menyatakan sebuah layanan siap production?
Build dan test otomatis yang hijau, artefak yang bisa dilacak (tag SHA), konfigurasi dan secret di luar kode, migrasi yang aman, health check yang jujur, observability lengkap dengan dashboard dan alert, graceful shutdown, permukaan serangan minimal, serta backup dan rollback yang sudah pernah benar-benar dicoba — bukan sekadar direncanakan.
Jebakan umum
- Mengekspos port Prometheus, Grafana, Jaeger, atau
/actuator/**ke internet di VPS; batasi dengan firewall atau reverse proxy. - Memakai
ENTRYPOINT java -jar app.jar(shell form), sehinggaSIGTERMditerima shell dan graceful shutdown tidak pernah terjadi. stop_grace_perioddefault 10 detik lebih pendek dari timeout shutdown aplikasi, sehingga Docker mengirimSIGKILLdi tengah request.- Meng-commit
.envberisi password, atau memakaiStrictHostKeyChecking=nodi pipeline deploy. - Readiness yang selalu
UPmeski database mati, sehingga smoke test lulus padahal aplikasi tidak berfungsi. - Sampling trace 100% dan log level
DEBUGdi production, membebani aplikasi dan penyimpanan.
Ringkasan
- Proyek mini ini merangkai seluruh M12 pada API pesanan: image, Compose, konfigurasi, observability, dan pipeline.
- Dockerfile multi-stage dengan JRE, layer terekstrak, user non-root, dan exec-form
ENTRYPOINT. compose.yamlmenjalankan app, PostgreSQL, Redis, Prometheus, Grafana, dan Jaeger dengan healthcheck dan secret dari.env.- Actuator mengekspos health probes dan metrik Prometheus; tracing OTLP ke Jaeger; log JSON memuat
traceId; graceful shutdown aktif. - Pipeline: test → image bertag SHA → deploy via SSH dengan secret di environment, smoke test readiness, dan rollback ke SHA sebelumnya.
- Gunakan production readiness checklist berbasis bukti sebelum menyatakan layanan siap.