Skip to content
Blog DevOps

Bagaimana Saya Development di Lokal dengan Domain dan HTTPS Sungguhan

By Romi Fadli, S.Kom. 22 min read Environments & Deployment
On this page

Bertahun-tahun environment lokal dan production saya tak pernah sepakat soal nama aplikasi yang sedang dikerjakan. Di production namanya https://tnromy.com. Di laptop, http://localhost:9210. Tiap selisih huruf antara dua string itu cepat atau lambat berubah jadi sesi debug — nyaris selalu setelah deploy, nyaris selalu sambil dikejar waktu, dan nyaris selalu di komponen yang tak pernah terpikir untuk diperiksa.

Jalan keluarnya ternyata bukan tool pintar, melainkan satu keputusan: berhenti membuat tiruan production di lokal, mulai menyalinnya apa adanya. Laptop saya kini melayani domain asli dengan sertifikat asli, dan config nginx-nya — web server yang berdiri di depan aplikasi, menerima tiap request lalu meneruskannya — sama persis dengan milik server. Kebetulan semua itu saya kerjakan di atas Linux, Let's Encrypt, nginx, dan Docker — bukan karena metodenya mensyaratkan begitu, tapi karena itulah tool yang memang sudah berjalan di sini. Yang akan saya uraikan adalah cara menata dua environment supaya berhenti berselisih, lengkap dengan harganya.

Pendahuluan

Setiap environment development sesungguhnya sedang mengklaim sesuatu tentang production. Klaimnya kira-kira: kalau code berperilaku begini di sini, begitu pula nanti di sana. Klaim itu tak pernah sepenuhnya benar. Pertanyaan yang menarik bukan apakah ada jurang, melainkan di mana letak jurangnya dan kapan tagihannya datang. Jurang soal ukuran mesin atau volume data akan mengumumkan diri sendiri begitu beban naik — terlambat, tapi nyaring. Jurang soal nama aplikasi lain perkara. Ia bungkam sepanjang pengujian lokal, lolos code review tanpa cacat karena memang tak ada baris yang salah di diff, lalu ambruk pada request pertama setelah deploy — di integrasi yang tak terlintas di pikiran siapa pun, umumnya milik pihak ketiga yang tak punya alasan untuk berbaik hati.

Justru waktunya itulah yang membuat perbedaan penamaan jadi mahal, jauh melampaui ukurannya. Bug-nya tersingkap di saat terburuk, oleh orang yang paling tak siap, dengan tools paling sedikit; production bukan tempat nyaman untuk memasang debugger. Dan begitu ketahuan, perbaikannya biasanya sepele — penghinaan tersendiri: satu jam panik hanya untuk menemukan bahwa callback URL yang terdaftar salah host. Mengerjakan persiapannya di depan berarti membayar ongkos kecil, kasatmata, sekali saja, supaya ongkos besar yang tak terlihat dan datang di saat paling buruk itu tak pernah tiba.

A. Kepingan yang terlibat, sekilas

Empat gagasan memikul hampir seluruh beban tulisan ini, dan ada baiknya disebut terang-terangan sebelum argumennya bersandar ke sana.

Domain adalah nama mesin yang bisa dibaca manusia, semisal tnromy.com. Komputer tak merutekan paket ke nama; yang dirutekan adalah alamat berupa angka. Penerjemah di antara keduanya adalah DNS, Domain Name System — semacam buku alamat tersebar yang ditanyai lewat jaringan untuk mengubah nama menjadi alamat. Saat browser diminta membuka https://tnromy.com, lookup DNS berlangsung lebih dulu sebelum koneksi mana pun terbuka, dan jawabannyalah yang menentukan mesin mana yang menerima request.

Hosts file adalah tombol pembatal untuk proses tadi. Wujudnya file text biasa berisi daftar alamat beserta nama yang mesti dijawabnya, dan sistem operasi membacanya sebelum bertanya ke DNS. Maka entri di sana bukan jawaban penantang, melainkan jawaban terakhir: lookup berhenti di situ, tak ada query yang keluar dari mesin, dan catatan publik tak pernah disentuh. Urutan baca itulah seluruh mekanismenya — mekanisme tua, mapan, dan tersedia di semua sistem operasi desktop.

