@@ -22,6 +22,7 @@ import (
2222 "github.com/docker/cli/internal/volumespec"
2323 "github.com/docker/cli/opts"
2424 "github.com/docker/go-connections/nat"
25+ "github.com/google/shlex"
2526 "github.com/moby/moby/api/types/container"
2627 "github.com/moby/moby/api/types/mount"
2728 "github.com/moby/moby/api/types/network"
@@ -131,6 +132,7 @@ type containerOptions struct {
131132 shmSize opts.MemBytes
132133 noHealthcheck bool
133134 healthCmd string
135+ healthCmdExec bool
134136 healthInterval time.Duration
135137 healthTimeout time.Duration
136138 healthStartPeriod time.Duration
@@ -261,6 +263,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
261263
262264 // Health-checking
263265 flags .StringVar (& copts .healthCmd , "health-cmd" , "" , "Command to run to check health" )
266+ flags .BoolVar (& copts .healthCmdExec , "health-cmd-exec" , false , "Use the exec (CMD) healthcheck form instead of CMD-SHELL (for images without a shell)" )
264267 flags .DurationVar (& copts .healthInterval , "health-interval" , 0 , "Time between running the check (ms|s|m|h) (default 0s)" )
265268 flags .IntVar (& copts .healthRetries , "health-retries" , 0 , "Consecutive failures needed to report unhealthy" )
266269 flags .DurationVar (& copts .healthTimeout , "health-timeout" , 0 , "Maximum time to allow one check to run (ms|s|m|h) (default 0s)" )
@@ -560,6 +563,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
560563 // Healthcheck
561564 var healthConfig * container.HealthConfig
562565 haveHealthSettings := copts .healthCmd != "" ||
566+ copts .healthCmdExec ||
563567 copts .healthInterval != 0 ||
564568 copts .healthTimeout != 0 ||
565569 copts .healthStartPeriod != 0 ||
@@ -571,9 +575,16 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
571575 }
572576 healthConfig = & container.HealthConfig {Test : []string {"NONE" }}
573577 } else if haveHealthSettings {
578+ if copts .healthCmdExec && copts .healthCmd == "" {
579+ return nil , errors .New ("--health-cmd-exec requires --health-cmd" )
580+ }
574581 var probe []string
575582 if copts .healthCmd != "" {
576- probe = []string {"CMD-SHELL" , copts .healthCmd }
583+ var err error
584+ probe , err = healthcheckProbe (copts .healthCmd , copts .healthCmdExec )
585+ if err != nil {
586+ return nil , err
587+ }
577588 }
578589 if copts .healthInterval < 0 {
579590 return nil , errors .New ("--health-interval cannot be negative" )
@@ -1130,6 +1141,23 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
11301141 return val , nil
11311142}
11321143
1144+ // healthcheckProbe builds the HealthConfig.Test slice for --health-cmd.
1145+ // When execForm is false, the command runs via CMD-SHELL; when true, via CMD
1146+ // (exec form), which is required for images without a shell (for example scratch).
1147+ func healthcheckProbe (cmd string , execForm bool ) ([]string , error ) {
1148+ if ! execForm {
1149+ return []string {"CMD-SHELL" , cmd }, nil
1150+ }
1151+ parts , err := shlex .Split (cmd )
1152+ if err != nil {
1153+ return nil , fmt .Errorf ("--health-cmd: %w" , err )
1154+ }
1155+ if len (parts ) == 0 {
1156+ return nil , errors .New ("--health-cmd: command must not be empty" )
1157+ }
1158+ return append ([]string {"CMD" }, parts ... ), nil
1159+ }
1160+
11331161// validateAttach validates that the specified string is a valid attach option.
11341162func validateAttach (val string ) (string , error ) {
11351163 s := strings .ToLower (val )
0 commit comments