Skip to content

fix(container): skip redundant deploy when container already runs the same image#71

Merged
bnema merged 4 commits intomainfrom
fix/skip-redundant-deploy
Feb 9, 2026
Merged

fix(container): skip redundant deploy when container already runs the same image#71
bnema merged 4 commits intomainfrom
fix/skip-redundant-deploy

Conversation

@bnema
Copy link
Owner

@bnema bnema commented Feb 9, 2026

Summary

  • Fix double-deploy race during gordon push: the registry's image.pushed event triggers a deploy, and the CLI's explicit POST /deploy triggers a second identical one. The second deploy now detects the container is already running the same image (by Docker image ID) and short-circuits immediately.
  • Add ImageID field to Container domain model and GetImageID() to the runtime interface, populated in both ListContainers and InspectContainer so the check works across restarts.
  • Add 4 tests covering: skip when same image, proceed when different image, proceed when container not running, proceed on GetImageID error (graceful degradation).

Root Cause

When docker push completes, two things happen concurrently:

  1. The registry fires EventImagePushed → event handler calls Deploy()
  2. The CLI calls POST /deploy/<domain> → also calls Deploy()

The domain deploy lock serializes them, but the second deploy was fully redundant — pulling the same image, creating a new container, running readiness checks — doubling the total push time.

Fix

After prepareDeployResources (image is pulled), compare the new image's Docker ID with the existing container's ImageID. If they match and the container is running, return the existing container immediately.

Summary by CodeRabbit

  • New Features

    • Deployment optimization: detect when a running container already uses the target image and skip unnecessary redeploys to reduce downtime and speed deployments.
    • Container metadata now includes image identity so the system can compare running images for smarter decisions.
  • Tests

    • Added comprehensive tests covering redundant-deploy detection, error handling, and edge cases.

Copilot AI review requested due to automatic review settings February 9, 2026 20:42
@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Added image-ID propagation and lookup: Docker runtime exposes GetImageID, Container gained ImageID, service deploy checks image IDs and may skip creating a new container when an identical image is already running; mocks and tests updated accordingly.

Changes

Cohort / File(s) Summary
Runtime & Interface
internal/adapters/out/docker/runtime.go, internal/boundaries/out/runtime.go
Added GetImageID(ctx, imageRef) (string, error) to Docker runtime and ContainerRuntime interface to inspect and return image IDs.
Domain
internal/domain/container.go
Added ImageID string field to Container to carry Docker image identifier.
Mocks
internal/boundaries/out/mocks/mock_container_runtime.go
Added GetImageID mock support: call shadow type, Expecter, Run/Return/RunAndReturn helpers, and behavior wiring.
Service Logic
internal/usecase/container/service.go
Added skipRedundantDeploy and integrated it into Deploy to compare resolved image ID with an existing container and skip creation when identical and running.
Tests
internal/usecase/container/service_test.go
Added tests covering skip-on-matching-image, differing-image behavior, image-inspect errors, non-running existing container replacement, and adjusted assertions for tracking updates.

Sequence Diagram

sequenceDiagram
    participant User
    participant Service
    participant Docker as Docker Runtime
    participant Existing as Existing Container

    User->>Service: Deploy(targetImageRef)
    Service->>Docker: GetImageID(targetImageRef)
    Docker-->>Service: targetImageID / error
    alt targetImageID obtained
        Service->>Existing: Read ImageID & Status
        Existing-->>Service: imageID, status
        alt imageID == targetImageID and status == Running
            Service-->>User: Return existing container (skip creation)
        else
            Service->>Service: Proceed with full create/start/ready flow
        end
    else inspection error
        Service->>Service: Proceed with full create/start/ready flow
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

A rabbit peeks at images in the yard, 🐇
If they match, no need to work so hard.
Skip the create, let the old one run,
Hop home early — the deploy's done. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main objective: skipping redundant deployments when a container already runs the same image, which is the primary change across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/skip-redundant-deploy

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prevents a redundant second deploy during gordon push by detecting when the currently running container is already on the exact same Docker image ID and short-circuiting the deploy. To support that check, it extends the container domain model and runtime interface to surface image IDs, and adds tests for the new behavior.

Changes:

  • Add ImageID to domain.Container and populate it from Docker in both ListContainers and InspectContainer.
  • Extend ContainerRuntime with GetImageID(imageRef) and implement it in the Docker runtime adapter (plus mocks).
  • Add deploy logic to skip a redundant redeploy when the tracked container is running the same image ID, with tests for success/fallback cases.

Reviewed changes