Name resolution memang menyambungkan kita ke satu mesin, tapi tak membuktikan mesin itu berhak atas nama tersebut. Tak ada apa pun di DNS yang mencegah penyerang di jaringan yang sama menjawab lookup dengan alamat miliknya sendiri. Penangkalnya adalah certificate TLS, penyebab munculnya huruf s pada HTTPS. Certificate mengikat nama domain ke sebuah public key, dan selama handshake server membuktikan bahwa private key pasangannya ada padanya. Yang dijamin certificate itu sempit, dan perlu disebut persis: bukan bahwa situsnya aman, jujur, atau dikelola baik, melainkan hanya bahwa pihak yang menjawab atas nama ini terbukti memegang key yang diterbitkan untuk nama tersebut. Segala makna lain yang dibaca orang dari ikon gembok cuma tafsiran.

Jaminan sebaik sumbernya, dan di situlah Certificate Authority berperan. CA adalah lembaga yang memverifikasi bahwa seseorang benar menguasai suatu domain, lalu menandatangani certificate untuknya. Browser dan sistem operasi datang membawa daftar bawaan berisi CA yang mereka percayai — beberapa ratus nama, dikurasi, diaudit, sesekali dicoret ketika ada yang bertingkah — dan certificate yang ditandatangani CA dari daftar itu diterima tanpa suara. Inilah sebabnya certificate self-signed memicu peringatan satu halaman penuh sementara certificate Let's Encrypt lewat mulus. Secara kriptografis keduanya objek sejenis dan memberi enkripsi yang setara. Bedanya, certificate self-signed menjamin dirinya sendiri, dan klaim yang buktinya hanya dirinya sendiri persis seperti yang akan disodorkan penyadap. Browser sedang tak mengeluhkan kriptografi lemah; ia mengeluh karena belum ada pihak terpercaya yang menyatakan key ini milik nama ini. Obatnya: tambahkan si penanda tangan ke daftar otoritas yang diterima pihak yang mesti percaya. Seperti akan terlihat di bagian berikut, kesulitannya bukan melakukan itu sekali — melainkan banyaknya pihak yang ternyata perlu diberi tahu.

Jawaban lazim buat kebutuhan HTTPS di lokal adalah certificate self-signed, dan untuk aplikasi satu proses jawaban itu sudah lebih dari cukup. Buat certificate-nya, suruh browser mempercayainya, gembok pun muncul. Bertahun-tahun saya memakai cara itu tanpa keluhan berarti.

Keluhannya baru datang bersama container kedua. Certificate self-signed cuma dipercaya pihak yang sudah disuruh mempercayainya, sedangkan container adalah sistem operasi baru yang belum punya pendapat apa pun tentang CA pribadi saya. Jadi CA tadi harus dipasang ke dalam image, atau di-bind-mount, atau disuntikkan saat startup — lalu diulang untuk image berikutnya, dan berikutnya lagi, masing-masing dengan base distribution sendiri, path trust store sendiri, dan pendiriannya sendiri soal di mana certificate semestinya tinggal.

  • Image Debian dan Ubuntu menuntut CA diletakkan di /usr/local/share/ca-certificates, disusul update-ca-certificates.

  • Image Alpine perlu paket ca-certificates dipasang lebih dulu, sebab bawaannya memang kosong.

  • Runtime bahasa kerap mengabaikan trust store sistem sepenuhnya. Node membaca NODE_EXTRA_CA_CERTS; requests di Python membaca REQUESTS_CA_BUNDLE; Java menuntut certificate-nya diimpor ke keystore.

Satu per satu tak ada yang sukar. Masalahnya, semua itu berlaku per container, terulang tiap image dibangun ulang, dan sepenuhnya sekali pakai. Di production certificate-nya datang dari Let's Encrypt dan seluruh container tadi mempercayainya tanpa diminta — artinya semua konfigurasi barusan dikerjakan semata untuk kelak dibuang. Itulah definisi kerja yang semestinya tak pernah dimulai.

