Shell script adalah file berisi perintah yang dijalankan bash dari atas ke bawah, persis seperti kalau kamu mengetiknya satu per satu. Di server, script paling penting adalah script deploy: ambil kode baru, pasang dependensi, migrasi database, restart app. Bayangin checklist pilot sebelum terbang: kalau satu cek gagal, pesawatnya gak boleh lepas landas. Script deploy yang baik juga begitu: begitu satu langkah gagal, dia berhenti dan bilang gagal. Dasar script (shebang, variabel, if, $?) ada di pelajaran Shell Scripts Dasar, track Terminal.
🧪 Tebakan cepat:
deploy.shmilik catatan-api ditulis tanpaset -e. Saat deploy, GitHub gak bisa dihubungi, jadigit pullgagal. Baris terakhir yang tercetak apa, dan exit code script-nya berapa? Tebak dulu, jawabannya ada di bagian "Exit code: 0 berhasil, selain 0 gagal".
deploy.sh untuk catatan-api
#!/bin/bash
set -euo pipefail
APP_DIR="/srv/catatan-api"
BRANCH="${1:-main}"
echo "Deploying catatan-api from branch $BRANCH"
cd "$APP_DIR"
git pull --ff-only origin "$BRANCH"
npm ci --omit=dev
node migrate.js | tee -a deploy.log
sudo systemctl restart catatan-api
echo "Deploy complete!"
#!/bin/bash(shebang) memberi tahu sistem bahwa file ini dijalankan bash. Di Ubuntu,/bin/bashdan/usr/bin/bashadalah file yang sama.#!/usr/bin/env bashmencari bash diPATH, berguna di mesin yang menaruh bash di tempat lain.set -euo pipefail: tiga pengaman, dibahas di bawah.APP_DIR="/srv/catatan-api": variabel ditulis tanpa spasi di sekitar=."${1:-main}"mengambil argumen pertama, ataumainkalau kosong, jadi./deploy.sh fitur-loginmen-deploy branchfitur-login.git pull --ff-onlycuma mau maju lurus. Kalau kode di server pernah diubah langsung, dia menolak, bukan membuat merge diam-diam.npm ci --omit=devmemasang dependensi persis sesuaipackage-lock.json, tanpa devDependencies.tee -a deploy.logmenampilkan output migrasi di layar sekaligus menambahkannya kedeploy.log.sudo systemctl restart catatan-api(pelajaran sebelumnya) mengganti proses lama dengan kode baru.
Sekali saja: chmod u+x deploy.sh (pelajaran File System & Permissions), lalu jalankan dengan ./deploy.sh.
Exit code: 0 berhasil, selain 0 gagal
Setiap perintah selesai dengan exit code: 0 artinya berhasil, angka lain artinya gagal. git memakai 128 untuk error fatal. echo $? mencetak exit code perintah terakhir. Ini deploy.sh yang sama, tanpa baris set, saat GitHub gak bisa dihubungi:
deploy@web-1:/srv/catatan-api$ ./deploy.sh
Deploying catatan-api from branch main
ssh: connect to host github.com port 22: Connection timed out
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
added 64 packages, and audited 65 packages in 3s
found 0 vulnerabilities
Tidak ada migrasi baru.
Deploy complete!
deploy@web-1:/srv/catatan-api$ echo $?
0
git pull gagal dengan exit 128, tapi bash cuma lanjut ke baris berikutnya. npm ci memasang ulang kode lama, app di-restart dengan kode lama, lalu script mencetak Deploy complete! dan keluar dengan 0, karena exit code script adalah exit code perintah terakhirnya, echo. Itu jawaban tebakannya. CI atau rekan setim yang membaca exit code-nya mengira deploy berhasil.
set -euo pipefail: tiga pengaman
Dengan set -euo pipefail di baris kedua, kejadian yang sama berhenti di tempat:
deploy@web-1:/srv/catatan-api$ ./deploy.sh
Deploying catatan-api from branch main
ssh: connect to host github.com port 22: Connection timed out
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
deploy@web-1:/srv/catatan-api$ echo $?
128
| Pengaman | Yang ditangkap di deploy.sh | Tanpa pengaman ini |
|---|---|---|
-e |
git pull gagal: script berhenti di baris 9, exit 128 |
jalan terus sampai Deploy complete! |
-u |
salah ketik $BRANH: ./deploy.sh: line 7: BRANH: unbound variable, exit 1 |
$BRANH diam-diam jadi teks kosong |
-o pipefail |
node migrate.js gagal di dalam pipe: berhenti di baris 11, exit 1 |
pipe dianggap sukses karena tee sukses |
- Status sebuah pipe adalah status perintah terakhir-nya. Tanpa
pipefail,node migrate.js | tee -a deploy.logdianggap berhasil walaupun migrasinya gagal, danset -egak punya alasan berhenti. -egak berlaku di dalam kondisiifdan di kiri&&atau||, karena di sana kegagalan memang sedang dicek. Detailnya, plustrapuntuk bersih-bersih, ada di pelajaran Defensive Scripting & trap.
Kutip setiap variabel
Tanpa kutip, bash memecah isi variabel di spasi sebelum menjalankan perintahnya:
LOG="storage/logs/deploy 2026-10-09.log"
touch $LOG # dua file: "storage/logs/deploy" dan "2026-10-09.log"
touch "$LOG" # satu file, persis namanya
Biasakan "$APP_DIR", "$BRANCH", "$file". Kutip dua tetap mengganti variabel dengan isinya, kutip satu ('$APP_DIR') gak.
Loop: tunggu app siap
Setelah restart, app butuh beberapa detik. for menjalankan isinya sekali untuk tiap item:
for i in 1 2 3 4 5; do
if curl -fsS http://127.0.0.1:3000/health > /dev/null; then
echo "Health check OK"
exit 0
fi
sleep 2
done
echo "Health check gagal setelah 5 kali" >&2
exit 1
curl -fkeluar dengan status gagal kalau server menjawab 4xx atau 5xx, jadiifbisa memakainya langsung.- Item loop juga bisa daftar file:
for file in /var/log/*.log; do echo "$file"; doneberjalan sekali untuk tiap file.log. exit 1di akhir membuat script gagal dengan jelas kalau app gak pernah sehat.
Di latihan: membuat script dengan JavaScript
generateDeployScript(config) menyusun teks script dari array baris yang digabung dengan "\n": shebang #!/bin/bash, set -euo pipefail, satu echo dengan nama app dan branch, semua langkah dari config.steps (pakai spread ...config.steps), lalu echo "Deploy complete!".
⚠️ Jebakan yang sering ketemu
- Tanpa
set -e. Script jalan terus setelah langkah gagal dan tetap mencetakDeploy complete!dengan exit 0. - Pipe menelan error.
perintah | tee fileselalu sukses kalauteesukses. Tambahkanset -o pipefail. - Variabel tanpa kutip. Path dengan spasi pecah jadi dua argumen, dan variabel kosong menghilang dari perintah.
sudomeminta password di tengah script. Script yang jalan tanpa terminal (misalnya dari CI) gagal di sana. Izinkan satu perintah itu saja lewatsudo visudo -f /etc/sudoers.d/deployberisideploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart catatan-api.greptanpa hasil di bawahset -e.grepkeluar dengan 1 kalau gak ada yang cocok, dan script berhenti. Kalau "gak ketemu" itu wajar, tulisgrep ... || true.
TL;DR: Script deploy dimulai dengan shebang #!/bin/bash (atau #!/usr/bin/env bash) dan set -euo pipefail: -e berhenti di perintah gagal pertama, -u menolak variabel yang belum diisi, -o pipefail membuat pipe gagal kalau salah satu bagiannya gagal. Exit code 0 berarti berhasil, selain 0 gagal, echo $? menampilkannya, dan tanpa -e script yang gagal tetap keluar dengan 0 dari echo terakhirnya. Variabel ditulis NAMA="nilai" tanpa spasi dan dibaca "$NAMA" berkutip, "${1:-main}" memberi nilai cadangan. for x in ...; do ...; done mengulang untuk tiap item. sudo di script butuh aturan NOPASSWD untuk perintah itu saja.