From 1fd2d1d1f37a32cc5f5ba08e35e22c887aa64eae Mon Sep 17 00:00:00 2001 From: "hiroto.toyoda" Date: Sun, 23 Aug 2026 22:46:12 +0900 Subject: [PATCH 1/2] fix(watch): exclude Dockerfile and compose files from initial sync Signed-off-by: hiroto.toyoda --- pkg/compose/watch.go | 10 +++++++++- pkg/compose/watch_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 71af227f3f..1861ed44d6 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -784,11 +784,19 @@ func (s *composeService) initialSync(ctx context.Context, service types.ServiceC if err != nil { return err } - // FIXME .dockerignore + + // the Dockerfile and compose files drive the build/orchestration, not the + // application: never copy them into the container on initial sync + dockerFileIgnore, err := watch.NewDockerPatternMatcher("/", []string{"Dockerfile", "*compose*.y*ml"}) + if err != nil { + return err + } + ignoreInitialSync := watch.NewCompositeMatcher( dockerIgnores, watch.EphemeralPathMatcher(), dotGitIgnore, + dockerFileIgnore, triggerIgnore) pathsToCopy, err := s.initialSyncFiles(service, trigger, ignoreInitialSync) diff --git a/pkg/compose/watch_test.go b/pkg/compose/watch_test.go index 04f8bf5a43..8fabbf9c11 100644 --- a/pkg/compose/watch_test.go +++ b/pkg/compose/watch_test.go @@ -241,6 +241,33 @@ func TestInitialSyncFilesRegularFile(t *testing.T) { }}) } +// initialSync's doc comment promises the Dockerfile and compose files are +// never copied into the container, but a refactor (ed10804e0) dropped the +// matcher enforcing it without replacement — neither the .dockerignore-derived +// matcher nor EphemeralPathMatcher cover this. +func TestInitialSync_ExcludesDockerfileAndComposeFiles(t *testing.T) { + hostDir := t.TempDir() + for _, name := range []string{"Dockerfile", "compose.yaml", "compose.override.yml", "app.go"} { + assert.NilError(t, os.WriteFile(filepath.Join(hostDir, name), []byte("content"), 0o600)) + } + + syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} + err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ + Name: "svc", + Build: &types.BuildConfig{Context: hostDir}, + }, types.Trigger{ + Path: hostDir, + Target: "/app", + }, syncer) + assert.NilError(t, err) + + paths := <-syncer.synced + assert.DeepEqual(t, paths, []*sync.PathMapping{{ + HostPath: filepath.Join(hostDir, "app.go"), + ContainerPath: "/app/app.go", + }}) +} + // TestPruneDanglingImagesOnRebuild verifies the post-rebuild prune only // removes superseded dangling images: a dangling image whose ID matches one // of the freshly built images must be spared. The lookup used to probe the From a4b308a34b35975887022ce6019de03d491d2958 Mon Sep 17 00:00:00 2001 From: "hiroto.toyoda" Date: Mon, 24 Aug 2026 19:53:51 +0900 Subject: [PATCH 2/2] fix(watch): match Dockerfile/compose ignore patterns by literal name and basename Signed-off-by: hiroto.toyoda --- pkg/compose/watch.go | 12 ++++++--- pkg/compose/watch_test.go | 57 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 1861ed44d6..00dff1206d 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -29,6 +29,7 @@ import ( gsync "sync" "time" + "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/utils" ccli "github.com/docker/cli/cli/command/container" @@ -785,9 +786,14 @@ func (s *composeService) initialSync(ctx context.Context, service types.ServiceC return err } - // the Dockerfile and compose files drive the build/orchestration, not the - // application: never copy them into the container on initial sync - dockerFileIgnore, err := watch.NewDockerPatternMatcher("/", []string{"Dockerfile", "*compose*.y*ml"}) + // also exclude override compose files and any custom-named Dockerfile + dockerFilePatterns := append([]string{"Dockerfile"}, cli.DefaultFileNames...) + dockerFilePatterns = append(dockerFilePatterns, cli.DefaultOverrideFileNames...) + if service.Build != nil && service.Build.Dockerfile != "" { + dockerFilePatterns = append(dockerFilePatterns, filepath.Base(service.Build.Dockerfile)) + } + + dockerFileIgnore, err := watch.NewDockerPatternMatcher("/", dockerFilePatterns) if err != nil { return err } diff --git a/pkg/compose/watch_test.go b/pkg/compose/watch_test.go index 8fabbf9c11..2fd3a5f25d 100644 --- a/pkg/compose/watch_test.go +++ b/pkg/compose/watch_test.go @@ -247,7 +247,7 @@ func TestInitialSyncFilesRegularFile(t *testing.T) { // matcher nor EphemeralPathMatcher cover this. func TestInitialSync_ExcludesDockerfileAndComposeFiles(t *testing.T) { hostDir := t.TempDir() - for _, name := range []string{"Dockerfile", "compose.yaml", "compose.override.yml", "app.go"} { + for _, name := range []string{"Dockerfile", "compose.yaml", "docker-compose.yml", "compose.override.yml", "app.go"} { assert.NilError(t, os.WriteFile(filepath.Join(hostDir, name), []byte("content"), 0o600)) } @@ -268,6 +268,61 @@ func TestInitialSync_ExcludesDockerfileAndComposeFiles(t *testing.T) { }}) } +// TestInitialSync_ExcludesCustomNamedDockerfile verifies that a service using +// a non-default Dockerfile name (build.dockerfile) still has it excluded from +// the initial sync, not just the literal "Dockerfile". +func TestInitialSync_ExcludesCustomNamedDockerfile(t *testing.T) { + hostDir := t.TempDir() + for _, name := range []string{"Dockerfile.prod", "app.go"} { + assert.NilError(t, os.WriteFile(filepath.Join(hostDir, name), []byte("content"), 0o600)) + } + + syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} + err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ + Name: "svc", + Build: &types.BuildConfig{Context: hostDir, Dockerfile: "Dockerfile.prod"}, + }, types.Trigger{ + Path: hostDir, + Target: "/app", + }, syncer) + assert.NilError(t, err) + + paths := <-syncer.synced + assert.DeepEqual(t, paths, []*sync.PathMapping{{ + HostPath: filepath.Join(hostDir, "app.go"), + ContainerPath: "/app/app.go", + }}) +} + +// TestInitialSync_ExcludesNestedCustomNamedDockerfile verifies that a +// build.dockerfile living in a subdirectory of the build context (e.g. +// "docker/Dockerfile.prod") is still excluded by its basename: the ignore +// matcher only ever receives filepath.Base(path), so appending the raw +// service.Build.Dockerfile value (which may include the subdirectory) would +// never match. +func TestInitialSync_ExcludesNestedCustomNamedDockerfile(t *testing.T) { + hostDir := t.TempDir() + assert.NilError(t, os.MkdirAll(filepath.Join(hostDir, "docker"), 0o755)) + assert.NilError(t, os.WriteFile(filepath.Join(hostDir, "docker", "Dockerfile.prod"), []byte("content"), 0o600)) + assert.NilError(t, os.WriteFile(filepath.Join(hostDir, "app.go"), []byte("content"), 0o600)) + + syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} + err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ + Name: "svc", + Build: &types.BuildConfig{Context: hostDir, Dockerfile: "docker/Dockerfile.prod"}, + }, types.Trigger{ + Path: hostDir, + Target: "/app", + }, syncer) + assert.NilError(t, err) + + paths := <-syncer.synced + assert.DeepEqual(t, paths, []*sync.PathMapping{{ + HostPath: filepath.Join(hostDir, "app.go"), + ContainerPath: "/app/app.go", + }}) +} + // TestPruneDanglingImagesOnRebuild verifies the post-rebuild prune only // removes superseded dangling images: a dangling image whose ID matches one // of the freshly built images must be spared. The lookup used to probe the