Alternatifnya jelas: lewati saja HTTPS di lokal, kerjakan semuanya di atas http://localhost. Cara ini menukar ongkos pemeliharaan dengan ongkos kebenaran, dan yang kedua lebih mahal karena tertunda sekaligus tak kasatmata. Banyak sekali perilaku yang ternyata terikat pada origin, bukan pada logika aplikasi yang berdiri di belakangnya.

  • Cookie bertanda Secure sama sekali tak di-set di atas HTTP polos, dan semantik SameSite berbeda antara domain asli dan localhost.

  • Provider OAuth2 mencocokkan redirect URI secara harfiah. Flow yang mulus dengan URI localhost terdaftar tak membuktikan apa pun soal URI production.

  • Key reCAPTCHA diterbitkan per domain dan menolak memvalidasi origin yang tak terdaftar.

  • Presigned URL object storage ditandatangani terhadap host tertentu. Ganti host-nya, batal pula signature-nya.

Jadi jalan pertama menagih pemeliharaan tanpa henti, jalan kedua menagih kebenaran. Saya menolak keduanya, dan itu berarti kedua environment harus berhenti berbeda sejak awal.

B. Tiga hambatan yang menghalangi

Jadi tujuannya gampang dirumuskan: membuat mesin lokal menyahut pada domain yang sama dengan production, lewat sertifikat yang sama, memakai config nginx yang sama — tanpa merawat CA sendiri, dan tanpa mengorbankan ketepatan yang jadi ongkos HTTP polos. Apa pun yang berbeda di antara kedua environment itu adalah kode yang belum teruji, dan sasarannya menyusutkan selisih itu sampai nyaris nol.

Gampang dirumuskan, canggung dibangun. Ada tiga hambatan yang berdiri antara tujuan tadi dan mesin yang benar-benar berperilaku begitu, dan masing-masing mementahkan cara yang paling pertama terpikirkan.

Pertama, domain itu sudah menjadi milik tempat lain. DNS publik menjawabnya dengan alamat server di data center, dan jawaban itu benar — memang begitu tugasnya. Mengarahkan domain sungguhan ke sebuah laptop bukan permintaan yang sanggup dilayani DNS, dan jalan pintas yang menggoda justru lebih buruk dari kelihatannya. Menyunting record DNS publik membuat situsnya rusak untuk semua orang. Menjalankan DNS server lokal berarti tiap mesin di jaringan harus diberi tahu. Yang dibutuhkan adalah pembatal yang berlaku untuk satu mesin saja, langsung berdampak, dan tak menyentuh record publiknya.

Kedua, container sama sekali tak mewarisi semua itu. Container adalah sistem operasi tersendiri dengan /etc/hosts dan resolver-nya sendiri, jadi pembatal yang tadi membereskan mesin host tak kelihatan dari dalamnya. Container yang menanyakan domain itu keluar ke DNS publik lalu mendarat di production — dan inilah kegagalan yang paling berbahaya, justru karena ia berhasil. Perbaikan yang paling terpikirkan adalah menuliskan alamat loopback ke dalam container, dan itu keliru dengan cara yang menghasilkan error membingungkan: di dalam container, 127.0.0.1 berarti container itu sendiri.

Ketiga, name resolution yang berhasil bukan berarti berhak atas nama itu. Setelah domainnya mengarah ke mesin ini, mesin tersebut masih harus membuktikan bahwa dialah domain itu, dan yang sanggup membuktikannya cuma sertifikat yang diterbitkan untuknya. Sertifikat self-signed sudah ditolak dengan alasan di atas — ia harus diajarkan ke tiap container, tiap kali image dibangun ulang. Memakai sertifikat asli memunculkan kesulitannya sendiri: ia memang dirancang kedaluwarsa tiap sembilan puluh hari, jadi susunan apa pun yang melibatkan penyalinan file adalah susunan yang diam-diam menjadi basi.

