From bc68c8166e3b37de170e9846a4e6d8449b47f835 Mon Sep 17 00:00:00 2001 From: Fikra Laksana Putra <79677026+Fikralaksana@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:55:34 +0700 Subject: [PATCH 1/3] Update docker-rollout --- docker-rollout | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docker-rollout b/docker-rollout index d6e28bf..e62d30c 100755 --- a/docker-rollout +++ b/docker-rollout @@ -84,12 +84,27 @@ scale() { main() { # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files - if [ -z "$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE")" ]; then + CONTAINERS_JSON=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps -a --format json "$SERVICE") + + # Run the original script when no containers exist + if [ -z "$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE")" ] && [ "$CONTAINERS_JSON" = "[]" ]; then echo "==> Service '$SERVICE' is not running. Starting the service." $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$SERVICE" exit 0 fi + # Run with --no-recreate param conditionally based on container state + echo "$CONTAINERS_JSON" | \ + jq -r '.[] | "\(.ID) \(.Service) \(.State)"' | \ + while read -r id service state; do + echo "==> Service '$SERVICE' is not running. Starting the service. exit, $state" + if [ "$state" == "exited" ]; then + $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach "$service" + fi + $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$service" + exit 0 + done + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files OLD_CONTAINER_IDS_STRING=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | tr '\n' '|' | sed 's/|$//') OLD_CONTAINER_IDS=$(echo "$OLD_CONTAINER_IDS_STRING" | tr '|' ' ') From b77cd4b5b71520700d03f2e706599717ab2959ec Mon Sep 17 00:00:00 2001 From: fikra Date: Tue, 19 May 2026 17:13:23 +0700 Subject: [PATCH 2/3] Replace jq with POSIX-only container state detection Use `docker compose ps --quiet` (running) vs `ps -a --quiet` (all) to detect exited containers without parsing JSON. Drops the jq dependency to keep the script portable to plain POSIX sh, per maintainer feedback. Also removes the `==` bashism and the `exit 0` inside the piped `while read` (which only exited the subshell, never the script). Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-rollout | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docker-rollout b/docker-rollout index e62d30c..a7c7aa8 100755 --- a/docker-rollout +++ b/docker-rollout @@ -84,26 +84,25 @@ scale() { main() { # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files - CONTAINERS_JSON=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps -a --format json "$SERVICE") + ALL_CONTAINER_IDS=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps -a --quiet "$SERVICE") + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files + RUNNING_CONTAINER_IDS=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE") # Run the original script when no containers exist - if [ -z "$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE")" ] && [ "$CONTAINERS_JSON" = "[]" ]; then + if [ -z "$ALL_CONTAINER_IDS" ]; then echo "==> Service '$SERVICE' is not running. Starting the service." + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$SERVICE" exit 0 fi - # Run with --no-recreate param conditionally based on container state - echo "$CONTAINERS_JSON" | \ - jq -r '.[] | "\(.ID) \(.Service) \(.State)"' | \ - while read -r id service state; do - echo "==> Service '$SERVICE' is not running. Starting the service. exit, $state" - if [ "$state" == "exited" ]; then - $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach "$service" - fi - $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$service" + # Containers exist but none are running (all exited) — recreate them + if [ -z "$RUNNING_CONTAINER_IDS" ]; then + echo "==> Service '$SERVICE' has only exited containers. Recreating." + # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files + $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach "$SERVICE" exit 0 - done + fi # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files OLD_CONTAINER_IDS_STRING=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | tr '\n' '|' | sed 's/|$//') From 1b6fb2f08919d25dc61e439aa56a262b16e15d8a Mon Sep 17 00:00:00 2001 From: fikra Date: Tue, 19 May 2026 17:53:03 +0700 Subject: [PATCH 3/3] Replace only stopped replicas; preserve running containers Diff `ps -a --quiet` against `ps --quiet` to identify the specific stopped container IDs, then `docker rm` only those and refill with `compose up --detach --no-recreate`. The running replicas are left untouched, preserving the zero-downtime contract when config has drifted (the common case when rollout is invoked). Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-rollout | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docker-rollout b/docker-rollout index a7c7aa8..3c81ce3 100755 --- a/docker-rollout +++ b/docker-rollout @@ -96,11 +96,22 @@ main() { exit 0 fi - # Containers exist but none are running (all exited) — recreate them - if [ -z "$RUNNING_CONTAINER_IDS" ]; then - echo "==> Service '$SERVICE' has only exited containers. Recreating." + # Identify stopped replicas (present in ps -a but not in ps --quiet) + STOPPED_CONTAINER_IDS="" + for id in $ALL_CONTAINER_IDS; do + if ! echo "$RUNNING_CONTAINER_IDS" | grep -qx "$id"; then + STOPPED_CONTAINER_IDS="$STOPPED_CONTAINER_IDS $id" + fi + done + + # Remove only the stopped replicas and let compose create fresh ones; running containers + # are preserved via --no-recreate so the zero-downtime contract isn't broken if config drifted. + if [ -n "$STOPPED_CONTAINER_IDS" ]; then + echo "==> Service '$SERVICE' has stopped containers:$STOPPED_CONTAINER_IDS. Replacing only those." + # shellcheck disable=SC2086 # DOCKER_ARGS and STOPPED_CONTAINER_IDS must be unquoted to allow multiple arguments + docker $DOCKER_ARGS rm $STOPPED_CONTAINER_IDS # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files - $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach "$SERVICE" + $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$SERVICE" exit 0 fi