
Setup HTTPS dan SSL Certificate untuk Server Produksi: Rutinitas yang Saya Pakai di Semua VPS
Panduan praktis setup HTTPS di server produksi dengan Certbot dan Nginx: dari instalasi, auto-renewal, hingga kesalahan sertifikat yang paling sering saya temui saat audit.
Minggu lama saya diminta tolong cek sebuah VPS milik temat yang tiba-tiba tampil warning "Not Secure" di browser. Ternyata sertifikat Let's Encrypt-nya sudah kedaluwarsa 12 hari — padahal dia yakin certbot sudah terpasang dan renewal berjalan otomatis. Masalahnya: timer systemd untuk renewal tidak pernah aktif karena dia mematikan snapd saat hardening server. Insiden seperti ini bukan pertama kali saya lihat, dan hampir semuanya bukan karena setup HTTPS-nya susah — tapi karena detail kecil yang terlewat. Artikel ini adalah rutinitas lengkap yang sekarang saya pakai setiap kali menyiapkan server produksi baru.
Kenapa Saya Selalu Mulai dari Nginx, Bukan dari Sertifikat
Kesalahan klasik pemula: pasang certbot dulu, baru mikirin web server. Urutan ini bikin certbot --nginx sering gagal karena challenge HTTP-01 butuh port 80 yang terbuka dan sebuah server block yang merespons. Alur yang saya pakai kebalikannya:
- Arahkan DNS
A recorddomain ke IP server, tunggu propagasi (biasanya hitungan menit, tapi saya cek dulu dengandig +short domain.com). - Pasang Nginx, buat server block sementara yang serve HTTP di port 80.
- Baru jalankan Certbot untuk issue sertifikat.
sudo apt update && sudo apt install -y nginx
sudo ufw allow 'Nginx Full'
Perhatikan bahwa saya allow Nginx Full (port 80 + 443), bukan Nginx HTTPS saja. Port 80 harus tetap terbuka karena renewal HTTP-01 challenge membutuhkannya — ini salah satu detail yang paling sering bikin renewal gagal diam-diam setelah orang terlalu agresif menutup firewall.
Issue Sertifikat dengan Certbot

Dengan Nginx dan server block HTTP yang sudah jalan, issuance-nya cuma satu perintah:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.contoh.com -d contoh.com
Yang saya suka dari plugin --nginx: dia otomatis menulis konfigurasi redirect dan blok ssl_certificate ke server block yang ada. Tapi saya tidak pernah percaya hasil otomatis begitu saja — selalu saya buka /etc/nginx/sites-available/ dan rapikan jadi seperti ini:
server {
listen 443 ssl http2;
server_name api.contoh.com;
ssl_certificate /etc/letsencrypt/live/api.contoh.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.contoh.com/privkey.pem;
# Konfigurasi TLS yang saya pakai di semua server produksi
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name api.contoh.com;
return 301 https://$host$request_uri;
}
Dua hal yang sering dilupakan orang saat aplikasi berada di belakang reverse proxy: header X-Forwarded-Proto harus diteruskan, dan aplikasi (Express/Fastify/NestJS) harus di-setup untuk trust proxy — kalau tidak, aplikasi tidak tahu request datang lewat HTTPS dan bisa salah generate URL. Saya pernah debugging sesi login yang selalu gagal reset password gara-gara ini: link yang dikirim via email jadi http:// padahal semuanya sudah HTTPS.
Auto-Renewal: Bagian yang Paling Sering Bocor
Sertifikat Let's Encrypt berlaku 90 hari. certbot terpasang via apt biasanya sudah membawa timer systemd untuk renewal otomatis, tapi saya tidak pernah berasumsi itu berjalan. Dua perintah ini wajib saya jalankan di server baru:
sudo systemctl list-timers | grep certbot
sudo certbot renew --dry-run
--dry-run itu bukan formalitas. Dia akan menemukan masalah seperti port 80 yang keblokir firewall, atau DNS yang salah, tanpa benar-benar menghitung rate limit Let's Encrypt. Kalau --dry-run hijau, saya baru yakin.
Untuk reload Nginx setelah renewal otomatis, saya buat hook kecil:
echo -e '#!/bin/bash
nginx -s reload' | sudo tee /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh
Kasus teman saya di pembuka artikel — timer renewal mati karena snapd dimatikan — terselesaikan dengan memasang Certbot dari apt (yang pakai systemd timer, bukan snap) dan verifikasi ulang dengan list-timers. Pelanannya: renewal HTTPS adalah infrastruktur yang butuh monitoring, bukan fitur "set and forget". Minimal, pasang uptime check yang memverifikasi expiry date sertifikat, bukan cuma status 200.
Kasus Multi-Domain Dinamis: Ketika Certbot Per-VPS Tidak Cukup
Kalau kamu hanya serve beberapa domain sendiri, setup di atas cukup. Tapi kalau produkmu SaaS yang memberi subdomain atau custom domain per tenant — seperti yang saya kerjakan sehari-hari — pendekatannya berbeda. Kamu tidak bisa jalankan certbot --nginx interaktif untuk ribuan domain. Di situ saya pakai DNS-01 challenge plus issuance terprogram, arsitektur yang juga dipakai Etsy untuk custom domain di produk Pattern mereka — mereka membahas detail operasionalnya di tulisan engineering Etsy, termasuk kenapa pendekatan per-request issuance tidak scale (How Etsy Manages HTTPS and SSL Certificates for Custom Domains on Pattern). Pelajaran praktis yang saya adopsi dari sana: pisahkan issuance (lambat, rate-limited) dari serving (harus instan), dan simpan sertifikat di satu tempat yang bisa di-cache oleh edge.
Ini juga relevant dengan topik multi-tenant yang pernah saya bahas di artikel tentang isolasi data multi-tenant dengan Row-Level Security vs schema-per-tenant — custom domain per tenant adalah tetangga dekat masalah isolasi itu, dan keduanya paling enak didesain bersamaan dari awal.
Sisi Klien: Kesalahan yang Sering Disalahartikan sebagai "Server Down"