Masing-masing dibahas di seksi tersendiri di bawah, dan urutannya adalah urutan wajib menyelesaikannya. Name resolution-nya harus benar dulu sebelum ada yang bisa dibuktikan tentangnya, container harus sepakat dengan host sebelum lalu lintasnya layak diuji, dan sertifikatnya baru berguna ketika keduanya sudah benar.

Metode

Metodenya terdiri dari dua bagian yang berdiri sendiri-sendiri. Bagian pertama membuat domain mengarah ke mesin saya; bagian kedua membuat mesin saya sanggup membuktikan bahwa ia memang domain itu. Tak satu pun menuntut perubahan pada aplikasi, dan — di sinilah letak intinya — keduanya dihapus saat deploy, bukan ditukar dengan versi lain.

Kalau config lokal berbeda dari config production, selisihnya adalah code yang belum teruji. Sasarannya bukan config yang mirip, melainkan config yang sama.

A. Mengarahkan domain asli ke mesin lokal

Inilah jawaban atas hambatan pertama: membuat domain milik data center berarti mesin ini, dan hanya di mesin ini.

DNS publik menyatakan tnromy.com berada di server di dalam data center. Di lokal nama itu harus berarti laptop saya, dan hosts file mengerjakan persis itu: dibaca sebelum DNS, sehingga entri di dalamnya menang tanpa satu paket pun keluar dari mesin. Di Linux dan macOS file-nya /etc/hosts, dan isinya sangat biasa.

ini
# /etc/hosts — dibaca sebelum DNS publik
127.0.0.1 tnromy.com
127.0.0.1 ca.example-ca.id
127.0.0.1 admin.example-net.com
127.0.0.1 app.example-client.co.id

Itu baris sungguhan dari mesin saya, dan polanya sederhana: tiap project yang sedang aktif dikerjakan mendapat jatah satu baris. File yang sama juga menampung entri menuju alamat VPN dan staging, dan di sanalah keuntungan keduanya — memindahkan domain antara lokal, staging, dan production tinggal urusan baris mana yang dikomentari, sementara aplikasinya tak pernah tahu ada yang bergeser.

Di Windows mekanismenya serupa, cuma detailnya gemar menjegal. File-nya ada di %SystemRoot%\System32\drivers\etc\hosts, yang pada instalasi bawaan berarti C:\Windows\System32\drivers\etc\hosts. Tiga hal lagi perlu diketahui sebelum menyuntingnya.

  1. File-nya tak berekstensi. Notepad akan diam-diam menyimpannya sebagai hosts.txt kalau opsi All files tak dipilih di dialog simpan, dan hasilnya file mati yang tak memunculkan error maupun efek apa pun.

  2. Menyuntingnya menuntut hak administrator, dan editornya wajib dijalankan lewat Run as administrator sebelum file-nya dibuka. Tak ada jalan menaikkan hak di tengah jalan; yang muncul justru kegagalan simpan di ujung pekerjaan.

  3. Windows memuat entri hosts ke cache resolver DNS, jadi suntingan belum berlaku sampai cache itu dibersihkan. Hasil negatif ikut tersimpan, artinya hostname yang sempat dicoba sebelum penyuntingan akan tetap rusak sampai di-flush.

bash
# Windows (prompt elevated) — wajib setelah menyunting hosts
ipconfig /flushdns

# macOS — hosts tetap di /etc/hosts seperti Linux, tapi flush dua cache
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Linux kasus paling gampang: kebanyakan konfigurasi desktop membaca hosts file langsung tanpa lapisan cache di depannya, jadi suntingan berlaku seketika. Kalau ada resolver lokal seperti systemd-resolved atau dnsmasq, perlakuannya sama dengan Windows.

B. Mengajarkan hal serupa kepada container

Inilah jawaban atas hambatan kedua: container memegang resolver sendiri, jadi pembatal milik host harus diulang di dalamnya — dengan alamat yang berbeda.

Hosts file mengatur mesin host, dan container tak membacanya. Container punya /etc/hosts serta resolver sendiri, sehingga permintaan atas tnromy.com dari dalam sana melesat ke DNS publik dan mendarat di server production — diam-diam, sukses, dan keliru. Selama development inilah jenis kegagalan paling buruk, justru karena tampak berhasil.

Satu blok tujuan menerima dua jalur melengkung terpisah: jalur atas dari bentuk jendela browser, jalur bawah dari tiga blok container. Warnanya berbeda dan tak pernah bersinggungan sebelum tiba.
Nama host yang sama jatuh ke dua alamat lokal berbeda, tergantung siapa yang bertanya. Server block yang hanya mendengarkan salah satunya membuat yang lain menggantung sampai timeout, padahal semua komponen tampak beres.

Docker Compose — tool yang menyalakan sekumpulan container dari satu file konfigurasi — menyelesaikannya lewat extra_hosts, yang menuliskan entri ke hosts file milik container saat startup. Kehalusannya terletak pada alamat tujuan: 127.0.0.1 di dalam container menunjuk container itu sendiri, bukan host. Sasaran yang benar adalah alamat gateway network Docker, tempat host bisa dijangkau dari dalam.

yaml
services:
  backend:
    # Presigned URL ditandatangani terhadap origin publik https://tnromy.com.
    # Dari dalam container nama itu resolve ke loopback, jadi arahkan ke
    # gateway network Docker, tempat nginx milik host sedang listen.
    extra_hosts:
      - "tnromy.com:10.93.0.1"

Gateway bukan nilai tetap dan sebaiknya dibaca langsung dari network-nya, jangan ditebak. Compose umumnya membuat network per project, sehingga nilainya berbeda antar mesin maupun antar project di mesin yang sama.

bash
# Baca gateway-nya, jangan diasumsikan
docker network inspect tnromy.com.net \
  --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
# 10.93.0.1

Di project lain nilainya saya simpan dalam variabel berdefault masuk akal, supaya ketergantungannya terdokumentasi sendiri dan tetap hidup ketika repo di-clone ke mesin yang Docker-nya memilih subnet berbeda. Komentar di sana mencatat gejala, bukan perbaikan, sebab gejala itulah yang akan saya ketikkan ke kotak pencarian lain kali.

yaml
    extra_hosts:
      # Tanpa ini container resolve ke alamat publik Cloudflare dan menerima
      # tantangan 403, bukan menjangkau stack lokal.
      - "sso.example-client.go.id:${GATEWAY_IP:-10.0.42.1}"

Ada konsekuensi di sisi nginx yang gampang terlewat, karena semuanya terlihat benar justru ketika keliru. Request dari browser tiba lewat loopback; request dari container tiba lewat gateway Docker. Keduanya alamat lokal yang berlainan, dan server block yang cuma diikat ke 127.0.0.1 tak akan cocok dengan yang kedua. nginx toh tetap menjawab — jatuh ke default server, yang di mesin berisi banyak project berarti project lain sama sekali — lalu request itu di-proxy entah ke mana sampai timeout. Mengikat listener ke semua interface melenyapkan seluruh kelas masalah ini.

nginx
server {
    # Tanpa alamat: mengikat semua interface, sehingga loopback maupun gateway
    # Docker sama-sama sampai ke block ini. Mengikat hanya 127.0.0.1 akan
    # melayani browser tapi diam-diam melempar trafik container ke default server.
    listen 443 ssl;
    server_name tnromy.com;

    ssl_certificate     /etc/letsencrypt/live/tnromy.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tnromy.com/privkey.pem;
}

C. Memasang certificate production di path production

Inilah jawaban atas hambatan ketiga: membuktikan hak atas nama itu, dengan cara yang tetap hidup meski sertifikatnya kedaluwarsa tiap sembilan puluh hari.

Setelah name resolution-nya mengarah ke lokal, mesin itu masih berutang bukti bahwa ia memang nama tersebut, dan satu-satunya bukti sah adalah certificate yang diterbitkan untuknya. Maka certificate itulah yang saya pakai: fullchain.pem dan privkey.pem saya tarik dari server production, tempat Let's Encrypt memperbaruinya, lalu saya pasang di lokal pada path yang sama persis dengan di production — /etc/letsencrypt/live/<domain>/.

