Rantai toko kini terhitung penuh dari ujung ke ujung: commit memicu test, build berjalan di atas cache registry, gerbang scan menutup jalan bagi image bermasalah, tag dihitung dari keadaan Git, dan rilis termirror ke registry privat dengan digest yang identik. Namun mata rantai paling akhir masih dikerjakan dengan tangan: begitu tab Actions berwarna hijau, Budi membuka terminal, masuk SSH ke srv-toko, dan mengetik ritual rilis dari Chapter Update dan Rollback — git checkout, docker compose pull, docker compose up -d — lima perintah yang sama setiap kali, dikerjakan manusia yang lelahnya tidak sama setiap kali. Chapter penutup section ini menghapus ketikan terakhir itu: deploy yang didorong CI lewat SSH dengan persetujuan manual sebelum naik panggung, smoke test yang menguji jalur pelanggan dari DNS sampai database, dan rollback otomatis yang bangun duluan supaya tidak ada yang perlu bangun tengah malam.
Empat Pola Otomasi Deploy #
Memberi CI hak menyentuh server produksi bisa dikerjakan banyak cara, dan tiap cara menempatkan kendali di tangan yang berbeda. Empat pola berikut adalah pilihan utama di lapangan:
| Pola | Arah instruksi | Kontrol mutu | Kompleksitas | Kapan tepat |
|---|---|---|---|---|
| SSH push-based | CI mendorong perintah ke server | gerbang penuh di CI; persetujuan environment | rendah | satu-dua server, tim kecil — pilihan toko |
| Pull-based (Watchtower) | server menarik image baru sendiri | tanpa gerbang, tanpa verifikasi | sangat rendah | homelab dan eksperimen — bukan produksi |
| Webhook receiver | server menyediakan endpoint; CI memanggilnya | sedang; butuh service tambahan di server | sedang | beberapa server tanpa akses SSH dari CI |
| GitOps env repo | reconciler menyelaraskan server dengan repo | kebijakan tertulis di repo; audit = riwayat Git | tinggi | banyak lingkungan; landasan Kubernetes |
Toko memilih pola pertama dengan alasan yang terbaca langsung dari tabelnya: server produksi toko hanya satu, sehingga kompleksitas rendah bukan pengorbanan, dan seluruh gerbang mutu yang kita bangun sepanjang section ini — test, scan, tag, mirror — tetap berdiri di CI sebagai satu-satunya pintu menuju server. Pola keempat adalah arah pertumbuhan jangka panjang yang akan kita semaikan di akhir chapter; dua pola di tengah kita lewati dengan alasan yang akan dijelaskan pada bagian jalan yang tidak ditempuh.
Pola SSH: CI yang Mendorong #
Workflow deploy terpicu oleh tag rilis, lalu merangkai enam job berurutan. Job build dan scan persis mengikuti pola Chapter *Image Scanning di CI/CD`; tiga job setelahnya adalah isi baru chapter ini:
name: deploy
on:
push:
tags: ["v*"]
jobs:
mirror:
needs: scan
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Tiru image ke registry privat
run: |
VERSI="${GITHUB_REF_NAME#v}"
docker buildx imagetools create \
-t "registry.example.com/toko/backend:${VERSI}" \
"ghcr.io/budi/toko:${VERSI}"
deploy:
needs: mirror
environment: production
runs-on: ubuntu-latest
steps:
- name: Rilis lewat SSH ke srv-toko
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /srv/toko
git fetch --tags
git checkout ${{ github.ref_name }}
docker compose pull
docker compose up -d
docker image prune -f
verify:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: Smoke test jalur publik
run: |
for i in 1 2 3 4 5; do
curl -fsS -o /dev/null https://toko.example.com/api/produk.json
echo "percobaan ${i}/5: 200 OK"
sleep 5
done
rollback:
needs: [deploy, verify]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Mundur ke tag sebelumnya
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /srv/toko
RILIS_LAMA=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^)
echo "mundur ke ${RILIS_LAMA}"
git checkout "${RILIS_LAMA}"
docker compose pull
docker compose up -dMari kita telaah anatomi job-job baru tersebut:
- Job mirror berdiri setelah gerbang —
needs: scanmembuat penyalinan ke registry privat hanya terjadi untuk image yang lulus; produsen registry privat tidak pernah menerima barang gagal mutu. - Job deploy membawa
environment: production— satu baris yang mengaktifkan persetujuan manual dan jejak audit, dibedah pada bagian berikutnya. - appleboy/ssh-action@v1 menjalankan script di server — host, user, dan kunci privat dibaca dari secrets; script-nya adalah ritual manual Chapter Update dan Rollback yang sama, hanya saja kini ditulis sekali dan dijalankan identik selamanya.
- Job verify menguji dari luar — smoke test tidak menyentuh server; ia menyusuri jalur yang disusuri pelanggan.
- Job rollback hanya bangun saat kegagalan —
if: failure()membuatnya senyap pada rilis sehat, dan aktif ketika deploy atau verify merah.
Saat deploy 1.6.0 berjalan, log job deploy memperlihatkan ritual yang dulu diketik Budi kini dieksekusi oleh runner:
Output yang Dihasilkan (dipotong):
out: From github.com:budi/toko
out: * [new tag] v1.6.0 -> v1.6.0
out: HEAD is now at d41c8f2 keranjang: batasi jumlah item per pelanggan
out: [+] Pulling 5/5
out: ✔ db Pulled 0.0s
out: ✔ cache Pulled 0.0s
out: ✔ proxy Pulled 0.0s
out: ✔ frontend Pulled 2.1s
out: ✔ backend Pulled 6.4s
out: [+] Running 5/5
out: ✔ Container toko-db-1 Running 0.0s
out: ✔ Container toko-cache-1 Running 0.0s
out: ✔ Container toko-backend-1 Recreated 1.8s
out: ✔ Container toko-frontend-1 Recreated 1.2s
out: ✔ Container toko-proxy-1 Running 0.0s
out: Total reclaimed space: 139.4MBMari kita telaah baris-baris kunci log SSH tersebut:
HEAD is now at d41c8f2—git checkout v1.6.0mengarahkan working tree/srv/tokoke titik rilis persis; compose.yaml yang terbaca adalah compose.yaml saat rilis ditandai, bukan versi terbaru cabang main.- Lima service diperiksa, hanya dua yang membawa layer baru —
db,cache, danproxydijawab up-to-date dalam hitungan nol detik;backenddanfrontendmengunduh perubahan rilis ini — perilaku content-addressable yang kita kenal dari Chapter Update dan Rollback. Recreatedhanya untuk service yang image-nya berganti — database dan cache tidak disentuh; koneksi pool dan isi volume meneruskan hidupnya, dan jeda pergantian hanya hitungan detik.Total reclaimed space: 139.4MB—docker image prune -fmenyapu image dangling yang ditinggalkan container lama; disk server tidak merayap penuh seiring jumlah rilis.
Satu Image, Dua Registry #
Job mirror layak diberi perhatian lebih karena menyelesaikan satu masalah distribusi. Server produksi toko hanya boleh bicara dengan registry.example.com — jaringan internal, kredensial perusahaan, compliance — sementara CI membangun dan memindai di GHCR. Alih-alih membangun ulang image untuk registry kedua (build kedua bisa berbeda dari yang di-scan!), job mirror menyalin manifest dan blob yang sudah teruji:
$ docker buildx imagetools inspect registry.example.com/toko/backend:1.6.0Output yang Dihasilkan (dipotong):
Name: registry.example.com/toko/backend:1.6.0
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:e5b1d8f2a4c60739b1d5e8f2a4c6073b9d1e5f8a2c4b6d0e2f4a6c8b0d2f4a6cDigest pada keluaran itu identik dengan digest image yang diperiksa job scan — bukan kebetulan, melainkan sifat content-addressable yang kita buktikan di Chapter Private Registry: blob yang disalin byte demi byte menghasilkan manifest berdigest sama, di registry mana pun. Artinya, pernyataan “yang berjalan di produksi adalah image yang lulus gerbang” tetap dapat dibuktikan dengan satu angka, tanpa mempercayai proses penyalinan.
Persetujuan Sebelum Panggung #
Baris environment: production pada job deploy bukan dekorasi. Environment adalah fitur GitHub Actions — diperkenalkan pada Chapter Build, Test, dan Publish — yang mengelompokkan deployment ke lingkungan tertentu beserta aturan perlindungannya. Pada environment production, toko mengaktifkan required reviewers: begitu job deploy siap berjalan, GitHub menghentikan pipeline dan menunggu hingga seseorang dari daftar penerima persetujuan menekan tombol Approve. Persetujuan itu adalah titik kontrol manusia terakhir — setelahnya, seluruh rangkaian deploy sampai smoke test berjalan tanpa sentuhan tangan lagi.
Environment juga menyimpan jejak audit yang tidak bisa dipalsukan dari dalam workflow: halaman deployments mencatat siapa yang menyetujui rilis 1.6.0, jam berapa, untuk environment mana, dan dari workflow run yang mana. Ketika sesi post-mortem bertanya “siapa yang melepas 1.6.0 ke produksi?”, jawabannya bukan “mungkin Budi, atau Siti” melainkan satu baris log dengan nama dan waktu. Kontrol semacam ini murah dipasang dan mahal ketika absen.
Smoke Test: Menguji Rantai yang Utuh #
Healthcheck pada compose.yaml menguji container dari dalam; smoke test menguji sesuatu yang lebih besar — jalur lengkap yang dilalui pelanggan. Ketika curl mengetuk toko.example.com/api/produk.json lewat HTTPS, permintaan itu melewati resolusi DNS, jabatan TLS, proxy nginx, resolusi nama service di jaringan Compose, backend, hingga query database yang memuat berkas produk: satu rantai penuh, satu jawaban. Healthcheck yang hijau tidak membuktikan rantai itu utuh; jawaban 200 dari luar membuktikan.
$ for i in 1 2 3 4 5; do curl -fsS -o /dev/null https://toko.example.com/api/produk.json && echo "200 OK"; sleep 5; doneOutput yang Dihasilkan:
200 OK
200 OK
200 OK
200 OK
200 OKLima percobaan dengan jeda lima detik adalah standar toko — pola yang kita wariskan dari ritual verifikasi Chapter Update dan Rollback. Satu jawaban 200 bisa kebetulan; lima jawaban stabil berjarak waktu adalah denyut yang sehat. Flag -f membuat curl keluar gagal untuk jawaban 4xx atau 5xx, dan -sS menjaga keluaran tetap bersih tanpa menyembunyikan galat — keluaran bukan nol berarti job verify merah, dan inilah pemicu barisan berikutnya.
Kabar buruk memang datang pada rilis 1.6.0: percobaan ketiga gagal.
Output yang Dihasilkan (job verify, dipotong):
percobaan 1/5: 200 OK
percobaan 2/5: 200 OK
curl: (56) Recv failure: Connection reset by peer
Error: Process completed with exit code 56.Dua jawaban sehat lalu koneksi diputus ulang — gejala khas container yang bangkit lalu tumbang berulang. Penyebab persisnya menjadi urusan post-mortem keesokan hari (sebuah variabel environment baru yang belum terpasang di server); urusan pipeline lebih sempit: kegagalan itu tidak boleh dibiarkan menggantung sampai pagi. Exit code 56 membuat job verify merah, dan if: failure() pada job rollback akhirnya mendapat gilirannya bekerja.
Rollback Otomatis ke Tag Sebelumnya #
Job rollback menjalankan logika yang dulu hafal di luar kepala Budi: cari tag rilis sebelumnya, arahkan server ke sana, jalankan ritual deploy yang sama. Kuncinya ada pada satu baris git describe:
RILIS_LAMA=$(git describe --tags --abbrev=0 v1.6.0^)Mari kita telaah baris tersebut: v1.6.0^ menunjuk commit tepat sebelum tag v1.6.0, dan git describe --tags --abbrev=0 mencari tag terdekat yang terjangkau dari commit itu — hasilnya v1.5.0, rilis sebelumnya yang sehat. Dari titik itu, tiga perintah sisanya adalah ritual yang sama dengan deploy: git checkout ke titik rilis lama, docker compose pull, docker compose up -d. Log job rollback mencatat kejadiannya:
Output yang Dihasilkan (dipotong):
out: mundur ke v1.5.0
out: Previous HEAD position was d41c8f2 keranjang: batasi jumlah item per pelanggan
out: HEAD is now at b62d914 pesanan: nomor pesanan + trace_id di log
out: [+] Pulling 5/5
out: ✔ backend Pulled 4.8s
out: [+] Running 5/5
out: ✔ Container toko-backend-1 Recreated 1.6s
out: ✔ Container toko-frontend-1 Recreated 1.1s
out: ✔ Container toko-db-1 Running 0.0s
out: ✔ Container toko-proxy-1 Running 0.0sServer kembali melayani dari image 1.5.0 — image yang masih tersimpan di host dari rilis sebelumnya, sehingga arah mundur tidak menunggu jaringan, persis jaminan tiga generasi image dari Chapter Update dan Rollback. Rilis 1.6.0 sendiri tidak dibuang: tag-nya tetap di registry, digest-nya tetap tercatat, dan perbaikannya akan lahir sebagai 1.6.1 dengan variabel environment yang benar — maju-mundur yang tertib, bukan amnesia.
Jalan yang Tidak Ditempuh #
Dua pintu otomasi deploy sengaja tidak dibuka, dan alasan keduanya layak dicatat agar tidak terbuka diam-diam. Pintu pertama adalah jalan pintas yang menggoda setiap orang yang pernah lelah mengetik SSH: membuka daemon Docker server ke jaringan lalu membiarkan CI memanggilnya langsung dengan docker -H tcp://srv-toko:2375. Masalahnya bukan idenya melainkan harganya: daemon yang mendengarkan TCP tanpa TLS dan tanpa autentikasi memberikan siapa pun di jaringan itu seluruh kemampuan daemon — membuat container yang mem-mount direktori root, menjalankan image dari mana pun, setara root penuh di host. Ini bukan metafora melainkan mekanika yang kita buktikan pada Chapter Docker Socket Security.
Pintu kedua adalah pola pull-based dengan Watchtower — container yang memasang socket Docker lalu diam-diam menarik image baru dan mengganti container yang berjalan setiap ada tag bergeser. Kita sudah menolaknya di Chapter Docker di VPS dan alasan penolakannya tidak berubah dengan adanya pipeline: Watchtower bekerja dengan hak setara root di host melalui socket yang dipasang ke dalam containernya, mengganti image berjalan tanpa satu gerbang dari rantai mutu kita — tanpa test hijau, tanpa scan lolos, tanpa persetujuan — dan proyeknya sendiri sudah tidak lagi dikembangkan. Pull-based tanpa verifikasi adalah rollback yang belum tahu dirinya rollback.
Benih GitOps #
Pola SSH toko bekerja karena servernya satu. Bayangkan lintasannya pada sepuluh server dengan tiga lingkungan: masing-masing butuh secrets SSH sendiri, urutan job sendiri, dan jawaban sendiri untuk pertanyaan “server mana yang menjalankan versi apa”. Keluarga pola keempat pada tabel awal — GitOps — lahir tepat dari kelelahan itu: seluruh keadaan yang diinginkan (desired state) ditulis deklaratif pada sebuah repositori environment — compose.yaml, tag image, variabel — dan sebuah reconciler di sisi server yang terus-menerus membaca repo itu lalu menyelaraskan dunia nyata ke arahnya. Perubahan produksi berarti pull request pada repo environment; audit berarti riwayat Git; drift terdeteksi karena reconciler tidak menemukan keadaan dunia sesuai deklarasi.
Alat keluarganya — Argo CD dan Flux — hidup di ekosistem Kubernetes dan akan menyusul di section berikutnya; bagi toko yang masih satu host, pola SSH dengan persetujuan dan smoke test sudah menjawab seluruh kebutuhan hari ini. Yang penting dibawa pulang adalah benih konsepnya: keadaan yang diinginkan ditulis, bukan dieksekusi — karena yang tertulis bisa direview, dan yang bisa direview bisa dipercaya. Konsep desired state inilah yang kelak menjadi bahasa utama seluruh orchestration.
Rantai toko kini tertutup penuh: satu git push memicu test, build yang tercache, gerbang keamanan, tag yang terhitung, mirror yang terverifikasi, persetujuan manusia, deploy, smoke test, dan rollback yang siap bangun duluan. Seluruh section ini membangun kepercayaan itu di atas satu asumsi yang belum pernah kita ganggu: stack produksi hidup di satu host, dijalankan satu docker compose up -d. Asumsi itu kokoh untuk toko hari ini, dan goyah begitu beban tumbuh — ketika satu host tidak lagi cukup, ketika service perlu dijadwalkan lintas mesin, ketika container yang mati harus dilahirkan ulang di tempat lain tanpa menunggu manusia. Batas itulah titik tolak section berikutnya: Chapter Batasan Docker Compose membedah dengan jujur sampai mana compose sanggup berdiri, dan kenapa dunia orchestration lahir dari sisi lain garis itu.
Referensi #
- Using environments for deployment — protection rules, persetujuan manual, dan jejak audit environment.
- Docker Compose CLI reference
— perilaku
pull,up -d, dan statusRecreateddi sisi server. - Docker security — mengapa akses daemon setara akses root, termasuk risiko TCP tanpa TLS.