Seluruh ritual section ini — volume berlabel, log terkorelasi, backup malaman, rilis tujuh langkah — berjalan di atas satu mesin bernama srv-toko yang selama ini kita perlakukan seperti sudah ada sejak awal. Chapter ini membongkar cara mesin itu dibangun dari nol: empat langkah runbook yang dikerjakan Budi pada hari pertama VPS kosong itu disewa, langkah yang sama yang dipakai ulang oleh langkah pertama runbook pemulihan pada Chapter Backup dan Recovery saat server pengganti dibutuhkan. Urutannya sengaja keras: server dikerahkan sebelum ada satu pun layanan dipasang, Docker diinstal dari repositori resmi sebelum satu pun image ditarik, kode dan rahasia turun sebelum stack dinyalakan, dan verifikasi HTTPS menjadi gerbang terakhir sebelum toko dinyatakan hidup. Setelah runbook tuntas, kehidupan sesungguhnya dimulai — server produksi bukan proyek sekali jadi melainkan kebun yang disiangi rutin, dan empat topik pemeliharaan di paruh kedua chapter adalah kalender penyiangannya.

Runbook Deploy dalam Empat Langkah #

Langkah 1 — Hardening Sebelum Ada Layanan #

VPS baru menjawab ssh [email protected] dengan akun root telanjang — keadaan yang tidak boleh bertahan satu jam pun di internet publik. Tiga gerakan pertama menciptakan pengguna manusia, melarang kebiasaan root, dan menyalakan tembok api:

bash
$ ssh [email protected]
$ adduser budi
$ usermod -aG sudo budi
$ ufw allow OpenSSH
$ ufw allow 80/tcp
$ ufw allow 443/tcp
$ ufw enable

Output yang Dihasilkan (dipotong):

text
Adding user `budi' ...
Adding new group `budi' (1000) ...
Creating home directory `/home/budi' ...
New password:
Retype new password:
passwd: password updated successfully
...
Command may disrupt existing ssh connections. Proceed with the operation (y|n)? y
Firewall is active and enabled on system startup

Kebijakan bawaan ufw menolak seluruh koneksi masuk (deny incoming) dan hanya tiga pintu yang dibuka: SSH untuk operator, 80 untuk redirect dan tantangan ACME, 443 untuk toko itu sendiri. Tabel statusnya menjadi dokumen kontrak tembok:

bash
$ sudo ufw status verbose

Output yang Dihasilkan:

text
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing)
New profiles: skip

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
OpenSSH (v6)               ALLOW IN    Anywhere (v6)
80/tcp (v6)                ALLOW IN    Anywhere (v6)
443/tcp (v6)               ALLOW IN    Anywhere (v6)

Enam baris izin itu tampak kokoh — dan justru di sinilah kejutan terbesar Docker menunggu:

Pemetaan loopback itu memang dipasang pada db produksi toko, dan keadaan sebenarnya dari seluruh pintu host selalu bisa diverifikasi dari sisi kernel:

bash
$ sudo ss -tlnp | grep docker-proxy

Output yang Dihasilkan:

text
LISTEN 0  4096   0.0.0.0:80       0.0.0.0:*    users:(("docker-proxy",pid=1841,fd=7))
LISTEN 0  4096   0.0.0.0:443      0.0.0.0:*    users:(("docker-proxy",pid=1857,fd=7))
LISTEN 0  4096   127.0.0.1:5432   0.0.0.0:*    users:(("docker-proxy",pid=1902,fd=7))

Tiga baris pendengar docker-proxy tersebut adalah seluruh permukaan host yang menyentuh container: dua baris 0.0.0.0 adalah pintu publik proxy yang memang disengaja, dan satu baris 127.0.0.1:5432 adalah pemetaan debug db yang terkurung di loopback. Tidak ada baris keempat yang mendengarkan 0.0.0.0 — kalau baris seperti itu muncul suatu hari, itulah alarm kebocoran yang pertama.

Langkah 2 — Instalasi Docker Engine #

Dengan pengguna budi dan tembok api berdiri, Docker Engine dipasang dari repositori resmi — urutan yang sama persis dengan Chapter Instalasi Docker di Linux, dari membersihkan paket bentrok sampai lima paket inti:

bash
$ sudo apt-get remove docker.io docker-\
compose docker-doc podman-docker containerd runc
$ sudo apt-get update
$ sudo apt-get install ca-certificates curl
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL "GANTI-DENGAN-URL-KUNCI-GPG" -o /etc/apt/keyrings/docker.asc
$ sudo chmod a+r /etc/apt/keyrings/docker.asc
$ sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<'EOF'
Types: deb
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
URIs: GANTI-DENGAN-URL-REPOSITORI
Suites: noble
Components: stable
EOF
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io \
    docker-buildx-plugin docker-\
compose-plugin

Alamat kunci GPG dan repositori disalin apa adanya dari halaman instalasi resmi di bagian Referensi — domain download.docker.com dengan jalur /linux/ubuntu. Bedah lengkap tiap paket dan alasan tiap langkah sudah kita kerjakan di chapter asalnya; yang berbeda di VPS hanyalah niatnya — instalasi ini adalah fondasi produksi, bukan latihan laptop. Dua gerakan penutup melengkapi instalasi: budi dimasukkan ke grup docker agar bekerja tanpa sudo di setiap perintah selanjutnya, dan daemon membuktikan dirinya:

bash
$ sudo usermod -aG docker budi
$ exit
$ ssh [email protected]
$ docker run hello-world

Output yang Dihasilkan (dipotong):

text
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.

Sebelum satu pun image produksi turun, dua keputusan konfigurasi dari section ini dipasang sekali untuk selamanya: disk data /dev/vdb1 terpasang di /data lewat fstab, dan daemon.json membawa data-root terpisah beserta driver log local dengan rotasinya — resep lengkap keduanya milik Chapter Persistent Storage Production dan Logging Production. Satu restart dan dua baris docker info menjadi tanda tangan bahwa keduanya aktif:

bash
$ sudo systemctl restart docker
$ docker info | grep -E "Docker Root Dir|Logging Driver"

Output yang Dihasilkan:

text
 Docker Root Dir: /data/docker
 Logging Driver: local

Langkah 3 — Kode, Konfigurasi, dan Secrets #

Mesin siap; giliran isi toko yang turun. Repositori berisi seluruh cara membangun stack — compose.yaml, Dockerfile, nginx.conf, skrip backup — sehingga deploy di VPS adalah urusan git clone, bukan urutan salin-tempel manual:

bash
$ sudo mkdir -p /srv/toko && sudo chown budi:budi /srv/toko
$ git clone https://git.example.com/toko/toko.git /srv/toko
$ cd /srv/toko
$ git checkout v1.5.0
$ cp env/produksi.env.example env/produksi.env
$ vi env/produksi.env
$ vault kv get -field=password secret/toko/db > secrets/db_password.txt
$ chmod 600 secrets/db_password.txt
$ ls -l secrets/

Output yang Dihasilkan:

text
total 4.0K
-rw------- 1 budi budi 12 Okt 24 11:02 db_password.txt

Mari kita telaah urutan turunnya berkas-berkas itu:

  1. git checkout v1.5.0 — deploy menempel pada tag beranotasi, bukan cabang apa adanya; tag yang sama yang dicatat CHANGELOG dan menandai titik rollback.
  2. env/produksi.env dari templat — berkas non-rahasia berisi alamat, tingkat log, dan jumlah worker; salin dari templat lalu sesuaikan nilainya, persis disiplin Chapter Konfigurasi Production.
  3. secrets/db_password.txt dari vault — kata sandi database tidak pernah hidup di git; vault menuliskannya sekali ke berkas, dan chmod 600 mengunci baca-tulisnya hanya pada pemiliknya — pola compose secrets yang kita bangun di Chapter Secrets dan Environment.
  4. Sertifikat TLS menyusul sebelum stack menyala — penerbitan pertama lewat certbot DNS challenge dan loop perpanjangan otomatisnya adalah ritual utuh Chapter Reverse Proxy dan TLS; hasilnya berkas fullchain dan privkey yang ditunggu konfigurasi proxy.

Dua volume produksi diciptakan dengan label sebelum container pertama menyala, agar siklus hidup data tidak pernah sekalipun menjadi milik Compose:

bash
$ docker volume create --label env=prod toko_db-data
$ docker volume create --label env=prod toko_uploads

Output yang Dihasilkan:

text
toko_db-data
toko_uploads

Langkah 4 — Deploy dan Verifikasi #

bash
$ docker login registry.example.com
$ docker compose pull
$ docker compose up -d

Output yang Dihasilkan (dipotong):

text
...
[+] Running 5/5
 ✔ Container toko-db-1       Healthy
 ✔ Container toko-cache-1    Started
 ✔ Container toko-backend-1  Started
 ✔ Container toko-frontend-1 Started
 ✔ Container toko-proxy-1    Started

Urutan pada keluaran itu bukan kebetulan konyol: db menempuh gerbang depends_on dengan condition: service_healthy lebih dulu — probe pg_isready harus menjawab sebelum backend lahir — lalu cache, backend, frontend, dan proxy menyusul sesuai rantai ketergantungan yang kita bangun di Chapter Healthcheck dan Restart Policy. Gerbang verifikasi terakhir adalah pintu yang akan dipakai pelanggan:

bash
$ curl -fsSkI https://localhost

Output yang Dihasilkan:

text
HTTP/2 200 
server: nginx/1.29.4
date: Sat, 24 Oct 2026 04:35:12 GMT
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31536000; includeSubDomains

Flag -f membuat curl gagal pada jawaban 4xx/5xx, -s meredam progress bar, -S tetap menampilkan galat, -I mengirim HEAD, dan -k melewati verifikasi sertifikat karena localhost bukan nama domain yang tertera di sertifikat toko. Dua baris terakhir adalah bukti berlapis: HTTP/2 200 menandakan terminasi TLS dan protokol modern bekerja dari Chapter Reverse Proxy dan TLS, dan header strict-transport-security menandakan HSTS aktif — browser akan menolak versi tanpa enkripsi untuk kunjungan berikutnya.

Pemeliharaan Berkala #

Upgrade Paket dan Restart Daemon #

Paket OS dan Docker Engine menua, dan suatu hari restart daemon menjadi keharusan — entah karena apt-get upgrade mengganti paket docker-ce, entah karena konfigurasi daemon.json berubah. Momen itu adalah ujian sesungguhnya dari restart policy yang kita pasang di seluruh service:

bash
$ sudo apt-get update && sudo apt-get upgrade
$ sudo systemctl restart docker
$ docker compose ps

Output yang Dihasilkan:

text
NAME             IMAGE                                        COMMAND                  SERVICE   CREATED      STATUS                        PORTS
toko-proxy-1     nginx:1.29-alpine                            "/docker-entrypoint.…"   proxy     3 weeks ago  Up About a minute             0.0.0.0:80->80/tcp, ...
toko-frontend-1  registry.example.com/toko/frontend:1.4.0      "/docker-entrypoint.…"   frontend  3 weeks ago  Up About a minute             80/tcp
toko-backend-1   registry.example.com/toko/backend:1.5.0       "docker-entrypoint.s…"   backend   3 weeks ago  Up About a minute (healthy)   3000/tcp
toko-db-1        postgres:18-alpine                            "docker-entrypoint.s…"   db        3 weeks ago  Up About a minute (healthy)   127.0.0.1:5432->5432/tcp
toko-cache-1     redis:7-alpine                                "docker-entrypoint.s…"   cache     3 weeks ago  Up About a minute             6379/tcp

Lima container yang CREATED tiga minggu lalu kini Up About a minute — restart policy unless-stopped mengembalikan masing-masing begitu daemon kembali hidup, persis perilaku yang kita bedah pada Chapter Restart Policy: always dan unless-stopped sama-sama membangkitkan container setelah restart daemon; unless-stopped hanya melewatkan container yang sebelumnya dihentikan manual. Restart daemon tidak menghapus container, network, apalagi volume — seluruh keadaan hidup di data-root dan dideklarasikan ulang oleh compose.yaml. Ritualnya tinggal menjaga jendela waktu: lakukan di jam sepi, karena toko memang turu selama daemon berganti.

Update Terjadwal, Bukan Otomatis #

Godaan setiap operator: biarkan mesin memperbarui dirinya sendiri lewat Watchtower — container yang mengamati registry dan menarik image baru diam-diam di tengah malam. Untuk produksi toko, keputusannya tegas menolak:

fail2ban untuk Pintu SSH #

Tembok ufw membatasi pintu, tetapi pintu SSH tetap boleh diketuk siapa saja — dan log auth.log VPS publik selalu berisi rentetan percobaan sandi dari seluruh dunia. fail2ban membaca log itu dan memblokir alamat yang terlalu rajin gagal:

bash
$ sudo apt-get install fail2ban
$ sudo tee /etc/fail2ban/jail.local <<'EOF'
[sshd]
maxretry = 5
bantime  = 1h
EOF
$ sudo systemctl enable --now fail2ban
$ sudo fail2ban-client status sshd

Output yang Dihasilkan:

text
Status for the jail: sshd
|- Filter
|  |- Currently failed: 1
|  |- Total failed:     17
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 0
   |- Total banned:     3
   `- Banned IP list:   

