Skip to content

fix(compose): preserve container state across daemon restarts#72

Merged
bilby91 merged 1 commit into
mainfrom
fix/compose-shellout-no-recreate
May 18, 2026
Merged

fix(compose): preserve container state across daemon restarts#72
bilby91 merged 1 commit into
mainfrom
fix/compose-shellout-no-recreate

Conversation

@bilby91
Copy link
Copy Markdown
Member

@bilby91 bilby91 commented May 18, 2026

Closes #71.

Summary

  • Shellout path (the one DAP runs on today): thread a single existingContainer bool from up.go through createFreshComposeupComposeShelloutComposeUpSpec.NoRecreate. When set, buildUpArgs appends --no-recreate to docker compose up -d. Mirrors upstream devcontainers/cli's gate (container || expectExistingContainer).
  • Native orchestrator: when an existing container's LabelConfigHash and LabelImageDigest match the desired spec but its state is not Running, StartContainer it instead of stop+remove+recreate. The previous gate (c.State == runtime.StateRunning as a third reuse condition) destroyed perfectly-good containers any time dockerd had been restarted, because daemon-restored containers come back Exited.

Both branches share one root cause: drift in compose-side config-hash (or a temporarily-stopped container) was being treated as a recreate signal, which destroys the container's writable layer. For workspaces where ~/.claude, ~/.bash_history, install caches, or anything else lives in $HOME, that's data loss.

Test plan

  • go test ./... clean.
  • New TestBuildUpArgs_NoRecreate / TestBuildUpArgs_NoRecreateOmittedByDefault cover the argv edges. Verified the positive case fails against the pre-fix buildUpArgs.
  • New TestUp_StartsStoppedContainerOnConfigMatch simulates a daemon-restart by stopping a previously-Up'd container and Up-ing again; asserts the container ID is preserved, StartContainer is called once, and RunContainer/RemoveContainer are not. Verified it fails against the pre-fix orchestrator.
  • Live reproduction in DAP: pre-fix path destroys ~/.claude/projects/<encoded>/<sid>.jsonl across pod restart and Claude SDK errors with "No conversation found with session ID: <sid>". With this patch the JSONL survives.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Stopped containers with matching configuration are now automatically restarted instead of being recreated.
    • Added --no-recreate flag support to preserve existing containers when configuration changes.
  • Tests

    • Added tests to verify container reuse and --no-recreate flag behavior.

Review Change Stack

Shellout path: thread an `existingContainer` signal from `up.go` into
`upComposeShellout` and append `--no-recreate` to `docker compose up`
when a container is already known for this workspace. Matches the
upstream devcontainers/cli gate (`container || expectExistingContainer`).
Without the flag, compose's default recreate-on-drift behavior can
destroy a perfectly-good container whenever it detects any change in
the generated override files or in caller-injected env, taking the
container's writable layer (e.g. `~/.claude/projects/...`) with it.

Native orchestrator: when an existing container's config-hash and
image-digest match but its state is not Running (e.g. dockerd was
just restarted and brought stopped containers back from on-disk
state), start it instead of stop+remove+recreating. Same root cause
as the shellout flag gap — drop it now so it doesn't follow us to
the native backend.

Tests:
- runtime/docker: `--no-recreate` is appended iff NoRecreate is set.
- compose: a config-matched Exited container is reused via
  StartContainer with no RunContainer / RemoveContainer calls.

Both tests fail against the prior code (verified by reverting each
fix in isolation), so they would catch a future regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 95257fe3-4ab6-442b-a8e1-1a10acf0624f

📥 Commits

Reviewing files that changed from the base of the PR and between 4dd3c64 and 571db62.

📒 Files selected for processing (6)
  • compose/orchestrator.go
  • compose/orchestrator_test.go
  • runtime/docker/compose.go
  • runtime/docker/compose_test.go
  • runtime/runtime.go
  • up.go

📝 Walkthrough

Walkthrough

This PR fixes container state destruction across daemon restarts by preventing recreation when compose config hashes match. It adds a NoRecreate flag to ComposeUpSpec, threads existing-container detection through the engine's compose refresh path, implements --no-recreate flag passing to docker compose, and updates the native orchestrator backend to start stopped containers instead of recreating them on config match.

Changes

Container preservation on compose refresh

Layer / File(s) Summary
ComposeUpSpec NoRecreate field
runtime/runtime.go
Adds NoRecreate bool field to ComposeUpSpec with documentation explaining it gates --no-recreate for docker compose to preserve existing containers during config-matched refresh.
Docker compose --no-recreate flag implementation
runtime/docker/compose.go, runtime/docker/compose_test.go
buildUpArgs conditionally appends --no-recreate when NoRecreate is set; two test cases verify the flag is included or omitted based on the boolean value.
Engine.Up threading and upComposeShellout wiring
up.go
Engine.Up detects existing containers and threads existing != nil through createFreshComposeupComposeShellout, where it sets ComposeUpSpec.NoRecreate to preserve containers across compose refresh.
Orchestrator stopped container reuse
compose/orchestrator.go, compose/orchestrator_test.go
ensureService now starts stopped containers when config hash and image digest match, instead of falling through to stop-and-recreate; regression test verifies container ID is preserved and StartContainer is called on reuse after daemon-like restart.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A rabbit hops through compose with glee,
Preserving state across restarts free,
No recreate flags make containers stay,
Through daemon sleep, they live another day! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preserving container state across daemon restarts through the compose backend fix.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #71: NoRecreate field added, buildUpArgs appends --no-recreate flag, existing container signal threaded through up.go/upComposeShellout, native orchestrator updated to start stopped config-matched containers, and comprehensive tests added.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #71: runtime specs, compose arg building, shellout path threading, orchestrator reuse logic, and corresponding unit/regression tests—no unrelated modifications present.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/compose-shellout-no-recreate

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

@bilby91 bilby91 merged commit 34ca9b4 into main May 18, 2026
18 checks passed
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.

Compose shellout: missing --no-recreate flag destroys container state across docker daemon restarts

1 participant