Repository: docker/compose-cli
Pull request #1381 (2021-03-02 17:14:35)

Fix compose up --build not forcing rebuild of present image
Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>

**What I did**
* check if need to force-build image before ignoring present images

**Related issue**
came across this when deploying local dos.docker.com with compose

<!-- optional tests
You can add a / mention to run tests executed by default only on main branch :
* `test-kube` to run Kube E2E tests
* `test-aci` to run ACI E2E tests
* `test-ecs` to run ECS E2E tests
* `test-windows` to run tests & E2E tests on windows
-->

**(not mandatory) A picture of a cute animal, if possible in relation with what you did**



Patch:
diff --git a/aci/compose.go b/aci/compose.go
index f004ddbd..8beba0ba 100644
--- a/aci/compose.go
+++ b/aci/compose.go
@@ -44,7 +44,7 @@ func newComposeService(ctx store.AciContext) aciComposeService {
 	}
 }
 
-func (cs *aciComposeService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
+func (cs *aciComposeService) Build(ctx context.Context, project *types.Project) error {
 	return errdefs.ErrNotImplemented
 }
 
diff --git a/api/client/compose.go b/api/client/compose.go
index f68378b8..75e7be16 100644
--- a/api/client/compose.go
+++ b/api/client/compose.go
@@ -28,7 +28,7 @@ import (
 type composeService struct {
 }
 
-func (c *composeService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
+func (c *composeService) Build(ctx context.Context, project *types.Project) error {
 	return errdefs.ErrNotImplemented
 }
 
diff --git a/api/compose/api.go b/api/compose/api.go
index a7156497..cd9dc4d8 100644
--- a/api/compose/api.go
+++ b/api/compose/api.go
@@ -28,7 +28,7 @@ import (
 // Service manages a compose project
 type Service interface {
 	// Build executes the equivalent to a `compose build`
-	Build(ctx context.Context, project *types.Project, options BuildOptions) error
+	Build(ctx context.Context, project *types.Project) error
 	// Push executes the equivalent ot a `compose push`
 	Push(ctx context.Context, project *types.Project) error
 	// Pull executes the equivalent of a `compose pull`
@@ -65,28 +65,12 @@ type Service interface {
 	UnPause(ctx context.Context, project *types.Project) error
 }
 
-// BuildOptions group options of the Build API
-type BuildOptions struct {
-	// Pull always attempt to pull a newer version of the image
-	Pull bool
-	// Progress set type of progress output ("auto", "plain", "tty")
-	Progress string
-}
-
 // CreateOptions group options of the Create API
 type CreateOptions struct {
-	// Services defines the services user interacts with
-	Services []string
 	// Remove legacy containers for services that are not defined in the project
 	RemoveOrphans bool
 	// Recreate define the strategy to apply on existing containers
 	Recreate string
-	// RecreateDependencies define the strategy to apply on dependencies services
-	RecreateDependencies string
-	// Inherit reuse anonymous volumes from previous container
-	Inherit bool
-	// Timeout set delay to wait for container to gracelfuly stop before sending SIGKILL
-	Timeout *time.Duration
 }
 
 // StartOptions group options of the Start API
diff --git a/cli/cmd/compose/build.go b/cli/cmd/compose/build.go
index b81c1710..622b753f 100644
--- a/cli/cmd/compose/build.go
+++ b/cli/cmd/compose/build.go
@@ -23,16 +23,13 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
-	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
 
 type buildOptions struct {
 	*projectOptions
 	composeOptions
-	quiet    bool
-	pull     bool
-	progress string
+	quiet bool
 }
 
 func buildCommand(p *projectOptions) *cobra.Command {
@@ -54,8 +51,6 @@ func buildCommand(p *projectOptions) *cobra.Command {
 		},
 	}
 	cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
-	cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
-	cmd.Flags().StringVar(&opts.progress, "progress", "auto", `Set type of progress output ("auto", "plain", "tty")`)
 	return cmd
 }
 
@@ -71,10 +66,7 @@ func runBuild(ctx context.Context, opts buildOptions, services []string) error {
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Build(ctx, project, compose.BuildOptions{
-			Pull:     opts.pull,
-			Progress: opts.progress,
-		})
+		return "", c.ComposeService().Build(ctx, project)
 	})
 	return err
 }
diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go
index 7f01f8c5..7cf9b0c1 100644
--- a/cli/cmd/compose/compose.go
+++ b/cli/cmd/compose/compose.go
@@ -17,12 +17,8 @@
 package compose
 
 import (
-	"fmt"
-
 	"github.com/compose-spec/compose-go/cli"
 	"github.com/compose-spec/compose-go/types"
-	"github.com/morikuni/aec"
-	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 
@@ -36,8 +32,7 @@ type projectOptions struct {
 	ProjectName string
 	Profiles    []string
 	ConfigPaths []string
-	WorkDir     string
-	ProjectDir  string
+	WorkingDir  string
 	EnvFile     string
 }
 
@@ -46,9 +41,8 @@ func (o *projectOptions) addProjectFlags(f *pflag.FlagSet) {
 	f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
 	f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
 	f.StringVar(&o.EnvFile, "env-file", "", "Specify an alternate environment file.")
-	f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the Compose file)")
-	f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the Compose file)")
-	_ = f.MarkHidden("workdir")
+	f.StringVar(&o.WorkingDir, "workdir", "", "Specify an alternate working directory")
+	// TODO make --project-directory an alias
 }
 
 func (o *projectOptions) toProjectName() (string, error) {
@@ -93,7 +87,7 @@ func (o *projectOptions) toProjectOptions() (*cli.ProjectOptions, error) {
 		cli.WithEnvFile(o.EnvFile),
 		cli.WithDotEnv,
 		cli.WithOsEnv,
-		cli.WithWorkingDirectory(o.ProjectDir),
+		cli.WithWorkingDirectory(o.WorkingDir),
 		cli.WithName(o.ProjectName))
 }
 
@@ -105,13 +99,6 @@ func Command(contextType string) *cobra.Command {
 		Use:              "compose",
 		TraverseChildren: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-			if opts.WorkDir != "" {
-				if opts.ProjectDir != "" {
-					return errors.New(aec.Apply(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead.`, aec.RedF))
-				}
-				opts.ProjectDir = opts.WorkDir
-				fmt.Println(aec.Apply(`option "--workdir" is DEPRECATED at root level! Please use "--project-directory" instead.`, aec.RedF))
-			}
 			if contextType == store.DefaultContextType || contextType == store.LocalContextType {
 				Warning = "The new 'docker compose' command is currently experimental. " +
 					"To provide feedback or request new features please open issues at https://github.com/docker/compose-cli"
diff --git a/cli/cmd/compose/pull.go b/cli/cmd/compose/pull.go
index 0bbd6e41..d7b8b7dd 100644
--- a/cli/cmd/compose/pull.go
+++ b/cli/cmd/compose/pull.go
@@ -28,8 +28,7 @@ import (
 type pullOptions struct {
 	*projectOptions
 	composeOptions
-	quiet       bool
-	includeDeps bool
+	quiet bool
 }
 
 func pullCommand(p *projectOptions) *cobra.Command {
@@ -44,7 +43,6 @@ func pullCommand(p *projectOptions) *cobra.Command {
 		},
 	}
 	cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information")
-	cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies")
 	return cmd
 }
 
@@ -59,19 +57,6 @@ func runPull(ctx context.Context, opts pullOptions, services []string) error {
 		return err
 	}
 
-	if !opts.includeDeps {
-		enabled, err := project.GetServices(services...)
-		if err != nil {
-			return err
-		}
-		for _, s := range project.Services {
-			if !contains(services, s.Name) {
-				project.DisabledServices = append(project.DisabledServices, s)
-			}
-		}
-		project.Services = enabled
-	}
-
 	if opts.quiet {
 		return c.ComposeService().Pull(ctx, project)
 	}
diff --git a/cli/cmd/compose/run.go b/cli/cmd/compose/run.go
index 4eacd070..e3a7af70 100644
--- a/cli/cmd/compose/run.go
+++ b/cli/cmd/compose/run.go
@@ -93,11 +93,7 @@ func runRun(ctx context.Context, opts runOptions) error {
 		if err != nil {
 			return err
 		}
-		for _, s := range project.Services {
-			if s.Name != opts.Service {
-				project.DisabledServices = append(project.DisabledServices, s)
-			}
-		}
+		project.DisabledServices = append(project.DisabledServices, project.Services...)
 		project.Services = types.Services{enabled}
 	}
 
diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go
index a97e0d09..73d022f6 100644
--- a/cli/cmd/compose/up.go
+++ b/cli/cmd/compose/up.go
@@ -27,17 +27,17 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/compose-spec/compose-go/types"
-	"github.com/sirupsen/logrus"
-	"github.com/spf13/cobra"
-	"golang.org/x/sync/errgroup"
-
 	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/context/store"
 	"github.com/docker/compose-cli/api/progress"
 	"github.com/docker/compose-cli/cli/cmd"
 	"github.com/docker/compose-cli/cli/formatter"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"golang.org/x/sync/errgroup"
 )
 
 // composeOptions hold options common to `up` and `run` to run compose project
@@ -56,9 +56,7 @@ type upOptions struct {
 	removeOrphans bool
 	forceRecreate bool
 	noRecreate    bool
-	recreateDeps  bool
 	noStart       bool
-	noDeps        bool
 	cascadeStop   bool
 	exitCodeFrom  string
 	scale         []string
@@ -66,77 +64,19 @@ type upOptions struct {
 	noPrefix      bool
 	timeChanged   bool
 	timeout       int
-	noInherit     bool
-}
-
-func (opts upOptions) recreateStrategy() string {
-	if opts.noRecreate {
-		return compose.RecreateNever
-	}
-	if opts.forceRecreate {
-		return compose.RecreateForce
-	}
-	return compose.RecreateDiverged
+	noDeps        bool
 }
 
-func (opts upOptions) dependenciesRecreateStrategy() string {
-	if opts.noRecreate {
+func (o upOptions) recreateStrategy() string {
+	if o.noRecreate {
 		return compose.RecreateNever
 	}
-	if opts.recreateDeps {
+	if o.forceRecreate {
 		return compose.RecreateForce
 	}
 	return compose.RecreateDiverged
 }
 
-func (opts upOptions) GetTimeout() *time.Duration {
-	if opts.timeChanged {
-		t := time.Duration(opts.timeout) * time.Second
-		return &t
-	}
-	return nil
-}
-
-func (opts upOptions) apply(project *types.Project, services []string) error {
-	if opts.noDeps {
-		enabled, err := project.GetServices(services...)
-		if err != nil {
-			return err
-		}
-		for _, s := range project.Services {
-			if !contains(services, s.Name) {
-				project.DisabledServices = append(project.DisabledServices, s)
-			}
-		}
-		project.Services = enabled
-	}
-
-	if opts.exitCodeFrom != "" {
-		_, err := project.GetService(opts.exitCodeFrom)
-		if err != nil {
-			return err
-		}
-	}
-
-	for _, scale := range opts.scale {
-		split := strings.Split(scale, "=")
-		if len(split) != 2 {
-			return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
-		}
-		name := split[0]
-		replicas, err := strconv.Atoi(split[1])
-		if err != nil {
-			return err
-		}
-		err = setServiceScale(project, name, replicas)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
 func upCommand(p *projectOptions, contextType string) *cobra.Command {
 	opts := upOptions{
 		composeOptions: &composeOptions{
@@ -162,9 +102,6 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
 				if opts.forceRecreate && opts.noRecreate {
 					return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
 				}
-				if opts.recreateDeps && opts.noRecreate {
-					return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
-				}
 				return runCreateStart(cmd.Context(), opts, args)
 			default:
 				return runUp(cmd.Context(), opts, args)
@@ -192,8 +129,6 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
 		flags.StringVar(&opts.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
 		flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
 		flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.")
-		flags.BoolVar(&opts.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
-		flags.BoolVarP(&opts.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.")
 	}
 
 	return upCmd
@@ -205,7 +140,7 @@ func runUp(ctx context.Context, opts upOptions, services []string) error {
 		return err
 	}
 
-	err = opts.apply(project, services)
+	err = applyScaleOpt(opts.scale, project)
 	if err != nil {
 		return err
 	}
@@ -224,19 +159,39 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 		return err
 	}
 
-	err = opts.apply(project, services)
+	if opts.noDeps {
+		enabled, err := project.GetServices(services...)
+		if err != nil {
+			return err
+		}
+		project.DisabledServices = append(project.DisabledServices, project.Services...)
+		project.Services = enabled
+	}
+
+	err = applyScaleOpt(opts.scale, project)
 	if err != nil {
 		return err
 	}
 
+	if opts.exitCodeFrom != "" {
+		_, err := project.GetService(opts.exitCodeFrom)
+		if err != nil {
+			return err
+		}
+	}
+
+	if opts.timeChanged {
+		timeoutValue := types.Duration(time.Duration(opts.timeout) * time.Second)
+		for i, s := range project.Services {
+			s.StopGracePeriod = &timeoutValue
+			project.Services[i] = s
+		}
+	}
+
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
 		err := c.ComposeService().Create(ctx, project, compose.CreateOptions{
-			Services:             services,
-			RemoveOrphans:        opts.removeOrphans,
-			Recreate:             opts.recreateStrategy(),
-			RecreateDependencies: opts.dependenciesRecreateStrategy(),
-			Inherit:              !opts.noInherit,
-			Timeout:              opts.GetTimeout(),
+			RemoveOrphans: opts.removeOrphans,
+			Recreate:      opts.recreateStrategy(),
 		})
 		if err != nil {
 			return "", err
@@ -307,6 +262,25 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 	return err
 }
 
+func applyScaleOpt(opts []string, project *types.Project) error {
+	for _, scale := range opts {
+		split := strings.Split(scale, "=")
+		if len(split) != 2 {
+			return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
+		}
+		name := split[0]
+		replicas, err := strconv.Atoi(split[1])
+		if err != nil {
+			return err
+		}
+		err = setServiceScale(project, name, replicas)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func setServiceScale(project *types.Project, name string, replicas int) error {
 	for i, s := range project.Services {
 		if s.Name == name {
diff --git a/docs/ecs-compose-features.md b/docs/ecs-compose-features.md
index 5fb4b7e2..9b232f46 100644
--- a/docs/ecs-compose-features.md
+++ b/docs/ecs-compose-features.md
@@ -104,8 +104,8 @@ by passing configuration attributes prefixed with `awslogs-`.
 ## Exposing ports
 
 When one or more services expose ports, a Load Balancer is created for the application.
-As all services are exposed through the same Load Balancer, only one service can expose a given port number.
-The source and target ports defined in the Compose file MUST be the same, as service-to-service communication don't go through the Load Balancer and could not
+As all services are expose through the same Load Balancer, only one service can expose a given port number.
+The source and target ports defined in the Compose file MUST be the same, as service-to-service communication don't go trought the Load Balancer and could not
 benefit from Listeners abstraction to assign a distinct published port.
 
 If services in the Compose file only expose ports 80 or 443, an Application Load Balancer is created, otherwise ECS integration will provision a Network Load Balancer.
diff --git a/ecs/local/compose.go b/ecs/local/compose.go
index f8a8da8e..6ecce3e7 100644
--- a/ecs/local/compose.go
+++ b/ecs/local/compose.go
@@ -32,8 +32,8 @@ import (
 	"github.com/docker/compose-cli/api/errdefs"
 )
 
-func (e ecsLocalSimulation) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
-	return e.compose.Build(ctx, project, options)
+func (e ecsLocalSimulation) Build(ctx context.Context, project *types.Project) error {
+	return e.compose.Build(ctx, project)
 }
 
 func (e ecsLocalSimulation) Push(ctx context.Context, project *types.Project) error {
diff --git a/ecs/up.go b/ecs/up.go
index b899bd0d..697cabe0 100644
--- a/ecs/up.go
+++ b/ecs/up.go
@@ -31,7 +31,7 @@ import (
 	"github.com/compose-spec/compose-go/types"
 )
 
-func (b *ecsAPIService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
+func (b *ecsAPIService) Build(ctx context.Context, project *types.Project) error {
 	return errdefs.ErrNotImplemented
 }
 
diff --git a/kube/compose.go b/kube/compose.go
index 9ca2911f..230914a6 100644
--- a/kube/compose.go
+++ b/kube/compose.go
@@ -124,7 +124,7 @@ func (s *composeService) List(ctx context.Context, opts compose.ListOptions) ([]
 }
 
 // Build executes the equivalent to a `compose build`
-func (s *composeService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
+func (s *composeService) Build(ctx context.Context, project *types.Project) error {
 	return errdefs.ErrNotImplemented
 }
 
diff --git a/local/compose/build.go b/local/compose/build.go
index 80e8ae99..08ac0812 100644
--- a/local/compose/build.go
+++ b/local/compose/build.go
@@ -23,8 +23,6 @@ import (
 	"path"
 	"strings"
 
-	"github.com/docker/compose-cli/api/compose"
-
 	"github.com/compose-spec/compose-go/types"
 	"github.com/docker/buildx/build"
 	"github.com/docker/buildx/driver"
@@ -34,20 +32,18 @@ import (
 	bclient "github.com/moby/buildkit/client"
 )
 
-func (s *composeService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
+func (s *composeService) Build(ctx context.Context, project *types.Project) error {
 	opts := map[string]build.Options{}
 	imagesToBuild := []string{}
 	for _, service := range project.Services {
 		if service.Build != nil {
 			imageName := getImageName(service, project.Name)
 			imagesToBuild = append(imagesToBuild, imageName)
-			buildOptions := s.toBuildOptions(service, project.WorkingDir, imageName)
-			buildOptions.Pull = options.Pull
-			opts[imageName] = buildOptions
+			opts[imageName] = s.toBuildOptions(service, project.WorkingDir, imageName)
 		}
 	}
 
-	err := s.build(ctx, project, opts, options.Progress)
+	err := s.build(ctx, project, opts)
 	if err == nil {
 		displayScanSuggestMsg(ctx, imagesToBuild)
 	}
@@ -69,11 +65,6 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
 			return err
 		}
 
-		if service.Image != "" {
-			if localImagePresent {
-				continue
-			}
-		}
 		if service.Build != nil {
 			if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
 				continue
@@ -82,6 +73,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
 			opts[imageName] = s.toBuildOptions(service, project.WorkingDir, imageName)
 			continue
 		}
+		if service.Image != "" {
+			if localImagePresent {
+				continue
+			}
+		}
 
 		// Buildx has no command to "just pull", see
 		// so we bake a temporary dockerfile that will just pull and export pulled image
@@ -97,7 +93,7 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
 
 	}
 
-	err := s.build(ctx, project, opts, "auto")
+	err := s.build(ctx, project, opts)
 	if err == nil {
 		displayScanSuggestMsg(ctx, imagesToBuild)
 	}
@@ -115,7 +111,7 @@ func (s *composeService) localImagePresent(ctx context.Context, imageName string
 	return true, nil
 }
 
-func (s *composeService) build(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) error {
+func (s *composeService) build(ctx context.Context, project *types.Project, opts map[string]build.Options) error {
 	if len(opts) == 0 {
 		return nil
 	}
@@ -136,7 +132,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opts
 	// build and will lock
 	progressCtx, cancel := context.WithCancel(context.Background())
 	defer cancel()
-	w := progress.NewPrinter(progressCtx, os.Stdout, mode)
+	w := progress.NewPrinter(progressCtx, os.Stdout, "auto")
 
 	// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
 	_, err = build.Build(ctx, driverInfo, opts, nil, nil, w)
diff --git a/local/compose/convergence.go b/local/compose/convergence.go
index fe0571a5..97fa40fc 100644
--- a/local/compose/convergence.go
+++ b/local/compose/convergence.go
@@ -42,7 +42,7 @@ const (
 		"Remove the custom name to scale the service.\n"
 )
 
-func (s *composeService) ensureScale(ctx context.Context, project *types.Project, service types.ServiceConfig, timeout *time.Duration) (*errgroup.Group, []moby.Container, error) {
+func (s *composeService) ensureScale(ctx context.Context, project *types.Project, service types.ServiceConfig) (*errgroup.Group, []moby.Container, error) {
 	cState, err := GetContextContainerState(ctx)
 	if err != nil {
 		return nil, nil, err
@@ -73,7 +73,7 @@ func (s *composeService) ensureScale(ctx context.Context, project *types.Project
 		for i := scale; i < len(actual); i++ {
 			container := actual[i]
 			eg.Go(func() error {
-				err := s.apiClient.ContainerStop(ctx, container.ID, timeout)
+				err := s.apiClient.ContainerStop(ctx, container.ID, nil)
 				if err != nil {
 					return err
 				}
@@ -85,8 +85,8 @@ func (s *composeService) ensureScale(ctx context.Context, project *types.Project
 	return eg, actual, nil
 }
 
-func (s *composeService) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error {
-	eg, actual, err := s.ensureScale(ctx, project, service, timeout)
+func (s *composeService) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string) error {
+	eg, actual, err := s.ensureScale(ctx, project, service)
 	if err != nil {
 		return err
 	}
@@ -106,7 +106,7 @@ func (s *composeService) ensureService(ctx context.Context, project *types.Proje
 		diverged := container.Labels[configHashLabel] != expected
 		if diverged || recreate == compose.RecreateForce || service.Extensions[extLifecycle] == forceRecreate {
 			eg.Go(func() error {
-				return s.recreateContainer(ctx, project, service, container, inherit, timeout)
+				return s.recreateContainer(ctx, project, service, container)
 			})
 			continue
 		}
@@ -209,10 +209,10 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
 	return nil
 }
 
-func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container, inherit bool, timeout *time.Duration) error {
+func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container) error {
 	w := progress.ContextWriter(ctx)
 	w.Event(progress.NewEvent(getContainerProgressName(container), progress.Working, "Recreate"))
-	err := s.apiClient.ContainerStop(ctx, container.ID, timeout)
+	err := s.apiClient.ContainerStop(ctx, container.ID, nil)
 	if err != nil {
 		return err
 	}
@@ -226,12 +226,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
 	if err != nil {
 		return err
 	}
-
-	var inherited *moby.Container
-	if inherit {
-		inherited = &container
-	}
-	err = s.createMobyContainer(ctx, project, service, name, number, inherited, false)
+	err = s.createMobyContainer(ctx, project, service, name, number, &container, false)
 	if err != nil {
 		return err
 	}
@@ -268,13 +263,13 @@ func (s *composeService) restartContainer(ctx context.Context, container moby.Co
 	return nil
 }
 
-func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, inherit *moby.Container,
+func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container,
 	autoRemove bool) error {
 	cState, err := GetContextContainerState(ctx)
 	if err != nil {
 		return err
 	}
-	containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit, autoRemove)
+	containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, container, autoRemove)
 	if err != nil {
 		return err
 	}
diff --git a/local/compose/create.go b/local/compose/create.go
index f35557d9..13cd307a 100644
--- a/local/compose/create.go
+++ b/local/compose/create.go
@@ -42,10 +42,6 @@ import (
 )
 
 func (s *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
-	if len(opts.Services) == 0 {
-		opts.Services = project.ServiceNames()
-	}
-
 	err := s.ensureImagesExists(ctx, project)
 	if err != nil {
 		return err
@@ -101,10 +97,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
 	prepareNetworkMode(project)
 
 	return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
-		if contains(opts.Services, service.Name) {
-			return s.ensureService(c, project, service, opts.Recreate, opts.Inherit, opts.Timeout)
-		}
-		return s.ensureService(c, project, service, opts.RecreateDependencies, opts.Inherit, opts.Timeout)
+		return s.ensureService(c, project, service, opts.Recreate)
 	})
 }
 


Test patch:
diff --git a/cli/cmd/compose/up_test.go b/cli/cmd/compose/up_test.go
index 566a6847..03dac57e 100644
--- a/cli/cmd/compose/up_test.go
+++ b/cli/cmd/compose/up_test.go
@@ -34,8 +34,7 @@ func TestApplyScaleOpt(t *testing.T) {
 			},
 		},
 	}
-	opt := upOptions{scale: []string{"foo=2"}}
-	err := opt.apply(&p, nil)
+	err := applyScaleOpt([]string{"foo=2"}, &p)
 	assert.NilError(t, err)
 	foo, err := p.GetService("foo")
 	assert.NilError(t, err)
diff --git a/local/e2e/compose/compose_build_test.go b/local/e2e/compose/compose_build_test.go
index 707f7562..2ad456bc 100644
--- a/local/e2e/compose/compose_build_test.go
+++ b/local/e2e/compose/compose_build_test.go
@@ -36,7 +36,7 @@ func TestLocalComposeBuild(t *testing.T) {
 		c.RunDockerOrExitError("rmi", "build-test_nginx")
 		c.RunDockerOrExitError("rmi", "custom-nginx")
 
-		res := c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "build")
+		res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "build")
 
 		res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
 		c.RunDockerCmd("image", "inspect", "build-test_nginx")
@@ -47,12 +47,13 @@ func TestLocalComposeBuild(t *testing.T) {
 		c.RunDockerOrExitError("rmi", "build-test_nginx")
 		c.RunDockerOrExitError("rmi", "custom-nginx")
 
-		res := c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "up", "-d")
+		res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "up", "-d")
 		t.Cleanup(func() {
-			c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "down")
+			c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "down")
 		})
 
 		res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
+		res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
 
 		output := HTTPGetWithRetry(t, "http://localhost:8070", http.StatusOK, 2*time.Second, 20*time.Second)
 		assert.Assert(t, strings.Contains(output, "Hello from Nginx container"))
@@ -62,13 +63,20 @@ func TestLocalComposeBuild(t *testing.T) {
 	})
 
 	t.Run("no rebuild when up again", func(t *testing.T) {
-		res := c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "up", "-d")
+		res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "up", "-d")
 
-		assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static /usr/share/nginx/html"), res.Stdout())
+		assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"), res.Stdout())
+	})
+
+	t.Run("rebuild when up --build", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "up", "-d", "--build")
+
+		res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
+		res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
 	})
 
 	t.Run("cleanup build project", func(t *testing.T) {
-		c.RunDockerCmd("compose", "--project-directory", "fixtures/build-test", "down")
+		c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "down")
 		c.RunDockerCmd("rmi", "build-test_nginx")
 		c.RunDockerCmd("rmi", "custom-nginx")
 	})
diff --git a/local/e2e/compose/compose_test.go b/local/e2e/compose/compose_test.go
index dc8008f2..37b9355b 100644
--- a/local/e2e/compose/compose_test.go
+++ b/local/e2e/compose/compose_test.go
@@ -118,7 +118,7 @@ func TestLocalComposeUp(t *testing.T) {
 func TestComposePull(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 
-	res := c.RunDockerOrExitError("compose", "--project-directory", "fixtures/simple-composefile", "pull")
+	res := c.RunDockerOrExitError("compose", "--workdir", "fixtures/simple-composefile", "pull")
 	output := res.Combined()
 
 	assert.Assert(t, strings.Contains(output, "simple Pulled"))
diff --git a/local/e2e/compose/fixtures/build-test/compose.yml b/local/e2e/compose/fixtures/build-test/compose.yml
index 0e7f2c12..2db602a2 100644
--- a/local/e2e/compose/fixtures/build-test/compose.yml
+++ b/local/e2e/compose/fixtures/build-test/compose.yml
@@ -5,5 +5,5 @@ services:
       - 8070:80
 
   nginx2:
-    build: nginx-build
+    build: nginx-build2
     image: custom-nginx
diff --git a/local/e2e/compose/fixtures/build-test/nginx-build2/Dockerfile b/local/e2e/compose/fixtures/build-test/nginx-build2/Dockerfile
new file mode 100644
index 00000000..b717000d
--- /dev/null
+++ b/local/e2e/compose/fixtures/build-test/nginx-build2/Dockerfile
@@ -0,0 +1,17 @@
+#   Copyright 2020 Docker Compose CLI authors
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+FROM nginx
+
+COPY static2 /usr/share/nginx/html
diff --git a/local/e2e/compose/fixtures/build-test/nginx-build2/static2/index.html b/local/e2e/compose/fixtures/build-test/nginx-build2/static2/index.html
new file mode 100644
index 00000000..63159b9e
--- /dev/null
+++ b/local/e2e/compose/fixtures/build-test/nginx-build2/static2/index.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Docker Nginx</title>
+</head>
+<body>
+  <h2>Hello from Nginx container</h2>
+</body>
+</html>
diff --git a/local/e2e/compose/volumes_test.go b/local/e2e/compose/volumes_test.go
index 1078b6a3..b129170c 100644
--- a/local/e2e/compose/volumes_test.go
+++ b/local/e2e/compose/volumes_test.go
@@ -37,7 +37,7 @@ func TestLocalComposeVolume(t *testing.T) {
 		c.RunDockerOrExitError("rmi", "compose-e2e-volume_nginx")
 		c.RunDockerOrExitError("volume", "rm", projectName+"_staticVol")
 		c.RunDockerOrExitError("volume", "rm", "myvolume")
-		c.RunDockerCmd("compose", "--project-directory", "fixtures/volume-test", "--project-name", projectName, "up", "-d")
+		c.RunDockerCmd("compose", "--workdir", "fixtures/volume-test", "--project-name", projectName, "up", "-d")
 	})
 
 	t.Run("access bind mount data", func(t *testing.T) {