Tiga baris kuncinya: lima percobaan gagal (maxretry) dari satu alamat memicu blokir satu jam (bantime); Total failed: 17 adalah latar belakang normal internet yang menggores pintu setiap server publik; dan Total banned: 3 mencatat bahwa pagar itu benar-benar bekerja. Berkas jail.local sengaja tidak menyentuh konfigurasi bawaan — lokal menimpa, bawaan tetap utuh untuk diperbarui paket.

Monitoring Minimum #

Dua alat pemantauan yang layak hidup di VPS tanpa menjadi proyek baru. Yang pertama adalah skrip penjaga ambang disk — peringatan 80% yang dijanjikan Chapter Persistent Storage Production, kini dengan mekanisme kirimnya:

bash
#!/usr/bin/env bash
set -euo pipefail

AMBANG=80
TARGET=/data
PCENT=$(df --output=pcent "$TARGET" | tail -n 1 | tr -dc '0-9')

if [ "$PCENT" -ge "$AMBANG" ]; then
  curl -fsS -d "srv-toko: disk $TARGET terpakai ${PCENT}% (ambang ${AMBANG}%)" \
    https://ntfy.example.com/toko-alert > /dev/null
fi

Berkas /srv/toko/cek-disk.sh itu dibaca dari atas ke bawah dalam satu napas: df --output=pcent /data mencetak kolom pemakaian persen beserta tajuknya, tail -n 1 membuang tajuk, tr -dc '0-9' menyisakan angkanya, dan pembanding $PCENT -ge 80 mengirim satu notifikasi push ke ponsel Budi — lewat layanan generik seperti ntfy — hanya pada hari disk melewati ambang. Setengah jam sekali menjadi iramanya:

bash
$ sudo crontab -e

Baris berikut masuk ke berkas crontab:

text
*/30 * * * * /srv/toko/cek-disk.sh

Alat kedua berjalan mingguan dari tangan manusia: docker stats --no-stream memotret seluruh stack dalam satu tabel delapan kolom — data yang dibaca dengan mata telanjang justru paling cepat menunjukkan tren:

bash
$ docker stats --no-stream

Output yang Dihasilkan:

text
CONTAINER ID   NAME             CPU %   MEM USAGE / LIMIT   MEM %   NET I/O           BLOCK I/O       PIDS
6f3e9d1a2b7c   toko-proxy-1     0.31%   13.9MiB / 512MiB   2.72%   214MB / 118MB     0B / 0B         3
9a4c2e8b6d1f   toko-frontend-1  0.12%   8.4MiB / 256MiB    3.28%   96.2MB / 89.1MB   0B / 0B         2
5d8f2a6c4b9e   toko-backend-1   0.84%   96.7MiB / 512MiB   18.9%  188MB / 74.2MB     0B / 0B        11
3c7e1a9d5f2b   toko-db-1        3.10%   212MiB / 1GiB      20.7%   102MB / 214MB     41.2MB / 88.5MB 18
8b2d6f4a0c9e   toko-cache-1     0.22%   7.1MiB / 256MiB    2.77%   74.8MB / 61.3MB   0B / 0B         4

Kolom MEM USAGE / LIMIT mengonfirmasi bahwa resource limit dari Chapter CPU dan Memory Limit benar-benar terpasang di produksi — db bernapas dalam anggaran 1GiB dan memakai seperlimanya; pemakaian yang minggu demi minggu merambat ke atas adalah diskursus kapasitas sebelum menjadi insiden. Seluruh kalender perawatan kini terangkum dalam satu tabel:

RitmeRitual
Harianbaca notifikasi cek-disk.sh; sepintas docker compose ps dan sapaan log terakhir
Mingguandocker stats --no-stream dan docker system df; fail2ban-client status sshd; pastikan tujuh malam baris backup hadir di log
Bulananapt-get upgrade dan restart daemon di jendela tenang; rilis terjadwal dengan tujuh langkah; audit checklist produksi

Server hidup, stack berjalan, kalender pemeliharaan tergantung di dinding — namun “rasanya sudah aman” bukanlah status yang bisa diaudit. Sebelum section ini kita tutup, satu chapter tersisa yang mengubah seluruh keputusan sepanjang perjalanan ini menjadi daftar periksa yang bisa dieksekusi dari terminal: Checklist Production.

Referensi #