diff --git a/daemon/container.go b/daemon/container.go index c8cf9ef194..2a462692b8 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -79,6 +79,7 @@ type CommonContainer struct { MountLabel, ProcessLabel string RestartCount int HasBeenStartedBefore bool + HasBeenManuallyStopped bool // used for unless-stopped restart policy hostConfig *runconfig.HostConfig command *execdriver.Command monitor *containerMonitor @@ -1063,6 +1064,7 @@ func copyEscapable(dst io.Writer, src io.ReadCloser) (written int64, err error) func (container *Container) shouldRestart() bool { return container.hostConfig.RestartPolicy.Name == "always" || + (container.hostConfig.RestartPolicy.Name == "unless-stopped" && !container.HasBeenManuallyStopped) || (container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0) } diff --git a/daemon/daemon.go b/daemon/daemon.go index 0fb8b0f2f3..979a3f0d1d 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -103,6 +103,7 @@ type Daemon struct { EventsService *events.Events netController libnetwork.NetworkController root string + shutdown bool } // Get looks for a container using the provided information, which could be @@ -744,6 +745,7 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo } func (daemon *Daemon) Shutdown() error { + daemon.shutdown = true if daemon.containers != nil { group := sync.WaitGroup{} logrus.Debug("starting clean shutdown of all containers...") diff --git a/daemon/monitor.go b/daemon/monitor.go index 1f020574b0..21fd9a6b68 100644 --- a/daemon/monitor.go +++ b/daemon/monitor.go @@ -119,6 +119,10 @@ func (m *containerMonitor) Start() error { } m.Close() }() + // reset stopped flag + if m.container.HasBeenManuallyStopped { + m.container.HasBeenManuallyStopped = false + } // reset the restart count m.container.RestartCount = -1 @@ -223,11 +227,12 @@ func (m *containerMonitor) shouldRestart(exitCode int) bool { // do not restart if the user or docker has requested that this container be stopped if m.shouldStop { + m.container.HasBeenManuallyStopped = !m.container.daemon.shutdown return false } switch { - case m.restartPolicy.IsAlways(): + case m.restartPolicy.IsAlways(), m.restartPolicy.IsUnlessStopped(): return true case m.restartPolicy.IsOnFailure(): // the default value of 0 for MaximumRetryCount means that we will not enforce a maximum count diff --git a/docs/reference/api/docker_remote_api_v1.21.md b/docs/reference/api/docker_remote_api_v1.21.md index 7a3545a0e6..5cd2a00a41 100644 --- a/docs/reference/api/docker_remote_api_v1.21.md +++ b/docs/reference/api/docker_remote_api_v1.21.md @@ -276,7 +276,8 @@ Json Parameters: - **Capdrop** - A list of kernel capabilities to drop from the container. - **RestartPolicy** – The behavior to apply when the container exits. The value is an object with a `Name` property of either `"always"` to - always restart or `"on-failure"` to restart only when the container + always restart, `"unless-stopped"` to restart always except when + user has manually stopped the container or `"on-failure"` to restart only when the container exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` controls the number of times to retry before giving up. The default is not to restart. (optional) diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index 8c093f18eb..9aa340e88f 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -58,7 +58,7 @@ Creates a new container. --pid="" PID namespace to use --privileged=false Give extended privileges to this container --read-only=false Mount the container's root filesystem as read only - --restart="no" Restart policy (no, on-failure[:max-retry], always) + --restart="no" Restart policy (no, on-failure[:max-retry], always, unless-stopped) --security-opt=[] Security options -t, --tty=false Allocate a pseudo-TTY --disable-content-trust=true Skip image verification diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index ab900d41b9..5791bd74d1 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -58,7 +58,7 @@ weight=1 --pid="" PID namespace to use --privileged=false Give extended privileges to this container --read-only=false Mount the container's root filesystem as read only - --restart="no" Restart policy (no, on-failure[:max-retry], always) + --restart="no" Restart policy (no, on-failure[:max-retry], always, unless-stopped) --rm=false Automatically remove the container when it exits --security-opt=[] Security Options --sig-proxy=true Proxy received signals to the process @@ -440,7 +440,16 @@ Docker supports the following restart policies: Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart - the container indefinitely. + the container indefinitely. The container will also always start + on daemon startup, regardless of the current state of the container. + + + + unless-stopped + + Always restart the container regardless of the exit status, but + do not start it on daemon startup if the container has been put + to a stopped state before. diff --git a/docs/reference/run.md b/docs/reference/run.md index df3f656ac6..4fd2c0f0c2 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -398,7 +398,16 @@ Docker supports the following restart policies: Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart - the container indefinitely. + the container indefinitely. The container will also always start + on daemon startup, regardless of the current state of the container. + + + + unless-stopped + + Always restart the container regardless of the exit status, but + do not start it on daemon startup if the container has been put + to a stopped state before. diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index b6bc10ee39..5cb9dbacd5 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -87,6 +87,60 @@ func (s *DockerDaemonSuite) TestDaemonRestartWithVolumesRefs(c *check.C) { } } +// #11008 +func (s *DockerDaemonSuite) TestDaemonRestartUnlessStopped(c *check.C) { + err := s.d.StartWithBusybox() + c.Assert(err, check.IsNil) + + out, err := s.d.Cmd("run", "-d", "--name", "top1", "--restart", "always", "busybox:latest", "top") + c.Assert(err, check.IsNil, check.Commentf("run top1: %v", out)) + + out, err = s.d.Cmd("run", "-d", "--name", "top2", "--restart", "unless-stopped", "busybox:latest", "top") + c.Assert(err, check.IsNil, check.Commentf("run top2: %v", out)) + + testRun := func(m map[string]bool, prefix string) { + var format string + for name, shouldRun := range m { + out, err := s.d.Cmd("ps") + c.Assert(err, check.IsNil, check.Commentf("run ps: %v", out)) + if shouldRun { + format = "%scontainer %q is not running" + } else { + format = "%scontainer %q is running" + } + c.Assert(strings.Contains(out, name), check.Equals, shouldRun, check.Commentf(format, prefix, name)) + } + } + + // both running + testRun(map[string]bool{"top1": true, "top2": true}, "") + + out, err = s.d.Cmd("stop", "top1") + c.Assert(err, check.IsNil, check.Commentf(out)) + + out, err = s.d.Cmd("stop", "top2") + c.Assert(err, check.IsNil, check.Commentf(out)) + + // both stopped + testRun(map[string]bool{"top1": false, "top2": false}, "") + + err = s.d.Restart() + c.Assert(err, check.IsNil) + + // restart=always running + testRun(map[string]bool{"top1": true, "top2": false}, "After daemon restart: ") + + out, err = s.d.Cmd("start", "top2") + c.Assert(err, check.IsNil, check.Commentf("start top2: %v", out)) + + err = s.d.Restart() + c.Assert(err, check.IsNil) + + // both running + testRun(map[string]bool{"top1": true, "top2": true}, "After second daemon restart: ") + +} + func (s *DockerDaemonSuite) TestDaemonStartIptablesFalse(c *check.C) { if err := s.d.Start("--iptables=false"); err != nil { c.Fatalf("we should have been able to start the daemon with passing iptables=false: %v", err) diff --git a/man/docker-create.1.md b/man/docker-create.1.md index fb70cabf4a..59179fe506 100644 --- a/man/docker-create.1.md +++ b/man/docker-create.1.md @@ -226,7 +226,7 @@ This value should always larger than **-m**, so you should always use this with Mount the container's root filesystem as read only. **--restart**="no" - Restart policy to apply when a container exits (no, on-failure[:max-retry], always) + Restart policy to apply when a container exits (no, on-failure[:max-retry], always, unless-stopped). **--security-opt**=[] Security Options diff --git a/man/docker-run.1.md b/man/docker-run.1.md index d48e412573..50a5013ee8 100644 --- a/man/docker-run.1.md +++ b/man/docker-run.1.md @@ -360,7 +360,7 @@ to write files anywhere. By specifying the `--read-only` flag the container wil its root filesystem mounted as read only prohibiting any writes. **--restart**="no" - Restart policy to apply when a container exits (no, on-failure[:max-retry], always) + Restart policy to apply when a container exits (no, on-failure[:max-retry], always, unless-stopped). **--rm**=*true*|*false* Automatically remove the container when it exits (incompatible with -d). The default is *false*. diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index f6a89a314a..3266f1577f 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -140,6 +140,13 @@ func (rp *RestartPolicy) IsOnFailure() bool { return rp.Name == "on-failure" } +// IsUnlessStopped indicates whether the container has the +// "unless-stopped" restart policy. This means the container will +// automatically restart unless user has put it to stopped state. +func (rp *RestartPolicy) IsUnlessStopped() bool { + return rp.Name == "unless-stopped" +} + // LogConfig represents the logging configuration of the container. type LogConfig struct { Type string diff --git a/runconfig/parse.go b/runconfig/parse.go index 5528d7af76..8c2e4060a3 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -450,9 +450,9 @@ func ParseRestartPolicy(policy string) (RestartPolicy, error) { p.Name = name switch name { - case "always": + case "always", "unless-stopped": if len(parts) > 1 { - return p, fmt.Errorf("maximum restart count not valid with restart policy of \"always\"") + return p, fmt.Errorf("maximum restart count not valid with restart policy of \"%s\"", name) } case "no": // do nothing