mirror of
https://github.com/docker/compose.git
synced 2026-05-13 13:58:02 +00:00
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:
parent
b043368028
commit
2f139b5c15
10 changed files with 463 additions and 0 deletions
|
|
@ -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
113
cmd/compose/deploy.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
28
docs/reference/compose_deploy.md
Normal file
28
docs/reference/compose_deploy.md
Normal 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-->
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
105
docs/reference/docker_compose_deploy.yaml
Normal file
105
docs/reference/docker_compose_deploy.yaml
Normal 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
|
||||
|
||||
|
|
@ -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
58
pkg/compose/deploy.go
Normal 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
124
pkg/e2e/deploy_test.go
Normal 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)
|
||||
}
|
||||
11
pkg/e2e/fixtures/deploy/compose.yaml
Normal file
11
pkg/e2e/fixtures/deploy/compose.yaml
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue