Konfigurasi Aplikasi: Spring Profiles & Environment Variables

13 min readIntermediate
KonfigurasiSpring BootEnvironment12-Factor

Setiap aplikasi yang "serius" akhirnya hidup di lebih dari satu tempat: laptop Anda untuk development, server staging untuk uji coba, dan server produksi untuk pengguna sungguhan. Masalahnya, ketiga tempat itu hampir pasti memakai URL database yang berbeda, kredensial yang berbeda, dan bahkan perilaku yang berbeda — misalnya log yang lebih detail di lokal dibanding di produksi.

Naluri pertama banyak developer adalah menulis semuanya langsung di kode: jdbc:mysql://localhost:3306/app lalu username root dan password root123. Di laptop Anda itu bekerja mulus. Sampai suatu hari kode itu di-push ke repository, seseorang membacanya, dan... selamat datang, database produksi Anda berhasil diakses orang lain. Artikel ini akan membahas cara yang benar: memisahkan konfigurasi dari kode menggunakan Spring Profiles dan environment variables, mengikuti aturan 12-Factor. Ini adalah artikel pendamping yang bagus untuk CI/CD dengan GitHub Actions — karena pipeline yang baik pun tetap membutuhkan konfigurasi yang benar.

Masalah: Kredensial & URL Berbeda di Tiap Lingkungan

Bandingkan tiga "lingkungan" (lingkungan di sini berarti tempat aplikasi dijalankan) berikut:

LingkunganDatabaseUsernamePasswordLog level
Lokal (laptop Anda)H2 in-memoryDEBUG
StagingMySQL di cloud (sandbox)staging_apprahasia stagingINFO
ProduksiMySQL cluster terkelolaprod_apprahasia produksiWARN

Jika setiap nilai di atas ditulis hardcode di kode, yang terjadi adalah:

  1. Secret bocor. Semua orang yang punya akses ke repository — termasuk history git — bisa membaca password produksi.
  2. Konfigurasi salah di produksi. Seorang developer menambah fitur yang butuh URL baru, lupa mengubahnya saat deploy, lalu aplikasi tersambung ke database staging di tengah jam sibuk.
  3. Kode penuh percabangan. Anda mulai menulis if (isProduction) { ... } else { ... } di puluhan tempat. Kode menjadi sulit dibaca dan sulit diuji.

Prinsip inti: kode adalah sesuatu yang berubah jarang dan sama di mana-mana; konfigurasi adalah sesuatu yang berubah sering dan berbeda antar lingkungan. Keduanya harus dipisahkan.

Konfigurasi di Spring Boot

Spring Boot punya sistem externalized configuration yang sangat fleksibel: banyak "sumber" nilai yang saling menimpa berdasarkan urutan prioritas. Anda tidak perlu memilih satu saja — nilailah yang paling tinggi prioritasnya yang akan menang.

application.properties vs application.yml

Tempat paling dasar untuk menaruh konfigurasi adalah file application.properties (format key=value) atau application.yml (format YAML). Keduanya setara; YAML lebih mudah dibaca untuk struktur yang bertingkat.

# application.yml — konfigurasi BASE yang berlaku di semua lingkungan
server:
  port: 8080

spring:
  application:
    name: inventory-service

app:
  contact:
    name: Tim Operasional
    email: ops@example.com

File ini menjadi "default" — nilai yang dipakai bila tidak ada sumber lain yang lebih tinggi prioritasnya. Lalu ada application-{profile}.yml, yaitu konfigurasi spesifik untuk satu profile (kita bahas sebentar lagi). Jika kedua file mengatur key yang sama, nilai dari file spesifik profile-lah yang menang.

Urutan Prioritas Externalized Configuration

Spring Boot menimbang konfigurasi dari banyak tempat, dan yang lebih tinggi prioritasnya menimpa yang lebih rendah. Berikut ringkasannya dari dokumentasi resmi:

PrioritasSumberContoh
TertinggiCommand-line argumentsjava -jar app.jar --spring.profiles.active=prod
environment variables (OS level)SPRING_PROFILES_ACTIVE=prod
application-{profile}.ymlapplication-prod.yml
application.yml (file base)application.yml
TerendahDefault di dalam kode@Value("${app.name:fallback}")

Pola penting di tabel ini: semakin tinggi prioritas, semakin sedikit keterlibatan kode. environment variables lebih tinggi dari application.yml karena env var bisa diatur oleh platform deployment (Docker, Kubernetes, CI) tanpa menyentuh repository sama sekali — persis yang dicari oleh aturan 12-Factor. Command-line arguments menang karena itu yang paling "segar": diberikan langsung saat proses dimulai, tidak pernah ter-commit.

Spring Profiles

Apa itu Profile

Profile adalah sekadar grup konfigurasi bernama. Bayangkan sebuah file application.yml sebagai konfigurasi dasar, dan tiap application-{profile}.yml sebagai lapisan tambahan yang aktif hanya jika profile itu diaktifkan. Nama yang umum dipakai: dev, test, staging, prod.

Profile diaktifkan lewat tiga cara (urutan prioritasnya sesuai tabel di atas):

# 1. Lewat command-line saat menjalankan JAR
java -jar inventory-service.jar --spring.profiles.active=prod

# 2. Lewat environment variable (cara paling umum di Docker/CI)
SPRING_PROFILES_ACTIVE=prod java -jar inventory-service.jar

# 3. Di dalam application.yml (paling tidak fleksibel, hati-hati)
# spring.profiles.active: dev

Perhatikan penamaan env var: spring.profiles.active menjadi SPRING_PROFILES_ACTIVE (dot diganti underscore, semua huruf besar). Spring Boot memetakan nama key menjadi env var dengan aturan ini, jadi Anda tidak perlu menulis konfigurasi tambahan apa pun.

Contoh Lengkap: H2 vs MySQL

Sekarang mari lihat kasus paling umum. Di lingkungan dev kita ingin database ringan H2 in-memory yang diisi otomatis; di prod kita ingin MySQL sungguhan.

# application.yml — base
spring:
  profiles:
    active: dev            # default bila tidak ada env var
  jpa:
    hibernate:
      ddl-auto: validate
# application-dev.yml — aktif ketika profile "dev"
spring:
  datasource:
    url: jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create-drop
    defer-datasource-initialization: true
  h2:
    console:
      enabled: true
# application-prod.yml — aktif ketika profile "prod"
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/inventory
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate

Satu JAR yang sama kini punya dua perilaku berbeda. Di dev, Spring Boot memuat H2, membuat tabel dari entity, dan mengaktifkan console H2 (jdbc:h2:mem:...). Di prod, ia memakai MySQL, dan nilai-nilai rahasia datang dari placeholder ${DB_HOST}, ${DB_USER}, ${DB_PASSWORD} — bukan dari kode. Perhatikan juga ddl-auto: validate di produksi: kita tidak pernah membiarkan aplikasi mengubah skema database produksi secara otomatis, melainkan hanya memvalidasi bahwa skema cocok.

@Profile untuk Bean Kondisional

Kadang perbedaan antar lingkungan bukan hanya soal nilai, tapi juga soal objek. Misalnya: di dev Anda ingin menampilkan dummy client pembayaran, di prod ingin memakai yang asli. Anotasi @Profile menghidupkan atau mematikan sebuah bean berdasarkan profile yang aktif.

// PembayaranClientDummy hanya dibuat saat profile "dev" aktif
@Profile("dev")
@Service
public class PembayaranClientDummy implements PembayaranClient {

    @Override
    public Invoice buatTagihan(Order order) {
        // kembalikan invoice palsu tanpa memanggil API pembayaran
        return new Invoice("DUMMY-" + order.getId(), order.getTotal());
    }
}
// PembayaranClientAsli hanya dibuat saat profile "prod" atau "staging" aktif
@Profile({"prod", "staging"})
@Service
public class PembayaranClientAsli implements PembayaranClient {

    @Override
    public Invoice buatTagihan(Order order) {
        // panggil API gateway pembayaran yang sebenarnya
    }
}

Kedua bean mengimplementasi interface PembayaranClient yang sama. Selama hanya satu yang aktif di satu waktu, tidak terjadi konflik — Spring meng-inject implementasi yang tersedia, dan kode pemanggil tidak perlu tahu yang mana.

@ConfigurationProperties untuk Konfigurasi Terstruktur

Alih-alih menabur @Value di banyak tempat, konfigurasi yang berkelompok sebaiknya dibungkus dalam satu objek bertipe aman dengan @ConfigurationProperties. Perhatikan blok app.contact pada application.yml di atas — inilah cara membacanya:

// record secara otomatis menyediakan accessor name() dan email()
@ConfigurationProperties(prefix = "app.contact")
public record ContactProperties(String name, String email) {
}
// dipakai di mana saja
@Service
public class NotifikasiService {

    private final ContactProperties contact;

    public NotifikasiService(ContactProperties contact) {
        this.contact = contact;
    }

    public String alamatBalasan() {
        return "%s <%s>".formatted(contact.name(), contact.email());
    }
}

Keuntungannya: salah ketik nama key langsung ketahuan saat aplikasi gagal mem-bind, dan semua properti terkait terkumpul rapi dalam satu kelas. Untuk aplikasi yang kompleks, ada mode validasi dengan anotasi Bean Validation (misalnya @NotBlank pada field email). Bila tim Anda masih memakai Java versi lama, bentuk klasik dengan getter & setter juga tetap didukung penuh.

Profile dalam Satu File: YAML Multi-Dokumen

Tidak suka file terpisah? YAML mendukung multi-dokumen menggunakan pemisah ---. Ini berguna untuk konfigurasi pendek, tapi untuk proyek besar sebaiknya tetap pisah per file.

spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:inventory

---

spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/inventory

Catatan sintaks: pada Spring Boot 2.4 ke atas, penanda profile dalam satu file ditulis spring.config.activate.on-profile, bukan spring.profiles yang dulu (kini deprecated).

Environment Variables

Kenapa Env Var untuk Rahasia

application.yml yang berisi password produksi tetap akan masuk ke repository git — dan history git tidak pernah benar-benar hilang. environment variables memecahkan masalah ini karena nilainya diberikan di waktu jalan (runtime), bukan tersimpan di file.

Rahasia umumEnv var yang lazimDiisi di mana
URL databaseDB_URLDocker Compose, cloud secret store
Kredensial databaseDB_USER, DB_PASSWORDDocker Compose, cloud secret store
Kunci JWTJWT_SECRETCI secrets, secret store
Kunci API eksternalAPI_KEY (misal STRIPE_API_KEY)CI secrets, secret store
Lokasi file uploadUPLOAD_DIRPlatform deployment

Pola ${VAR:default}

Spring Boot membaca env var lewat placeholder ${...}. Tambahkan nilai default setelah titik dua bila Anda ingin aplikasi tetap berjalan meski env var tidak di-set:

app:
  name: ${APP_NAME:inventory-service}
  allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000}
  upload-dir: ${UPLOAD_DIR:./uploads}

Pola di atas berarti: pakai nilai env var jika ada, kalau tidak gunakan yang di dalam kurung. Waspada: default yang berupa rahasia adalah jebakan (lihat bagian Jebakan Umum).

.env Lokal dan .env.example

Untuk pengembangan lokal, .env adalah alat yang nyaman — dotenv memuat isi file ke env var sebelum aplikasi mulai. Dua aturan wajib:

  • .env jangan pernah di-commit ke git. File ini berisi rahasia lokal Anda.
  • .env.example harus di-commit sebagai template berisi kunci tanpa nilai asli, agar developer baru tahu variabel apa saja yang dibutuhkan.
# .env (LOKAL, TIDAK DI-COMMIT)
SPRING_PROFILES_ACTIVE=dev
DB_URL=jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
JWT_SECRET=rahasia-lokal-jangan-diunggah
# .env.example (template yang DI-COMMIT)
SPRING_PROFILES_ACTIVE=dev
DB_URL=
JWT_SECRET=
# .gitignore
.env
*.env
!.env.example
FileDikomit?Isi
.envTidakNilai rahasia lokal yang asli
.env.exampleYaTemplate kosong, hanya nama variabel
.gitignoreYaAturan agar .env tidak ikut ter-push

Aturan 12-Factor: "Config"

Prinsip Config pada 12-Factor berbunyi singkat dan tegas: konfigurasi disimpan dalam environment variables, terpisah dari kode. Implikasinya menarik — satu artefak build (satu JAR) yang sama harus berjalan identik di semua lingkungan, hanya dengan nilai env var yang berbeda.

Praktik yang melanggar 12-FactorPraktik yang sesuai 12-Factor
Menulis URL database di dalam kode JavaURL datang dari env var DB_URL
Membuat file config/prod.yml yang ikut di-deployNilai disuntik platform saat proses berjalan
Mengubah kode untuk tiap lingkunganKode sama; env var yang berbeda
Secret tercampur dalam kodeSecret tidak pernah menyentuh repository

Alasan di balik aturan ini sangat praktis: karena artefaknya sama, apa yang lolos uji di staging persis seperti yang berjalan di produksi. Perbedaan satu-satunya adalah lingkungannya — bukan kodenya. Ini memangkas kelas bug "works on my machine, broke in production" secara drastis, dan membuat rollback hanya soal mengganti versi artefak, bukan mengganti kumpulan file konfigurasi.

Secret Management Lanjutan

.env lokal adalah titik awal yang baik, tetapi untuk produksi ada yang lebih kokoh: secret store khusus yang mengelola kredensial secara terpusat, ber-audit, dan bisa di-rotate (diganti) tanpa menurunkan aplikasi.

AlatUntuk apaKapan dipakai
HashiCorp VaultSecret store self-hosted dengan kebijakan akses detailTim yang mengelola infrastruktur sendiri
Cloud Secret Manager (AWS, GCP, Azure)Secret store terkelola dari penyedia cloudAplikasi sudah berjalan di cloud tersebut
CI Secrets (GitHub Actions, GitLab)Menyimpan rahasia untuk pipeline build/deploySaat build & deploy otomatis
Docker secrets / Kubernetes SecretsMenyuntik rahasia ke kontainer saat runtimeAplikasi dikontainerkan

Pola umumnya sama: nilai rahasia tidak lagi berada di file yang Anda commit, melainkan diambil oleh platform dan disuntikkan sebagai env var saat kontainer atau proses dimulai. Dari sudut pandang Spring Boot, tidak ada yang berubah — aplikasi tetap membaca ${DB_PASSWORD}; hanya sumbernya yang kini aman.

Jangan pernah log secret. Membuka debug log di produksi saat ada bug adalah naluri yang bagus, tapi aplikasi yang mencetak nilai DataSource (yang berisi password) ke log bisa membocorkan secret ke sistem log yang mungkin juga milik pihak ketiga. Batasi log konfigurasi pada key-nya, bukan value-nya.

Studi Kasus: Satu JAR untuk Tiga Lingkungan

Mari kita satukan semuanya. Sebuah layanan inventory-service dibangun sekali menjadi satu JAR, lalu dijalankan di tiga lingkungan — tanpa mengubah kode sedikit pun.

# application.yml — hanya nilai yang SAMA di semua lingkungan
server:
  port: ${PORT:8080}

spring:
  jpa:
    open-in-view: false

app:
  contact:
    name: Tim Operasional
    email: ops@example.com
# application-dev.yml — lokal
spring:
  datasource:
    url: jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
  jpa:
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true

logging:
  level:
    com.example.inventory: DEBUG
# application-staging.yml — uji coba di cloud, database sandbox
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/inventory_staging
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate

logging:
  level:
    com.example.inventory: INFO