Menyamakan path adalah inti seluruh gagasan ini, bukan sekadar kerapian. Kalau di lokal certificate-nya tinggal di tempat lain, config nginx lokal pun terpaksa berbeda, dan config yang berbeda berarti config yang belum teruji. Dengan path yang sejajar, file yang jalan di laptop saya adalah file yang jalan di server, dan berpindah di antara keduanya tak menguji apa pun yang baru.

Semua ini saya simpan dalam dua script kecil. Script backup membaca direktori live milik satu domain lalu menulis kedua file-nya ke folder yang dinamai menurut domain tersebut. Script inilah yang berjalan di server.

bash
# Di server production: ambil certificate yang sedang aktif
SRC="/etc/letsencrypt/live/$DOMAIN"
DEST="cert.$DOMAIN"

mkdir -p "$DEST"
cat "$SRC/fullchain.pem" > "$DEST/fullchain.pem"
cat "$SRC/privkey.pem"   > "$DEST/privkey.pem"

Script restore-nya lebih menarik, sebab ia sama sekali tidak menyalin. Direktori live dibuat bila belum ada, isinya dibersihkan, lalu path live tadi di-symlink ke folder backup.

bash
# Di laptop: tautkan path live ke folder backup
LIVE="/etc/letsencrypt/live/$DOMAIN"

sudo mkdir -p "$LIVE"
sudo rm -f "$LIVE/fullchain.pem" "$LIVE/privkey.pem"
sudo ln -s "$ABS_BACKUP/fullchain.pem" "$LIVE/fullchain.pem"
sudo ln -s "$ABS_BACKUP/privkey.pem"   "$LIVE/privkey.pem"

Memilih symlink ketimbang salinan tampak sepele, tapi berbuah tiap sembilan puluh hari. Certificate Let's Encrypt memang sengaja berumur pendek, dan ketika pasangan baru datang saya cukup menimpa isi folder backup — folder yang tinggal di dalam project, bersebelahan dengan file compose yang memakainya — lalu path live tetap bekerja tanpa perlu menjalankan ulang apa pun dan tanpa akses root. Salinan justru meninggalkan dua versi yang berlainan tanpa petunjuk mana yang terbaru.

Hasil dan Pembahasan

A. Ongkos sebenarnya dari ketiga pendekatan

Disandingkan, selisih ketiganya sebetulnya bukan soal HTTPS, melainkan soal seberapa banyak bagian setup lokal yang harus dibongkar sebelum sistem yang sama bisa berjalan di production.

DimensiCA self-signedTanpa HTTPSCertificate production
Setup trust per containerWajib di setiap imageTak adaTak ada
Redirect URI OAuth2Bisa diuji, tapi bukan URI asliPerlu registrasi localhostIdentik dengan production
reCAPTCHAGagal cek domainGagal cek domainJalan dengan key production
Presigned URLHost tak cocokHost tak cocokSignature cocok
Kesamaan config nginxPath certificate berbedaTak ada blok TLSIdentik
Yang dibongkar saat deployCA di setiap imageSemua asumsi originDua entri hosts
Tempat key boleh beradaHanya key lokalTak adaKey production, hanya di mesin yang saya kendalikan
Tiga pendekatan HTTPS di lokal, dibandingkan dari sisi tuntutannya selama development dan apa saja yang mesti dibongkar sebelum deploy.

Dua baris terakhir memikul seluruh argumennya. Certificate production adalah satu-satunya pendekatan yang konfigurasi khusus-lokalnya cuma beberapa baris di hosts file dan satu blok extra_hosts — keduanya sekadar absen di production, bukan digantikan versi lain. Ia pula satu-satunya pendekatan yang butuh aturan tentang di mana key-nya boleh berada, jadi aturan itu saya tetapkan sebelum memakainya.

B. Apa yang berhenti rusak

Kelas bug yang lenyap adalah bug yang baru menampakkan diri setelah deploy. Presigned URL contoh paling gamblang: backend menandatanganinya terhadap origin publik, sehingga di lokal URL itu entah ditandatangani terhadap localhost dan tak teruji, atau ditandatangani benar tapi tak terjangkau. Begitu domainnya mengarah ke lokal, URL tadi ditandatangani persis seperti production menandatanganinya dan diambil persis seperti production mengambilnya — code penandatanganannya benar-benar dijalankan, bukan didekati.

OAuth2 menempuh nasib serupa. Redirect URI yang terdaftar di provider kini adalah URI yang sungguh-sungguh dipakai laptop saya, sehingga error pada flow benar-benar error pada flow, bukan artefak dari URI localhost kedua yang hidup semata demi development. reCAPTCHA tak lagi butuh test key. Scope cookie dan flag Secure akhirnya bisa diuji sama sekali.

Yang tidak saya dapatkan adalah environment production. Datanya lain, bebannya lain, mesinnya lain, dan tak satu pun dari itu teruji di sini. Metode ini hanya melenyapkan kategori yang lebih sempit sekaligus lebih menjengkelkan: bug yang seluruh sebabnya cuma karena aplikasinya menyandang nama berbeda di lokal dan di production.

C. Di mana cara ini tidak cocok

Aturannya ketat. Metode ini hanya saya pakai untuk certificate milik sistem yang saya operasikan sendiri, dan hanya di satu mesin yang saya kendalikan dengan enkripsi disk penuh — mesin yang perlindungannya memang sudah harus sepadan dengan semua isinya. Untuk key yang bukan milik saya, tidak pernah, dan di mesin bersama atau pinjaman pun tidak pernah.

Di dalam tim, naluri menyalin key production ke laptop tiap developer adalah naluri yang keliru, padahal bentuk yang lebih baik sudah tersedia: terbitkan certificate untuk subdomain development, lalu pasang di path yang sama lewat mekanisme yang sama. Manfaat kesejajaran path tetap utuh, sebab yang penting adalah config-nya tak perlu tahu sedang berada di mesin mana — bukan soal certificate-nya harus benar-benar milik production. Bagian hosts dan extra_hosts tak terpengaruh, dan tetap layak dikerjakan meski berdiri sendiri.

Metode ini juga mengandaikan ada reverse proxy di depan aplikasi: web server, dalam kasus saya nginx, yang menerima tiap request dari luar, menangani certificate-nya, lalu meneruskan request itu ke aplikasi di belakangnya. Begitulah cara saya menjalankan segalanya, jadi bagi saya tak ada tambahan ongkos; tapi setup yang aplikasinya menerminasi TLS sendiri mesti menyambungkan path certificate ke config aplikasi. Prinsipnya selamat melewati perubahan itu: taruh certificate di tempat production menaruhnya, supaya config-nya tak perlu bercabang.

Simpulan

Kedua bagian ini nyaris sepele bila berdiri sendiri-sendiri. Satu entri di hosts file hanyalah satu baris. Menyalin dua file .pem hanyalah satu perintah. Yang membuat gabungannya sepadan adalah bahwa keduanya memungkinkan config nginx lokal menjadi config nginx production — tanpa percabangan, tanpa path khusus environment. Dan config yang tak punya cabang khusus environment mustahil berperilaku berbeda antar environment.

Detail yang paling ingin saya wariskan: request dari container tidak tiba dengan cara yang sama seperti request dari browser. Selebihnya dalam metode ini serba deklaratif dan kasatmata; yang satu itu berupa timeout tanpa pesan error yang berguna, tepat ketika hosts file, alamat gateway, certificate, dan server yang berjalan sama-sama terbukti benar. Tak ada yang rusak — request-nya cuma dijawab oleh server block yang tak sedang diperhatikan siapa pun.

