Repository: docker/compose

Issue:
support --env-file in docker-compose run command
NOTE: this is different than the `.env` file that is read by docker-compose commands and used for substitutions in the composition file... first mentioned here: https://github.com/docker/compose/issues/6170#issuecomment-673914083

---

We generate an env file that we would like to include via the `docker-compose run` command. e.g.

```
echo "FOO=bar" > vars.env
docker-compose run --env-file=vars.env --rm test-service
```

`docker run` supports this flag and it would be great if we saw `docker-compose run` maintain parity. 

Is there planned support for this feature -- or will it be implemented in composev2 / "docker <space> compose" ? 

our generated vars file is cumbersome (with multiline values) and relatively complicated to convert to various `-e FOO=bar` flags...

thanks!



Discussion:
@KoditkarVedant: @chris-crone @ndeloof Hi guys, I would like to work on this issue. I am not new to Go lang. 

I have got a question. If I pass `--env-file` option to run command then should we append that to `env-file` provided as part of service config in docker-compose.yaml file or should we override them? @KoditkarVedant: In meantime I've created a draft PR which assumes that if you pass --env-file from CLI it overrides the env-file specified in docker-compose.yaml file. I will update PR as per your suggestion if we should append them.@KoditkarVedant: @chris-crone @ndeloof @ulyssessouza Friendly ping.@briceburg: I would think would want to append the --env-file(s) passed via the cli to any included in the service's definition. appending would override any previously defined keys.@reggermont: I'm surprised this issue hasn't more discussions.

Just had this case in a project and that's a deal breaker, will have to generate a huge `docker run` command just because of that issue

I think there's a misconception with the original `--env-file` option which should never have the same name than `docker`'s `--env-file`.

Now [the topic is stuck because it may conflict with the original `--env-file`](https://github.com/docker/compose/pull/9169#issuecomment-1065099236)

What about a `--env-from` instead?@ndeloof: That's indeed sad this flag name was selected, but this is the legacy we have to deal with.
We could use `--service-env-file` or something comparable, even verbose, to workaround this ambiguity@KoditkarVedant: @ndeloof I have updated the PR with changes suggested.

Solution patch:
diff --git a/cmd/compose/run.go b/cmd/compose/run.go
index c5df79e0..137c2526 100644
--- a/cmd/compose/run.go
+++ b/cmd/compose/run.go
@@ -19,8 +19,10 @@ package compose
 import (
 	"context"
 	"fmt"
+	"os"
 	"strings"
 
+	"github.com/compose-spec/compose-go/v2/dotenv"
 	"github.com/compose-spec/compose-go/v2/format"
 	xprogress "github.com/moby/buildkit/util/progress/progressui"
 	"github.com/sirupsen/logrus"
@@ -44,6 +46,7 @@ type runOptions struct {
 	Service       string
 	Command       []string
 	environment   []string
+	envFiles      []string
 	Detach        bool
 	Remove        bool
 	noTty         bool
@@ -116,6 +119,29 @@ func (options runOptions) apply(project *types.Project) (*types.Project, error)
 	return project, nil
 }
 
+func (options runOptions) getEnvironment() (types.Mapping, error) {
+	environment := types.NewMappingWithEquals(options.environment).Resolve(os.LookupEnv).ToMapping()
+	for _, file := range options.envFiles {
+		f, err := os.Open(file)
+		if err != nil {
+			return nil, err
+		}
+		vars, err := dotenv.ParseWithLookup(f, func(k string) (string, bool) {
+			value, ok := environment[k]
+			return value, ok
+		})
+		if err != nil {
+			return nil, nil
+		}
+		for k, v := range vars {
+			if _, ok := environment[k]; !ok {
+				environment[k] = v
+			}
+		}
+	}
+	return environment, nil
+}
+
 func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
 	options := runOptions{
 		composeOptions: &composeOptions{
@@ -175,6 +201,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
 	flags := cmd.Flags()
 	flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
 	flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
+	flags.StringArrayVar(&options.envFiles, "env-from-file", []string{}, "Set environment variables from file")
 	flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
 	flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
 	flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected)")
@@ -264,6 +291,11 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
 		buildForRun = &bo
 	}
 
+	environment, err := options.getEnvironment()
+	if err != nil {
+		return err
+	}
+
 	// start container and attach to container streams
 	runOpts := api.RunOptions{
 		Build:             buildForRun,
@@ -278,7 +310,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
 		User:              options.user,
 		CapAdd:            options.capAdd.GetAll(),
 		CapDrop:           options.capDrop.GetAll(),
-		Environment:       options.environment,
+		Environment:       environment.Values(),
 		Entrypoint:        options.entrypointCmd,
 		Labels:            labels,
 		UseNetworkAliases: options.useAliases,
diff --git a/docs/reference/compose_run.md b/docs/reference/compose_run.md
index f46d1872..e4be01d2 100644
--- a/docs/reference/compose_run.md
+++ b/docs/reference/compose_run.md
@@ -66,6 +66,7 @@ specified in the service configuration.
 | `--dry-run`             | `bool`        |          | Execute command in dry run mode                                                  |
 | `--entrypoint`          | `string`      |          | Override the entrypoint of the image                                             |
 | `-e`, `--env`           | `stringArray` |          | Set environment variables                                                        |
+| `--env-from-file`       | `stringArray` |          | Set environment variables from file                                              |
 | `-i`, `--interactive`   | `bool`        | `true`   | Keep STDIN open even if not attached                                             |
 | `-l`, `--label`         | `stringArray` |          | Add or override a label                                                          |
 | `--name`                | `string`      |          | Assign a name to the container                                                   |
diff --git a/docs/reference/docker_compose_run.yaml b/docs/reference/docker_compose_run.yaml
index dc19a95d..6e6ec71f 100644
--- a/docs/reference/docker_compose_run.yaml
+++ b/docs/reference/docker_compose_run.yaml
@@ -117,6 +117,16 @@ options:
       experimentalcli: false
       kubernetes: false
       swarm: false
+    - option: env-from-file
+      value_type: stringArray
+      default_value: '[]'
+      description: Set environment variables from file
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
     - option: interactive
       shorthand: i
       value_type: bool



Test patch:
diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go
index 438cd6f6..fa8fda68 100644
--- a/pkg/e2e/compose_run_test.go
+++ b/pkg/e2e/compose_run_test.go
@@ -178,4 +178,10 @@ func TestLocalComposeRun(t *testing.T) {
 		assert.Assert(t, strings.Contains(res.Combined(), "backend Pulling"), res.Combined())
 		assert.Assert(t, strings.Contains(res.Combined(), "backend Pulled"), res.Combined())
 	})
+
+	t.Run("compose run --env-from-file", func(t *testing.T) {
+		res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env-from-file", "./fixtures/run-test/run.env",
+			"front", "env")
+		res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
+	})
 }
diff --git a/pkg/e2e/fixtures/run-test/compose.yaml b/pkg/e2e/fixtures/run-test/compose.yaml
index 0168dc24..8162dba0 100644
--- a/pkg/e2e/fixtures/run-test/compose.yaml
+++ b/pkg/e2e/fixtures/run-test/compose.yaml
@@ -1,4 +1,3 @@
-version: '3.8'
 services:
   back:
     image: alpine
diff --git a/pkg/e2e/fixtures/run-test/run.env b/pkg/e2e/fixtures/run-test/run.env
new file mode 100644
index 00000000..6ac867af
--- /dev/null
+++ b/pkg/e2e/fixtures/run-test/run.env
@@ -0,0 +1 @@
+FOO=BAR