diff --git a/daemon/container.go b/daemon/container.go index 855ce0296c..23959c5e64 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 @@ -1066,6 +1067,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 1e68c0e37e..0df49d5a22 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 @@ -749,6 +750,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 70e2c5a2a3..d8332b2765 100644 --- a/docs/reference/api/docker_remote_api_v1.21.md +++ b/docs/reference/api/docker_remote_api_v1.21.md @@ -278,7 +278,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 fd7db14e6f..74d5185a34 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -59,7 +59,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 e563ab6382..6ee97906d9 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -59,7 +59,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 @@ -441,7 +441,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 036918901d..1dfef0c70f 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -402,7 +402,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 f31823b91e..54b773f81e 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 06965332dd..13ea642441 100644 --- a/man/docker-create.1.md +++ b/man/docker-create.1.md @@ -234,7 +234,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 b49c9b69d6..7fb9d0f162 100644 --- a/man/docker-run.1.md +++ b/man/docker-run.1.md @@ -370,7 +370,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 818abdce92..6000a94dc0 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 a6a6b62345..14aff6bb0c 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -426,9 +426,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