Repository: docker/compose (go)

Issue:
`docker compose run` fails to use stdin properly
## Description of the issue

## Context information (for bug reports)

- [x] Using Compose V2 `docker compose ...`
- [ ] Using Compose V1 `docker-compose ...`

**Output of `docker(-)compose version`**
```
Docker Compose version v2.0.0-rc.3
```
Installed with Docker-desktop from the link in https://github.com/docker/compose/issues/8652#issuecomment-924622448 (build 68933)

**Output of `docker version`**
```
Client:
 Cloud integration: 1.0.17
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:55:20 2021
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.8
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.6
  Git commit:       75249d8
  Built:            Fri Jul 30 19:52:10 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.9
  GitCommit:        e25210fe30a0a703442421b0f60afac609f950a3
 runc:
  Version:          1.0.1
  GitCommit:        v1.0.1-0-g4144b63
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```

**Output of `docker-compose config`**
(Make sure to add the relevant `-f` and other flags)
```
services:
  app:
    image: debian
    networks:
      default: null
networks:
  default:
    name: compose-bug_default
```


## Steps to reproduce the issue

1. `echo yay | docker-compose run --rm app md5sum`

### Observed result

```console
$ echo yay | docker-compose run --rm app md5sum
yay
Error response from daemon: You cannot remove a running container aeefb979dd842ad84fe34821840aa4040372d926fecc7a666ec55fa940bc7719. Stop the container before attempting removal or force remove
```

- The content of stdin is displayed as is in stdout 
- And not passed to the command inside `docker-compose run`
- And docker-compose crashes trying to execute the `--rm` while the container is still running

### Expected result
Note: this is what I have with docker-compose version 1.29.2 from pip

```console
$ echo yay | docker-compose run --rm app md5sum
Creating compose-bug_app_run ... done
9945453f7ca223c906883f63c11f16d7  -
```

- The content of stdin is passed to the process inside `docker-compose run`
- `docker-compose` waits for the process to end
- and removes it thanks to `--rm`

### Stacktrace / full error message

none

## Additional information

The real-life command I tried to run was:
```
xz -d -f -k dev/example.sql.xz --stdout | docker-compose run --rm web psql -h db -d warehouse -U postgres -v ON_ERROR_STOP=1 -1 -f -
```
What I observed was similar. This worked with the old `docker-compose` v1