# application-prod.yml — produksi, log lebih hemat
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/inventory
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate

logging:
  level:
    com.example.inventory: WARN

Cara menjalankannya:

# Lokal (default profile "dev" dari application.yml)
mvn spring-boot:run

# Staging
SPRING_PROFILES_ACTIVE=staging \
DB_HOST=staging-db.internal \
DB_USER=staging_app \
DB_PASSWORD=$(cat ~/.secrets/staging-db) \
java -jar target/inventory-service.jar

# Produksi (profile via command-line, nilai rahasia dari secret store)
java -jar target/inventory-service.jar \
  --spring.profiles.active=prod \
  --DB_PASSWORD=$(vault read -field=value secret/inventory/prod)

Perhatikan hasilnya: database berbeda (H2 vs MySQL inventory_staging vs MySQL inventory), log level berbeda (DEBUGINFOWARN), tetapi artefak JAR-nya sama. Rahasia tidak pernah ada di repository. Inilah yang dimaksud konfigurasi terpisah dari kode.

Jebakan Umum

Secret bocor lewat git history

Anda sadar ada .env ter-commit, lalu menghapusnya — tetapi riwayat git tetap menyimpannya. Sekali secret ter-push, anggap ia bocir dan segera rotate (ganti) di semua layanan yang menggunakannya. Jangan pernah berasumsi "tidak apa, repo ini privat".

Default value yang berbahaya

spring:
  datasource:
    password: ${DB_PASSWORD:root}

Bila DB_PASSWORD lupa di-set, aplikasi diam-diam memakai root — dan bisa jadi tersambung ke database produksi dengan password root. Rahasia tidak boleh punya default. Simpan placeholder tanpa default, atau berikan default yang jelas-jelas tidak valid agar aplikasi cepat gagal alih-alih gagal secara diam-diam:

spring:
  datasource:
    password: ${DB_PASSWORD}

Terlalu banyak konfigurasi dalam satu file

application.yml yang membengkak sampai ratusan baris dengan komentar # TODO: ganti di produksi adalah tanda bahaya. Pisahkan per lingkungan, dan pindahkan blok yang berulang ke konfigurasi bersama bila perlu.

Profile tidak di-set → salah lingkungan

Jika spring.profiles.active tidak pernah di-set dan tidak ada default yang jelas, aplikasi bisa berjalan dengan konfigurasi base yang keliru — misalnya memakai database lokal dev di produksi. Biasakan deploy produksi selalu menyebutkan profile secara eksplisit (melalui env var atau command-line), dan pertimbangkan fail-fast: jika env var rahasia tidak ada, biarkan aplikasi gagal mulai.

JebakanGejalaPencegahan
Secret di git historyKredensial terpakai orang tak dikenal.gitignore sejak awal; rotate bila bocor
Default rahasiaTerhubung dengan kredensial lemahRahasia tanpa default
Satu file penuh semua lingkunganEdit produksi berisiko merusak devPisah application-{profile}.yml
Profile tidak di-setBerjalan di lingkungan yang salahSet profile secara eksplisit saat deploy

Ringkasan

Konfigurasi yang baik adalah konfigurasi yang terpisah dari kode. Spring Boot memberi fondasi yang rapi: file application.yml sebagai default, application-{profile}.yml sebagai lapisan per lingkungan, dan sistem prioritas yang membuat environment variables serta command-line arguments menang atas file. Aktifkan profile lewat SPRING_PROFILES_ACTIVE atau --spring.profiles.active=..., gunakan @Profile untuk bean kondisional dan @ConfigurationProperties untuk konfigurasi terstruktur. Simpan rahasia sebagai env var yang disuntikkan platform, bukan di repository — mengikuti aturan 12-Factor Config yang menjamin satu artefak build berjalan di semua lingkungan hanya dengan env yang berbeda. Terakhir, waspadai jebakan: secret yang bocor lewat git history, default rahasia yang berbahaya, dan profile yang tidak pernah di-set.

Lanjut membaca

← Back to technical articles