Copilot reviewed 3 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/usecase/container/service.go Adds the redundant-deploy short-circuit logic based on Docker image ID.
internal/usecase/container/service_test.go Adds new test coverage for skip/proceed scenarios; adjusts an existing deploy test.
internal/domain/container.go Extends Container model with ImageID.
internal/boundaries/out/runtime.go Adds GetImageID() to the runtime interface.
internal/boundaries/out/mocks/mock_container_runtime.go Updates mocks to implement the new runtime method.
internal/adapters/out/docker/runtime.go Populates ImageID for containers and implements GetImageID() via ImageInspect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +242 to +251
// Skip redundant deploy: if the existing container is already running
// the exact same image (by Docker image ID), return it immediately.
// This prevents the double-deploy caused by the event-based deploy
// (triggered by image.pushed) racing with the explicit CLI deploy call.
if hasExisting && existing.ImageID != "" {
if skip, container := s.skipRedundantDeploy(ctx, existing, resources.actualImageRef); skip {
return container, nil
}
}

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new redundancy short-circuit makes Deploy a no-op whenever a running tracked container has the same Docker image ID. This changes the behavior of manual/explicit deploys (the CLI help text says deploy/redeploy happens even if a container is already running) and can prevent legitimate redeploys intended to apply updated env/volume/attachment config without changing the image. Consider scoping the optimization to the specific double-deploy race (e.g., only skip when the previous successful deploy for this domain completed very recently and the image ID matches, or gate it behind a context/flag for event-triggered deploys) so explicit deploy remains a true redeploy when desired.

Suggested change
// Skip redundant deploy: if the existing container is already running
// the exact same image (by Docker image ID), return it immediately.
// This prevents the double-deploy caused by the event-based deploy
// (triggered by image.pushed) racing with the explicit CLI deploy call.
if hasExisting && existing.ImageID != "" {
if skip, container := s.skipRedundantDeploy(ctx, existing, resources.actualImageRef); skip {
return container, nil
}
}

Copilot uses AI. Check for mistakes.
@@ -313,11 +313,64 @@ func TestService_Deploy_ReplacesExistingContainer(t *testing.T) {

assert.NoError(t, err)
assert.Equal(t, "new-container", result.ID)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test no longer verifies that the service updates its tracked container entry after replacing an existing container (the earlier svc.Get(...) assertions were removed). Since replace/zero-downtime deploy is a distinct path from the initial deploy, it would be good to restore an assertion that svc.Get("test.example.com") returns the new container ID after Deploy completes.

Suggested change
assert.Equal(t, "new-container", result.ID)
assert.Equal(t, "new-container", result.ID)
// Ensure the service's tracked container entry now points to the new container.
trackedContainer, ok := svc.Get("test.example.com")
assert.True(t, ok)
assert.Equal(t, "new-container", trackedContainer.ID)

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/usecase/container/service.go`:
- Around line 242-250: The current short-circuit in service.go returns the
existing container based only on ImageID and running state, which ignores
configuration changes; update the logic so s.skipRedundantDeploy (and the call
sites around the other block at 274-304) also verifies that container
configuration matches the requested deploy (compare a persisted config
hash/label or individual fields such as env vars, volumes, labels, attachments,
networks) or restrict skipping to a safe recent-deploy window; modify
skipRedundantDeploy to accept the desired config (or compute/compare a config
hash) and only return skip=true when both image ID and config match, otherwise
proceed with redeploy.

Comment on lines +242 to +250
// Skip redundant deploy: if the existing container is already running
// the exact same image (by Docker image ID), return it immediately.
// This prevents the double-deploy caused by the event-based deploy
// (triggered by image.pushed) racing with the explicit CLI deploy call.
if hasExisting && existing.ImageID != "" {
if skip, container := s.skipRedundantDeploy(ctx, existing, resources.actualImageRef); skip {
return container, nil
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Skip logic can ignore config/env changes when image ID is unchanged.

Deploy now short-circuits solely on image ID + running state, so explicit redeploys with the same image won’t apply updated env vars, volumes, labels, attachments, or network changes. That’s a behavior regression for users who redeploy to pick up configuration changes.

Consider gating the skip to only known redundant cases (e.g., a short “recent deploy” window) or comparing a persisted config hash/label so redeploys still happen when config changes.

Also applies to: 274-304

🤖 Prompt for AI Agents
In `@internal/usecase/container/service.go` around lines 242 - 250, The current
short-circuit in service.go returns the existing container based only on ImageID
and running state, which ignores configuration changes; update the logic so
s.skipRedundantDeploy (and the call sites around the other block at 274-304)
also verifies that container configuration matches the requested deploy (compare
a persisted config hash/label or individual fields such as env vars, volumes,
labels, attachments, networks) or restrict skipping to a safe recent-deploy
window; modify skipRedundantDeploy to accept the desired config (or
compute/compare a config hash) and only return skip=true when both image ID and
config match, otherwise proceed with redeploy.

@bnema bnema merged commit 6ed59e0 into main Feb 9, 2026
2 of 3 checks passed
@bnema bnema deleted the fix/skip-redundant-deploy branch February 9, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants