Skip to content

Restart Bukan Deploy: Gerbang Pra-Restart Kontainer API

Adityo Guni Waluyo

Gerbang restart: skrip kecil menolak restart kontainer API saat working tree masih kotor.

Ringkasan

Penulis kaget skor dashboard jadi minus cuma gara-gara restart kontainer. Ternyata compose pakai bind mount, jadi restart malah menjalankan kode setengah jadi yang ada di disk, bukan dari image. Solusinya, skrip restart sekarang ngecek dulu kalau ansible/web bersih via git status, nolak jalan kalau masih kotor.

Modul skor di ansible/web/ baru saja saya bongkar setengah jadi ketika saya memutuskan me-restart kontainer API server-old-api-1. Sekadar biar layanannya segar. Beberapa detik kemudian dashboard health menampilkan ring skor -/100 di semua metrik.

Tebakan pertama saya: image-nya rusak. Nalar waktu itu sederhana. Kontainer kurestart, bukan dibangun ulang, jadi kode yang jalan mestinya sama dengan sebelumnya.

Nalar itu meleset karena satu detail: service API di compose dijalankan dengan bind mount dari root repo. Yang disajikan ke publik bukan hasil build di dalam image, melainkan working tree di host, apa adanya [1]. Refactor setengah jadi saya ikut ter-deploy oleh satu perintah restart yang saya anggap sepele.

Restart itu stop-start, bukan deploy

Perintah restart kontainer hanya menghentikan lalu menyalakan lagi kontainer yang sama: kirim sinyal SIGTERM dulu, lanjut SIGKILL kalau melewati batas waktu [3]. Tidak ada image baru yang ditarik, tidak ada layer yang dibangun ulang. uvicorn memuat kode dari disk saat proses mulai, dan karena path aplikasi adalah mount, pujian untuk siapa pun yang sudah menduga: yang di-load ulang adalah working tree saya saat itu juga [4].

Jadi restart dan deploy itu dua operasi beda. Di setup tanpa image, restart justru memegang kekuatan deploy: dia mengeksekusi apa pun yang kebetulan ada di disk, termasuk file yang belum pernah masuk ke commit mana pun.

Gerbang dua puluh baris di restart_api.sh

Perbaikannya saya taruh di depan pintu, bukan di atas kertas. Inti dari scripts/restart_api.sh:

#!/usr/bin/env bash
# Guarded api restart: refuses when ansible/web/ is dirty.
# Why: docker-compose bind-mounts ./ into server-old-api-1 — uvicorn serves the
# WORKING TREE, so a restart with uncommitted ansible/web/ code ships stale
# code live (incident 2026-09-18: score rings went '–/100').
# Usage: scripts/restart_api.sh [--check]   (--check = guard only, no restart)
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
DIRTY=$(git -C "$REPO" status --porcelain -- ansible/web)
if [ -n "$DIRTY" ]; then
  echo "REFUSED: ansible/web/ is dirty — commit or stash first:" >&2
  echo "$DIRTY" >&2
  exit 1
fi
if [ "${1:-}" = "--check" ]; then
  echo "GUARD OK: ansible/web/ clean at HEAD $(git -C "$REPO" rev-parse --short HEAD)"
  exit 0
fi
docker restart server-old-api-1

Probe-nya sengaja memakai format --porcelain=v1: outputnya dijamin stabil antar versi Git dan tidak terpengaruh konfigurasi user, jadi nyaman diparse oleh skrip [2]. Output kosong berarti bersih. Ada isinya, kontainer menolak jalan: skrip mencetak daftar file bermasalah lalu keluar dengan exit 1.

Flag --check disediakan buat cron atau probe CI: menjalankan guard saja tanpa menyentuh kontainer. Kalau guard lolos, baris terakhir menyalakan ulang server-old-api-1, dan skrip menutup dengan hint verifikasi health_score lewat curl.

Celah yang masih tersisa

Gerbang ini mempersempit celah, bukan menutupnya. File bisa berubah di selang waktu antara probe lolos dan proses baru benar-benar start. Tree yang bersih pun belum tentu tree yang sudah lolos tes; bersih artinya satu hal: isinya persis commit terakhir, termasuk file untrack yang kebetulan belum dihapus.

Dua pilihan kecil bikin guard ini tetap bisa diandalkan. Probe git berjalan di bawah set -euo pipefail, jadi kalau perintah statusnya gagal, skrip ikut berhenti; gagal punya arti yang beda dengan bersih. Scope pengecekan juga sengaja hanya ansible/web, path yang memang dilayani; bagian lain dari repo boleh saja kotor saat restart.

Pola ini satu keluarga dengan playbook reboot yang menolak jalan saat lock apt dipegang: beda mekanisme, sama prinsipnya. Operasi yang memutasi keadaan layanan pantas bergerbang dulu. Kalau kontainer bisa mengirim kode yang belum pernah saya commit, saya tidak punya proses deploy; saya punya lotre.

Sources

[1] Bind mounts, Docker Docs

[2] git-status, Git docs

[3] docker container restart, Docker Docs

[4] Uvicorn deployment docs

Artikel terkait