Tak satu pun dari semua ini bergantung pada tool yang saya pakai. Domainnya diarahkan ke lokal lewat hosts file karena file itulah yang dibaca mesin saya lebih dulu; di stack lain perannya bisa jatuh ke dnsmasq, resolver internal, atau satu record DNS yang menunjuk alamat privat. Yang terbawa adalah urutannya, bukan implementasinya: buat aplikasinya menyandang nama yang sama di kedua environment, biarkan nama itu dibuktikan oleh benda yang sama dengan pembuktinya di production, dan simpan file-nya di path yang dipakai production, supaya tak ada config yang perlu bertanya sedang berjalan di mesin mana.

Satu gagasan terakhir, untuk siapa pun yang stack-nya sama sekali tak beririsan dengan stack saya. Utamakan tambahan khusus lokal yang dihapus dalam perjalanan ke production, ketimbang yang ditukar dengan versi lain, sebab penukaran berarti satu config kedua yang tak pernah dijalankan siapa pun sampai hari deploy. Entri yang dihapus saat deploy mustahil menyimpang dari apa pun. Percabangan yang menukar satu nilai dengan nilai lain diam-diam berubah menjadi baris paling jarang teruji di seluruh sistem.

Dan ketika sistemnya pindah ke production, tak ada satu pun isi tulisan ini yang ikut serta. Entri hosts tinggal di laptop, extra_hosts dihapus dari file compose, dan certificate di /etc/letsencrypt/live/ memang sudah ada di sana, diperbarui oleh proses Let's Encrypt yang sama dengan yang menghasilkan salinan yang selama ini saya pinjam. Itulah ukuran yang saya pakai untuk menilai teknik environment lokal mana pun: bukan seberapa mirip ia dengan production, melainkan seberapa sedikit bagiannya yang harus dibongkar untuk sampai ke sana.

Istilah

Hosts file
File text lokal yang memetakan nama ke alamat, dibaca sebelum DNS, sehingga entri di dalamnya mengalahkan jawaban publik tanpa satu pun permintaan jaringan.
Alamat gateway
Alamat router virtual sebuah network Docker, jalan untuk menjangkau mesin host dari dalam container di network tersebut.
Server block
Satuan konfigurasi nginx yang dipilih berdasarkan alamat tempat request tiba dan hostname yang dimintanya. Meleset di salah satunya membuat request jatuh ke default server.
Presigned URL
URL object storage yang membawa signature hasil perhitungan atas host dan path tertentu, dan batal begitu host tempat ia diambil berbeda dari host saat ia ditandatangani.
Kesejajaran path
Menjaga satu file tetap berada di path absolut yang sama di setiap environment, agar config yang merujuknya tak perlu variasi khusus per environment.

Technologies covered

  • Linux
  • Docker
  • nginx
  • DNS

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A browser window sends one orange path into a single web server, which routes it on to the orange staging environment; dashed paths to the blue development and green production environments stay inactive

Dua perintah yang menjaga tiga environment tetap terpisah di satu mesin dan satu web server: satu menetapkan alamat environment dan menyelaraskan setiap file yang menyimpannya, merender nginx dari template dan menolak bila bentrok; satu lagi mengarahkan browser ke satu environment.

11 min read Read more →
Three stages in a row — development, staging and production — with an image passing from development to staging, configuration promoted from staging to production, staging fed with real data restored from a production backup, and production marked done once its core function is proven

Release Workflow: Development to Production adalah prosedur yang ditempuh tiap perubahan saya dari laptop sampai production — langkah mana yang saya otomatiskan, mana yang sengaja saya serahkan ke manusia, kenapa tiap langkah berada di tempatnya, cara saya mendiagnosis rilis yang gagal, dan cara pulihnya.

9 min read Read more →

Related Articles

A single file card holding six sections in a fixed order — Config, Helpers, Reporter, Steps, Summary, Entrypoint — with arguments, a config file and a prompt flowing in from the left and exit codes 0, 1, 2 and 130 flowing out to the right

Bentuk yang saya berikan pada tool kecil: satu file, satu tugas, seksi berurutan tetap, kontrak exit code di bagian atas, dan tiga pintu masuk input — ditambah varian Bash untuk generator, dan kapan tool sudah terlalu besar untuknya.

8 min read Read more →