Mengirim image Docker ke produksi dengan aman dan berulang: pipeline CI/CD dengan GitHub Actions, memisahkan environment development, staging, dan production, strategi zero-downtime deployment, serta rollback yang cepat

Di episode 14 kalian punya image Docker yang siap jalan. Tapi mengirim image ke server dengan ssh + docker run secara manual adalah kebiasaan berbahaya: tidak terdokumentasi, tidak bisa diulang, dan tidak bisa di-audit. Inilah yang diperbaiki CI/CD — pipeline otomatis yang mengetes, membangun, dan men-deploy kode setiap kali berubah.
Mengapa CI/CD penting? Karena deployment manual adalah sumber kegagalan terbesar di tim kecil menengah: "kemarin saya lupa menjalankan satu perintah". Pipeline otomatis menjamin setiap deployment melewati langkah yang sama, ke environment yang tepat, dan bisa di-rollback kapan saja. Episode ini membangun pipeline GitHub Actions lengkap, memisahkan environment, dan menerapkan zero-downtime deployment.
Setiap pipeline didefinisikan sebagai workflow dalam file YAML di .github/workflows/:
name: CI
on:
push:
branches: [main, staging]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.1"
- run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- name: Push ke GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}Perhatikan bagian penting:
on) — workflow berjalan saat push ke main/staging dan saat pull request.build menunggu test lulus (needs: test).GITHUB_TOKEN) dipakai untuk login registry; jangan pernah menulis secret ke dalam file.Note
Image di-tag dengan commit SHA (github.sha) — setiap commit menghasilkan image unik. Ini membuat deployment bisa dilacak balik ke kode yang persis, dan rollback cukup men-deploy tag lama. Prinsip immutable image dari episode 14 terwujud di sini.
Deployment ke production langsung tanpa uji adalah resiko. Pisahkan environment dengan kebijakan berbeda:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy ke staging
run: ./script/deploy.sh staging ghcr.io/${{ github.repository }}:${{ github.sha }}
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy ke production
run: ./script/deploy.sh production ghcr.io/${{ github.repository }}:${{ github.sha }}Di pengaturan GitHub, environment bisa diatur dengan protection rules — misalnya production butuh persetujuan (approved deployments), dan staging harus lulus dulu. Environment juga membawa secrets terpisah: DATABASE_URL staging berbeda dengan production.
Menjalankan docker rm -f lalu docker run baru berarti downtime — user terganggu, payment gagal. Strategi zero-downtime yang paling sederhana adalah rolling update: jalankan container baru, tunggu sehat, baru matikan yang lama.
#!/usr/bin/env bash
set -euo pipefail
IMAGE="$1"
CONTAINER="shop-api"
# 1. Jalankan container baru dengan port baru
docker run -d --name "${CONTAINER}-new" -p 3001:3000 "$IMAGE"
# 2. Tunggu sampai sehat (healthcheck episode 14)
for i in {1..30}; do
if curl -sf http://localhost:3001/health > /dev/null; then
echo "New container healthy"
break
fi
sleep 2
done
# 3. Tukar traffic dan matikan yang lama
docker stop "${CONTAINER}-old" 2>/dev/null || true
docker rename "${CONTAINER}" "${CONTAINER}-old" 2>/dev/null || true
docker rename "${CONTAINER}-new" "${CONTAINER}"
docker start "${CONTAINER}"
docker rm "${CONTAINER}-old" 2>/dev/null || truePrinsipnya: selalu ada container sehat yang melayani traffic. Kalau container baru gagal healthcheck, yang lama tetap hidup — deployment batal tanpa downtime. (Load balancer di episode 21 mengotomatiskan pola ini; di cloud, gunakan managed service.)
Bagian paling berbahaya dari deployment adalah migrasi database — tabel berubah, dan API baru serta API lama harus tetap kompatibel.
# 1. Jalankan migrasi (bukan breaking) dulu
bun run migrate:up
# 2. Baru deploy kode baru
./script/deploy.sh production "$IMAGE"
# 3. Setelah semua instance baru sehat, baru cleanup
bun run migrate:cleanupAturan expand-migrate-contract: tambahkan kolom (expand) → deploy kode yang membaca kolom baru (migrate) → buang kolom lama (contract). Ini membuat migration backward compatible sehingga rollback tidak merusak data.
Warning
Jangan pernah menjalankan migrasi yang menghapus kolom di deployment yang sama dengan kode baru — saat rollback, kode lama membaca kolom yang sudah hilang dan aplikasi rusak. Migrasi breaking harus dipisah jadi beberapa release. Ini penyebab downtime paling umum di dunia nyata.
Deployment yang baik selalu punya jalan keluar. Dua strategi:
${{ github.sha }} disimpan, deploy SHA sebelumnya. Paling sederhana dan selalu bisa.# image lama masih ada di registry
./script/deploy.sh production ghcr.io/devvnull/shop-api:abc123Karena image immutable (episode 14), rollback selalu mungkin — tidak ada "state yang sudah terlanjur berubah di server".
Mengubah server produksi dengan tangan = perubahan tidak terdokumentasi. Semua harus lewat pipeline.
Menulis password langsung di deploy.yml = bocor di git. Selalu pakai GitHub Secrets (episode 17).
Pipeline yang deploy lalu selesai tanpa cek health = bom waktu. Selalu verifikasi setelah deploy: healthcheck + smoke test alur kritis.
CI yang hanya menjalankan test setelah merge di main tidak menangkap apa pun. Jalankan test di setiap pull request — itu inti "continuous".
Tip
Mulai dari yang sederhana: satu workflow ci.yml untuk lint + test di tiap PR, satu workflow deploy.yml untuk build image dan deploy ke staging, dan production dideploy manual-lewat-UI setelah staging disetujui. Jangan langsung membangun CD penuh sebelum staging benar-benar dipakai.
Episode 15 mengirim image ke produksi dengan aman: alur CI/CD, pipeline GitHub Actions dengan image bertag SHA, pemisahan environment staging/production, rolling update zero-downtime, migrasi database yang backward compatible, dan strategi rollback.
Inti yang harus dibawa pulang:
Di episode 16 selanjutnya kita akan naik ke cloud: cloud services backend — compute, managed database, dan storage di AWS, GCP, dan Azure. Sampai jumpa di episode 16!