Tidak semua masalah HTTPS ada di servermu. Error klasik di sisi klien adalah SSLHandshakeException: PKIX path building failed di aplikasi Java/JVM — ini terjadi karena truststore JVM tidak mengenali sertifikatmu, biasanya karena sertifikat intermediate tidak terkirim lengkap oleh server. Diskusi panjang di Stack Overflow tentang resolving SSLHandshakeException PKIX path building failed menyoroti dua akar masalah: chain yang tidak lengkap di server, atau truststore klien yang outdated.
Poin pentingnya: hampir selalu masalahnya chain yang tidak lengkap, bukan JVM yang rusak. Sertifikat Let's Encrypt butuh intermediate chain, dan fullchain.pem (bukan cert.pem) memang harus dipakai di konfigurasi Nginx — persis seperti di config saya di atas. Cara verifikasi cepat:
openssl s_client -connect api.contoh.com:443 -servername api.contoh.com | grep -E 'Verify return code|s:'
Kalau Verify return code: 0 (ok) dan chain lengkap, masalahnya di sisi klien. Kalau chain terputus, perbaiki servermu dulu sebelum menyalahkan JVM.
Checklist yang Saya Jalankan di Setiap Server Baru
Biar bisa langsung dipakai, ini versi ringkas yang saya jalankan berurutan:
dig +short domain.com— pastikan DNS sudah mengarah ke server.- Pasang Nginx +
ufw allow 'Nginx Full'(port 80 dan 443). certbot --nginx -d domain.com— pakaifullchain.pemdi konfigurasi final.sudo certbot renew --dry-run— wajib hijau sebelum saya anggap selesai.systemctl list-timers | grep certbot— pastikan timer renewal aktif.- Pasang reload hook Nginx di
/etc/letsencrypt/renewal-hooks/post/. - Uptime monitoring yang cek expiry sertifikat, bukan cuma HTTP status.
openssl s_clientuntuk verifikasi chain lengkap dari luar.
Satu catatan trade-off: kalau aplikasimu di belakang load balancer managed (misal di AWS ALB atau Cloudflare), sebagian langkah ini digantikan oleh provider — sertifikat diterbitkan dan diperbarui otomatis di sana, dan tugasmu tinggal memastikan origin connection juga terenkripsi. Jangan biarkan traffic antara load balancer dan aplikasi berjalan plain HTTP hanya karena "udah behind LB anyway" — itu celah yang nyata dan sering terlewat saat audit. Selengkapnya soal mengelola kredensial dan konfigurasi sensitif untuk server produksi, saya sudah rangkum di artikel tentang praktik secrets management dari dev sampai produksi.
Sumber
- How Etsy Manages HTTPS and SSL Certificates for Custom Domains on Pattern — referensi arsitektur issuance sertifikat skala besar untuk custom domain multi-tenant.
- Resolving SSLHandshakeException: PKIX path building failed — pembahasan komprehensif penyebab error handshake TLS di sisi klien JVM dan chain sertifikat yang tidak lengkap.
Tomi Hartanto
Senior Software Engineer
Backend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.
Artikel Terkait
Tomi Hartanto
Senior Software EngineerBackend developer dengan 8+ tahun pengalaman membangun sistem scalable. Menulis tentang arsitektur backend, database, dan DevOps.


