Add docker compose deploy command

Implements a production-oriented deploy command that builds, pushes, and
applies a Compose project to a Docker server with force-recreate semantics
and optional health-check-based wait for zero-downtime deployments.

Signed-off-by: Eric Curtin <eric.curtin@docker.com>
This commit is contained in:
Eric Curtin 2026-03-18 21:45:00 +00:00
parent b043368028
commit 2f139b5c15
10 changed files with 463 additions and 0 deletions

View file

@ -615,6 +615,7 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
statsCommand(&opts, dockerCli),
watchCommand(&opts, dockerCli, backendOptions),
publishCommand(&opts, dockerCli, backendOptions),
deployCommand(&opts, dockerCli, backendOptions),
alphaCommand(&opts, dockerCli, backendOptions),
bridgeCommand(&opts, dockerCli),
volumesCommand(&opts, dockerCli, backendOptions),

113
cmd/compose/deploy.go Normal file
View file

@ -0,0 +1,113 @@
/*
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.
*/
package compose
import (
"context"
"fmt"
"time"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type deployOptions struct {
*ProjectOptions
composeOptions
build bool
noBuild bool
push bool
quiet bool
removeOrphans bool
wait bool
waitTimeout int
}
func deployCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := deployOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "deploy [OPTIONS] [SERVICE...]",
Short: "Deploy a Compose application to a Docker server",
Long: `Deploy a Compose application to a Docker server.
This command applies the Compose project to the target Docker server,
recreating containers with updated configuration and images. Images are
pulled from the registry unless --build is specified.
Use health checks defined in the Compose file to ensure zero-downtime
deployments by passing --wait.`,
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if opts.waitTimeout < 0 {
return fmt.Errorf("--wait-timeout must be a non-negative integer")
}
if opts.build && opts.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runDeploy(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.BoolVar(&opts.build, "build", false, "Build images before deploying")
flags.BoolVar(&opts.noBuild, "no-build", false, "Do not build images even if build configuration is defined")
flags.BoolVar(&opts.push, "push", false, "Push images to registry before deploying")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress pull/push progress output")
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
flags.BoolVar(&opts.wait, "wait", false, "Wait for services to be healthy before returning")
flags.IntVar(&opts.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for services to be healthy (0 = no timeout)")
return cmd
}
func runDeploy(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts deployOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
deployOpts := api.DeployOptions{
Push: opts.push,
Quiet: opts.quiet,
RemoveOrphans: opts.removeOrphans,
Wait: opts.wait,
Services: services,
}
if opts.waitTimeout > 0 {
deployOpts.WaitTimeout = time.Duration(opts.waitTimeout) * time.Second
}
if opts.build && !opts.noBuild {
deployOpts.Build = &api.BuildOptions{
Services: services,
}
}
return backend.Deploy(ctx, project, deployOpts)
}

View file

@ -19,6 +19,7 @@ Define and run multi-container applications with Docker
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem |
| [`create`](compose_create.md) | Creates containers for a service |
| [`deploy`](compose_deploy.md) | Deploy a Compose application to a Docker server |
| [`down`](compose_down.md) | Stop and remove containers, networks |
| [`events`](compose_events.md) | Receive real time events from containers |
| [`exec`](compose_exec.md) | Execute a command in a running container |

View file

@ -0,0 +1,28 @@
# docker compose deploy
<!---MARKER_GEN_START-->
Deploy a Compose application to a Docker server.
This command applies the Compose project to the target Docker server,
recreating containers with updated configuration and images. Images are
pulled from the registry unless --build is specified.
Use health checks defined in the Compose file to ensure zero-downtime
deployments by passing --wait.
### Options
| Name | Type | Default | Description |
|:-------------------|:-------|:--------|:--------------------------------------------------------------------------------|
| `--build` | `bool` | | Build images before deploying |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--no-build` | `bool` | | Do not build images even if build configuration is defined |
| `--push` | `bool` | | Push images to registry before deploying |
| `-q`, `--quiet` | `bool` | | Suppress pull/push progress output |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--wait` | `bool` | | Wait for services to be healthy before returning |
| `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for services to be healthy (0 = no timeout) |
<!---MARKER_GEN_END-->

View file

@ -12,6 +12,7 @@ cname:
- docker compose config
- docker compose cp
- docker compose create
- docker compose deploy
- docker compose down
- docker compose events
- docker compose exec
@ -48,6 +49,7 @@ clink:
- docker_compose_config.yaml
- docker_compose_cp.yaml
- docker_compose_create.yaml
- docker_compose_deploy.yaml
- docker_compose_down.yaml
- docker_compose_events.yaml
- docker_compose_exec.yaml

View file

@ -0,0 +1,105 @@
command: docker compose deploy
short: Deploy a Compose application to a Docker server
long: |-
Deploy a Compose application to a Docker server.
This command applies the Compose project to the target Docker server,
recreating containers with updated configuration and images. Images are
pulled from the registry unless --build is specified.
Use health checks defined in the Compose file to ensure zero-downtime
deployments by passing --wait.
usage: docker compose deploy [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: build
value_type: bool
default_value: "false"
description: Build images before deploying
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-build
value_type: bool
default_value: "false"
description: Do not build images even if build configuration is defined
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: push
value_type: bool
default_value: "false"
description: Push images to registry before deploying
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Suppress pull/push progress output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: remove-orphans
value_type: bool
default_value: "false"
description: Remove containers for services not defined in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: wait
value_type: bool
default_value: "false"
description: Wait for services to be healthy before returning
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: wait-timeout
value_type: int
default_value: "0"
description: |
Maximum duration in seconds to wait for services to be healthy (0 = no timeout)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View file

@ -149,6 +149,26 @@ type Compose interface {
Volumes(ctx context.Context, project string, options VolumesOptions) ([]VolumesSummary, error)
// LoadProject loads and validates a Compose project from configuration files.
LoadProject(ctx context.Context, options ProjectLoadOptions) (*types.Project, error)
// Deploy executes the equivalent to a `compose deploy`
Deploy(ctx context.Context, project *types.Project, options DeployOptions) error
}
// DeployOptions group options of the Deploy API
type DeployOptions struct {
// Build rebuilds service images before deploying
Build *BuildOptions
// Push pushes images to the registry before deploying
Push bool
// Quiet suppresses pull/push progress output
Quiet bool
// RemoveOrphans removes containers for services not defined in the project
RemoveOrphans bool
// Services is the list of services to deploy (defaults to all)
Services []string
// Wait waits for services to be healthy after deploy
Wait bool
// WaitTimeout is the maximum time to wait for services to become healthy
WaitTimeout time.Duration
}
type VolumesOptions struct {

58
pkg/compose/deploy.go Normal file
View file

@ -0,0 +1,58 @@
/*
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.
*/
package compose
import (
"context"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v5/pkg/api"
)
func (s *composeService) Deploy(ctx context.Context, project *types.Project, options api.DeployOptions) error {
if options.Build != nil {
if err := s.Build(ctx, project, *options.Build); err != nil {
return err
}
}
if options.Push {
if err := s.Push(ctx, project, api.PushOptions{
Quiet: options.Quiet,
}); err != nil {
return err
}
}
return s.Up(ctx, project, api.UpOptions{
Create: api.CreateOptions{
Services: options.Services,
Recreate: api.RecreateForce,
RecreateDependencies: api.RecreateForce,
RemoveOrphans: options.RemoveOrphans,
Inherit: true,
QuietPull: options.Quiet,
},
Start: api.StartOptions{
Project: project,
Services: options.Services,
Wait: options.Wait,
WaitTimeout: options.WaitTimeout,
},
})
}

124
pkg/e2e/deploy_test.go Normal file
View file

@ -0,0 +1,124 @@
//go:build !windows
/*
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.
*/
package e2e
import (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
)
func TestDeploy(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-deploy"
reset := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v", "--remove-orphans")
}
reset()
t.Cleanup(reset)
t.Log("Deploy the application")
c.RunDockerComposeCmd(t, "-f", "fixtures/deploy/compose.yaml", "--project-name", projectName, "deploy", "-d")
t.Log("Verify service is running")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--format", "json")
output := res.Stdout()
assert.Assert(t, strings.Contains(output, "running"), "Expected service to be running, got: %s", output)
}
func TestDeployWait(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-deploy-wait"
reset := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v", "--remove-orphans")
}
reset()
t.Cleanup(reset)
t.Log("Deploy the application with --wait")
timeout := time.After(30 * time.Second)
done := make(chan bool)
go func() {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/deploy/compose.yaml", "--project-name", projectName, "deploy", "--wait")
assert.Assert(t, strings.Contains(res.Combined(), projectName), "Expected project name in output")
done <- true
}()
select {
case <-timeout:
t.Fatal("deploy --wait did not complete in time")
case <-done:
break
}
t.Log("Verify service is healthy")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--format", "json")
output := res.Stdout()
assert.Assert(t, strings.Contains(output, "running"), "Expected service to be running, got: %s", output)
}
func TestDeployBuild(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-deploy-build"
reset := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v", "--remove-orphans")
}
reset()
t.Cleanup(reset)
t.Log("Deploy the application with --build")
c.RunDockerComposeCmd(t, "-f", "fixtures/deploy/compose.yaml", "--project-name", projectName, "deploy", "--build", "-d")
t.Log("Verify service is running")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--format", "json")
output := res.Stdout()
assert.Assert(t, strings.Contains(output, "running"), "Expected service to be running, got: %s", output)
}
func TestDeployRemoveOrphans(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-deploy-orphans"
reset := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v", "--remove-orphans")
}
reset()
t.Cleanup(reset)
t.Log("Deploy the application")
c.RunDockerComposeCmd(t, "-f", "fixtures/deploy/compose.yaml", "--project-name", projectName, "deploy", "-d")
t.Log("Verify service is running")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--format", "json")
output := res.Stdout()
assert.Assert(t, strings.Contains(output, "running"), "Expected service to be running, got: %s", output)
t.Log("Deploy with --remove-orphans")
c.RunDockerComposeCmd(t, "-f", "fixtures/deploy/compose.yaml", "--project-name", projectName, "deploy", "--remove-orphans", "-d")
t.Log("Verify service is still running")
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--format", "json")
output = res.Stdout()
assert.Assert(t, strings.Contains(output, "running"), "Expected service to be running, got: %s", output)
}

View file

@ -0,0 +1,11 @@
services:
web:
image: nginx:alpine
ports:
- "8080:80"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 1s
timeout: 3s
retries: 3
start_period: 2s