Pull request:
`run` must attach to stdin
`compose run` must configure created container with `attachStdin: true``
also, when stdin is set by piping from another command, tty must be force-disabled

Resolves #8660


Patch:
diff --git a/cmd/compose/create.go b/cmd/compose/create.go
index 266f6fc1d..7ce8a1853 100644
--- a/cmd/compose/create.go
+++ b/cmd/compose/create.go
@@ -31,6 +31,7 @@ type createOptions struct {
 	Build         bool
 	noBuild       bool
 	removeOrphans bool
+	ignoreOrphans bool
 	forceRecreate bool
 	noRecreate    bool
 	recreateDeps  bool
@@ -57,6 +58,7 @@ func createCommand(p *projectOptions, backend api.Service) *cobra.Command {
 		RunE: p.WithProject(func(ctx context.Context, project *types.Project) error {
 			return backend.Create(ctx, project, api.CreateOptions{
 				RemoveOrphans:        opts.removeOrphans,
+				IgnoreOrphans:        opts.ignoreOrphans,
 				Recreate:             opts.recreateStrategy(),
 				RecreateDependencies: opts.dependenciesRecreateStrategy(),
 				Inherit:              !opts.noInherit,
diff --git a/cmd/compose/exec.go b/cmd/compose/exec.go
index 9d2370475..f5be30985 100644
--- a/cmd/compose/exec.go
+++ b/cmd/compose/exec.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"os"
 
-	cgo "github.com/compose-spec/compose-go/cli"
 	"github.com/containerd/console"
 	"github.com/docker/cli/cli"
 	"github.com/spf13/cobra"
@@ -78,7 +77,7 @@ func execCommand(p *projectOptions, backend api.Service) *cobra.Command {
 }
 
 func runExec(ctx context.Context, backend api.Service, opts execOpts) error {
-	project, err := opts.toProject(nil, cgo.WithResolvedPaths(true))
+	project, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go
index 387c0a100..8e4f76a09 100644
--- a/cmd/compose/ps.go
+++ b/cmd/compose/ps.go
@@ -147,14 +147,7 @@ SERVICES:
 func writter(containers []api.ContainerSummary) func(w io.Writer) {
 	return func(w io.Writer) {
 		for _, container := range containers {
-			var ports []string
-			for _, p := range container.Publishers {
-				if p.URL == "" {
-					ports = append(ports, fmt.Sprintf("%d/%s", p.TargetPort, p.Protocol))
-				} else {
-					ports = append(ports, fmt.Sprintf("%s->%d/%s", p.URL, p.TargetPort, p.Protocol))
-				}
-			}
+			ports := DisplayablePorts(container)
 			status := container.State
 			if status == "running" && container.Health != "" {
 				status = fmt.Sprintf("%s (%s)", container.State, container.Health)
@@ -162,7 +155,7 @@ func writter(containers []api.ContainerSummary) func(w io.Writer) {
 				status = fmt.Sprintf("%s (%d)", container.State, container.ExitCode)
 			}
 			command := formatter2.Ellipsis(container.Command, 20)
-			_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", container.Name, strconv.Quote(command), container.Service, status, strings.Join(ports, ", "))
+			_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", container.Name, strconv.Quote(command), container.Service, status, ports)
 		}
 	}
 }
@@ -182,3 +175,73 @@ func filterByStatus(containers []api.ContainerSummary, status string) []api.Cont
 	}
 	return filtered
 }
+
+type portRange struct {
+	pStart   int
+	pEnd     int
+	tStart   int
+	tEnd     int
+	IP       string
+	protocol string
+}
+
+func (pr portRange) String() string {
+	var (
+		pub string
+		tgt string
+	)
+
+	if pr.pEnd > pr.pStart {
+		pub = fmt.Sprintf("%s:%d-%d->", pr.IP, pr.pStart, pr.pEnd)
+	} else if pr.pStart > 0 {
+		pub = fmt.Sprintf("%s:%d->", pr.IP, pr.pStart)
+	}
+	if pr.tEnd > pr.tStart {
+		tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd)
+	} else {
+		tgt = fmt.Sprintf("%d", pr.tStart)
+	}
+	return fmt.Sprintf("%s%s/%s", pub, tgt, pr.protocol)
+}
+
+// DisplayablePorts is copy pasted from https://github.com/docker/cli/pull/581/files
+func DisplayablePorts(c api.ContainerSummary) string {
+	if c.Publishers == nil {
+		return ""
+	}
+
+	sort.Sort(c.Publishers)
+
+	pr := portRange{}
+	ports := []string{}
+	for _, p := range c.Publishers {
+		prIsRange := pr.tEnd != pr.tStart
+		tOverlaps := p.TargetPort <= pr.tEnd
+
+		// Start a new port-range if:
+		// - the protocol is different from the current port-range
+		// - published or target port are not consecutive to the current port-range
+		// - the current port-range is a _range_, and the target port overlaps with the current range's target-ports
+		if p.Protocol != pr.protocol || p.URL != pr.IP || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps {
+			// start a new port-range, and print the previous port-range (if any)
+			if pr.pStart > 0 {
+				ports = append(ports, pr.String())
+			}
+			pr = portRange{
+				pStart:   p.PublishedPort,
+				pEnd:     p.PublishedPort,
+				tStart:   p.TargetPort,
+				tEnd:     p.TargetPort,
+				protocol: p.Protocol,
+				IP:       p.URL,
+			}
+			continue
+		}
+		pr.pEnd = p.PublishedPort
+		pr.tEnd = p.TargetPort
+	}
+	if pr.tStart > 0 {
+		ports = append(ports, pr.String())
+	}
+	return strings.Join(ports, ", ")
+}
diff --git a/cmd/compose/run.go b/cmd/compose/run.go
index 7bc3d4ba4..cdb8c0dcc 100644
--- a/cmd/compose/run.go
+++ b/cmd/compose/run.go
@@ -141,7 +141,7 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
 	flags := cmd.Flags()
 	flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
 	flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
-	flags.StringArrayVarP(&opts.labels, "labels", "l", []string{}, "Add or override a label")
+	flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label")
 	flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
 	flags.BoolVarP(&opts.noTty, "no-TTY", "T", notAtTTY(), "Disable pseudo-noTty allocation. By default docker compose run allocates a TTY")
 	flags.StringVar(&opts.name, "name", "", " Assign a name to the container")
@@ -163,12 +163,15 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
 	switch name {
 	case "volumes":
 		name = "volume"
+	case "labels":
+		name = "label"
 	}
 	return pflag.NormalizedName(name)
 }
 
 func notAtTTY() bool {
-	return !isatty.IsTerminal(os.Stdout.Fd())
+	b := isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd())
+	return !b
 }
 
 func runRun(ctx context.Context, backend api.Service, project *types.Project, opts runOptions) error {
diff --git a/cmd/compose/up.go b/cmd/compose/up.go
index 70460d64a..cfde2178a 100644
--- a/cmd/compose/up.go
+++ b/cmd/compose/up.go
@@ -120,6 +120,11 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
 			return nil
 		}),
 		RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
+			ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"]
+			create.ignoreOrphans = strings.ToLower(ignore) == "true"
+			if create.ignoreOrphans && create.removeOrphans {
+				return fmt.Errorf("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined")
+			}
 			return runUp(ctx, backend, create, up, project, services)
 		}),
 		ValidArgsFunction: serviceCompletion(p),
@@ -177,6 +182,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions
 	create := api.CreateOptions{
 		Services:             services,
 		RemoveOrphans:        createOptions.removeOrphans,
+		IgnoreOrphans:        createOptions.ignoreOrphans,
 		Recreate:             createOptions.recreateStrategy(),
 		RecreateDependencies: createOptions.dependenciesRecreateStrategy(),
 		Inherit:              !createOptions.noInherit,
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 00f3a3116..9efaa270b 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -61,7 +61,7 @@ type Service interface {
 	// Remove executes the equivalent to a `compose rm`
 	Remove(ctx context.Context, project *types.Project, options RemoveOptions) error
 	// Exec executes a command in a running service container
-	Exec(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
+	Exec(ctx context.Context, project string, opts RunOptions) (int, error)
 	// Copy copies a file/folder between a service container and the local filesystem
 	Copy(ctx context.Context, project *types.Project, opts CopyOptions) error
 	// Pause executes the equivalent to a `compose pause`
@@ -100,6 +100,8 @@ type CreateOptions struct {
 	Services []string
 	// Remove legacy containers for services that are not defined in the project
 	RemoveOrphans bool
+	// Ignore legacy containers for services that are not defined in the project
+	IgnoreOrphans bool
 	// Recreate define the strategy to apply on existing containers
 	Recreate string
 	// RecreateDependencies define the strategy to apply on dependencies services
@@ -297,7 +299,36 @@ type ContainerSummary struct {
 	State      string
 	Health     string
 	ExitCode   int
-	Publishers []PortPublisher
+	Publishers PortPublishers
+}
+
+// PortPublishers is a slice of PortPublisher
+type PortPublishers []PortPublisher
+
+// Len implements sort.Interface
+func (p PortPublishers) Len() int {
+	return len(p)
+}
+
+// Less implements sort.Interface
+func (p PortPublishers) Less(i, j int) bool {
+	left := p[i]
+	right := p[j]
+	if left.URL != right.URL {
+		return left.URL < right.URL
+	}
+	if left.TargetPort != right.TargetPort {
+		return left.TargetPort < right.TargetPort
+	}
+	if left.PublishedPort != right.PublishedPort {
+		return left.PublishedPort < right.PublishedPort
+	}
+	return left.Protocol < right.Protocol
+}
+
+// Swap implements sort.Interface
+func (p PortPublishers) Swap(i, j int) {
+	p[i], p[j] = p[j], p[i]
 }
 
 // ContainerProcSummary holds container processes top data
diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go
index 03163a73e..aff81e1f4 100644
--- a/pkg/api/proxy.go
+++ b/pkg/api/proxy.go
@@ -40,7 +40,7 @@ type ServiceProxy struct {
 	KillFn               func(ctx context.Context, project *types.Project, options KillOptions) error
 	RunOneOffContainerFn func(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
 	RemoveFn             func(ctx context.Context, project *types.Project, options RemoveOptions) error
-	ExecFn               func(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
+	ExecFn               func(ctx context.Context, project string, opts RunOptions) (int, error)
 	CopyFn               func(ctx context.Context, project *types.Project, opts CopyOptions) error
 	PauseFn              func(ctx context.Context, project string, options PauseOptions) error
 	UnPauseFn            func(ctx context.Context, project string, options PauseOptions) error
@@ -261,13 +261,10 @@ func (s *ServiceProxy) Remove(ctx context.Context, project *types.Project, optio
 }
 
 // Exec implements Service interface
-func (s *ServiceProxy) Exec(ctx context.Context, project *types.Project, options RunOptions) (int, error) {
+func (s *ServiceProxy) Exec(ctx context.Context, project string, options RunOptions) (int, error) {
 	if s.ExecFn == nil {
 		return 0, ErrNotImplemented
 	}
-	for _, i := range s.interceptors {
-		i(ctx, project)
-	}
 	return s.ExecFn(ctx, project, options)
 }
 
diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go
index 50bdf86a6..0652cc585 100644
--- a/pkg/compose/convergence.go
+++ b/pkg/compose/convergence.go
@@ -232,7 +232,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
 		name := getContainerName(project.Name, service, number)
 		i := i
 		eg.Go(func() error {
-			container, err := c.service.createContainer(ctx, project, service, name, number, false, true)
+			container, err := c.service.createContainer(ctx, project, service, name, number, false, true, false)
 			updated[actual+i] = container
 			return err
 		})
@@ -327,11 +327,11 @@ func getScale(config types.ServiceConfig) (int, error) {
 }
 
 func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
-	name string, number int, autoRemove bool, useNetworkAliases bool) (container moby.Container, err error) {
+	name string, number int, autoRemove bool, useNetworkAliases bool, attachStdin bool) (container moby.Container, err error) {
 	w := progress.ContextWriter(ctx)
 	eventName := "Container " + name
 	w.Event(progress.CreatingEvent(eventName))
-	container, err = s.createMobyContainer(ctx, project, service, name, number, nil, autoRemove, useNetworkAliases)
+	container, err = s.createMobyContainer(ctx, project, service, name, number, nil, autoRemove, useNetworkAliases, attachStdin)
 	if err != nil {
 		return
 	}
@@ -364,7 +364,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
 		inherited = &replaced
 	}
 	name = getContainerName(project.Name, service, number)
-	created, err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true)
+	created, err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true, false)
 	if err != nil {
 		return created, err
 	}
@@ -402,9 +402,9 @@ func (s *composeService) startContainer(ctx context.Context, container moby.Cont
 }
 
 func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
-	name string, number int, inherit *moby.Container, autoRemove bool, useNetworkAliases bool) (moby.Container, error) {
+	name string, number int, inherit *moby.Container, autoRemove bool, useNetworkAliases bool, attachStdin bool) (moby.Container, error) {
 	var created moby.Container
-	containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit, autoRemove)
+	containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit, autoRemove, attachStdin)
 	if err != nil {
 		return created, err
 	}
diff --git a/pkg/compose/create.go b/pkg/compose/create.go
index 9cd8f1018..3245752c9 100644
--- a/pkg/compose/create.go
+++ b/pkg/compose/create.go
@@ -17,8 +17,11 @@
 package compose
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"path"
 	"path/filepath"
 	"strconv"
@@ -86,7 +89,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
 		allServiceNames = append(allServiceNames, service.Name)
 	}
 	orphans := observedState.filter(isNotService(allServiceNames...))
-	if len(orphans) > 0 {
+	if len(orphans) > 0 && !options.IgnoreOrphans {
 		if options.RemoveOrphans {
 			w := progress.ContextWriter(ctx)
 			err := s.removeContainers(ctx, w, orphans, nil, false)
@@ -223,8 +226,8 @@ func getImageName(service types.ServiceConfig, projectName string) string {
 	return imageName
 }
 
-func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig, number int, inherit *moby.Container,
-	autoRemove bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
+func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig,
+	number int, inherit *moby.Container, autoRemove bool, attachStdin bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
 
 	labels, err := s.prepareLabels(p, service, number)
 	if err != nil {
@@ -243,9 +246,8 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 	}
 
 	var (
-		tty         = service.Tty
-		stdinOpen   = service.StdinOpen
-		attachStdin = false
+		tty       = service.Tty
+		stdinOpen = service.StdinOpen
 	)
 
 	volumeMounts, binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
@@ -345,6 +347,10 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 		volumesFrom = append(volumesFrom, v[len("container:"):])
 	}
 
+	securityOpts, err := parseSecurityOpts(p, service.SecurityOpt)
+	if err != nil {
+		return nil, nil, nil, err
+	}
 	hostConfig := container.HostConfig{
 		AutoRemove:     autoRemove,
 		Binds:          binds,
@@ -366,7 +372,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 		DNSSearch:      service.DNSSearch,
 		DNSOptions:     service.DNSOpts,
 		ExtraHosts:     service.ExtraHosts,
-		SecurityOpt:    service.SecurityOpt,
+		SecurityOpt:    securityOpts,
 		UsernsMode:     container.UsernsMode(service.UserNSMode),
 		Privileged:     service.Privileged,
 		PidMode:        container.PidMode(service.Pid),
@@ -378,6 +384,34 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 	return &containerConfig, &hostConfig, networkConfig, nil
 }
 
+// copy/pasted from https://github.com/docker/cli/blob/9de1b162f/cli/command/container/opts.go#L673-L697 + RelativePath
+// TODO find so way to share this code with docker/cli
+func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, error) {
+	for key, opt := range securityOpts {
+		con := strings.SplitN(opt, "=", 2)
+		if len(con) == 1 && con[0] != "no-new-privileges" {
+			if strings.Contains(opt, ":") {
+				con = strings.SplitN(opt, ":", 2)
+			} else {
+				return securityOpts, errors.Errorf("Invalid security-opt: %q", opt)
+			}
+		}
+		if con[0] == "seccomp" && con[1] != "unconfined" {
+			f, err := ioutil.ReadFile(p.RelativePath(con[1]))
+			if err != nil {
+				return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err)
+			}
+			b := bytes.NewBuffer(nil)
+			if err := json.Compact(b, f); err != nil {
+				return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err)
+			}
+			securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
+		}
+	}
+
+	return securityOpts, nil
+}
+
 func (s *composeService) prepareLabels(p *types.Project, service types.ServiceConfig, number int) (map[string]string, error) {
 	labels := map[string]string{}
 	for k, v := range service.Labels {
diff --git a/pkg/compose/exec.go b/pkg/compose/exec.go
index 74b14a6e2..6bd4b608f 100644
--- a/pkg/compose/exec.go
+++ b/pkg/compose/exec.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"io"
 
-	"github.com/compose-spec/compose-go/types"
 	"github.com/docker/cli/cli/streams"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
@@ -31,20 +30,15 @@ import (
 	"github.com/docker/compose/v2/pkg/api"
 )
 
-func (s *composeService) Exec(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
-	service, err := project.GetService(opts.Service)
-	if err != nil {
-		return 0, err
-	}
-
-	container, err := s.getExecTarget(ctx, project, service, opts)
+func (s *composeService) Exec(ctx context.Context, project string, opts api.RunOptions) (int, error) {
+	container, err := s.getExecTarget(ctx, project, opts)
 	if err != nil {
 		return 0, err
 	}
 
 	exec, err := s.apiClient.ContainerExecCreate(ctx, container.ID, moby.ExecConfig{
 		Cmd:        opts.Command,
-		Env:        s.getExecEnvironment(project, service, opts),
+		Env:        opts.Environment,
 		User:       opts.User,
 		Privileged: opts.Privileged,
 		Tty:        opts.Tty,
@@ -145,11 +139,11 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
 	}
 }
 
-func (s *composeService) getExecTarget(ctx context.Context, project *types.Project, service types.ServiceConfig, opts api.RunOptions) (moby.Container, error) {
+func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) {
 	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(
-			projectFilter(project.Name),
-			serviceFilter(service.Name),
+			projectFilter(projectName),
+			serviceFilter(opts.Service),
 			containerNumberFilter(opts.Index),
 		),
 	})
@@ -157,25 +151,12 @@ func (s *composeService) getExecTarget(ctx context.Context, project *types.Proje
 		return moby.Container{}, err
 	}
 	if len(containers) < 1 {
-		return moby.Container{}, fmt.Errorf("container %s not running", getContainerName(project.Name, service, opts.Index))
+		return moby.Container{}, fmt.Errorf("service %q is not running container #%d", opts.Service, opts.Index)
 	}
 	container := containers[0]
 	return container, nil
 }
 
-func (s *composeService) getExecEnvironment(project *types.Project, service types.ServiceConfig, opts api.RunOptions) []string {
-	var env []string
-	for k, v := range service.Environment.OverrideBy(types.NewMappingWithEquals(opts.Environment)).
-		Resolve(func(s string) (string, bool) {
-			v, ok := project.Environment[s]
-			return v, ok
-		}).
-		RemoveEmpty() {
-		env = append(env, fmt.Sprintf("%s=%s", k, *v))
-	}
-	return env
-}
-
 func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) {
 	resp, err := s.apiClient.ContainerExecInspect(ctx, execID)
 	if err != nil {
diff --git a/pkg/compose/logs.go b/pkg/compose/logs.go
index fcf1ca28d..5544c4923 100644
--- a/pkg/compose/logs.go
+++ b/pkg/compose/logs.go
@@ -37,7 +37,7 @@ func (s *composeService) Logs(ctx context.Context, projectName string, consumer
 	if options.Follow {
 		eg.Go(func() error {
 			printer := newLogPrinter(consumer)
-			return s.watchContainers(projectName, options.Services, printer.HandleEvent, containers, func(c types.Container) error {
+			return s.watchContainers(ctx, projectName, options.Services, printer.HandleEvent, containers, func(c types.Container) error {
 				return s.logContainers(ctx, consumer, c, options)
 			})
 		})
diff --git a/pkg/compose/ps.go b/pkg/compose/ps.go
index 1c27f2967..e1a76cbb4 100644
--- a/pkg/compose/ps.go
+++ b/pkg/compose/ps.go
@@ -18,7 +18,6 @@ package compose
 
 import (
 	"context"
-	"fmt"
 	"sort"
 
 	"golang.org/x/sync/errgroup"
@@ -46,12 +45,8 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
 				return container.Ports[i].PrivatePort < container.Ports[j].PrivatePort
 			})
 			for _, p := range container.Ports {
-				var url string
-				if p.PublicPort != 0 {
-					url = fmt.Sprintf("%s:%d", p.IP, p.PublicPort)
-				}
 				publishers = append(publishers, api.PortPublisher{
-					URL:           url,
+					URL:           p.IP,
 					TargetPort:    int(p.PrivatePort),
 					PublishedPort: int(p.PublicPort),
 					Protocol:      p.Type,
diff --git a/pkg/compose/run.go b/pkg/compose/run.go
index f40a3f6a1..8722e7c10 100644
--- a/pkg/compose/run.go
+++ b/pkg/compose/run.go
@@ -164,7 +164,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
 			return "", err
 		}
 	}
-	created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases)
+	created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, true)
 	if err != nil {
 		return "", err
 	}
@@ -186,6 +186,7 @@ func (s *composeService) getEscapeKeyProxy(r io.ReadCloser) (io.ReadCloser, erro
 
 func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
 	service.Tty = opts.Tty
+	service.StdinOpen = true
 	service.ContainerName = opts.Name
 
 	if len(opts.Command) > 0 {
@@ -209,6 +210,6 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
 		service.Environment.OverrideBy(projectEnv)
 	}
 	for k, v := range opts.Labels {
-		service.Labels.Add(k, v)
+		service.Labels = service.Labels.Add(k, v)
 	}
 }
diff --git a/pkg/compose/start.go b/pkg/compose/start.go
index cd0aa66bb..273fee6ab 100644
--- a/pkg/compose/start.go
+++ b/pkg/compose/start.go
@@ -47,7 +47,7 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti
 		}
 
 		eg.Go(func() error {
-			return s.watchContainers(project.Name, options.AttachTo, listener, attached, func(container moby.Container) error {
+			return s.watchContainers(context.Background(), project.Name, options.AttachTo, listener, attached, func(container moby.Container) error {
 				return s.attachContainer(ctx, container, listener, project)
 			})
 		})
@@ -69,13 +69,13 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti
 type containerWatchFn func(container moby.Container) error
 
 // watchContainers uses engine events to capture container start/die and notify ContainerEventListener
-func (s *composeService) watchContainers(projectName string, services []string, listener api.ContainerEventListener, containers Containers, onStart containerWatchFn) error {
+func (s *composeService) watchContainers(ctx context.Context, projectName string, services []string, listener api.ContainerEventListener, containers Containers, onStart containerWatchFn) error {
 	watched := map[string]int{}
 	for _, c := range containers {
 		watched[c.ID] = 0
 	}
 
-	ctx, stop := context.WithCancel(context.Background())
+	ctx, stop := context.WithCancel(ctx)
 	err := s.Events(ctx, projectName, api.EventsOptions{
 		Services: services,
 		Consumer: func(event api.Event) error {


Test patch:
diff --git a/pkg/compose/ps_test.go b/pkg/compose/ps_test.go
index 0644cc0c4..5de346729 100644
--- a/pkg/compose/ps_test.go
+++ b/pkg/compose/ps_test.go
@@ -54,7 +54,7 @@ func TestPs(t *testing.T) {
 
 	expected := []compose.ContainerSummary{
 		{ID: "123", Name: "123", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "healthy", Publishers: nil},
-		{ID: "456", Name: "456", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost:80", TargetPort: 90,
+		{ID: "456", Name: "456", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90,
 			PublishedPort: 80}}},
 		{ID: "789", Name: "789", Project: strings.ToLower(testProject), Service: "service2", State: "exited", Health: "", ExitCode: 130, Publishers: nil},
 	}