diff --git a/cli/command/service/update.go b/cli/command/service/update.go
index 2e7a0ee25fce..003d0c43805f 100644
--- a/cli/command/service/update.go
+++ b/cli/command/service/update.go
@@ -1219,10 +1219,13 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
return nil
}
-// updateLogDriver updates the log driver only if the log driver flag is set.
-// All options will be replaced with those provided on the command line.
+// updateLogDriver updates the log driver only if the log driver or log options
+// flags are set. If the log driver flag is set, all options will be replaced
+// with those provided on the command line. If only log options are set, the
+// service's current log driver is kept, and the options provided are merged
+// with its existing options.
func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
- if !flags.Changed(flagLogDriver) {
+ if !anyChanged(flags, flagLogDriver, flagLogOpt) {
return nil
}
@@ -1230,6 +1233,15 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
if err != nil {
return err
}
+ options := opts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetSlice())
+
+ if !flags.Changed(flagLogDriver) && taskTemplate.LogDriver != nil {
+ name = taskTemplate.LogDriver.Name
+ merged := make(map[string]string, len(taskTemplate.LogDriver.Options)+len(options))
+ maps.Copy(merged, taskTemplate.LogDriver.Options)
+ maps.Copy(merged, options)
+ options = merged
+ }
if name == "" {
return nil
@@ -1237,7 +1249,7 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
taskTemplate.LogDriver = &swarm.Driver{
Name: name,
- Options: opts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetSlice()),
+ Options: options,
}
return nil
diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go
index 51ba0eaec24f..f7f92397164d 100644
--- a/cli/command/service/update_test.go
+++ b/cli/command/service/update_test.go
@@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
+ "maps"
"net/netip"
"slices"
"strconv"
@@ -333,6 +334,94 @@ func TestUpdatePortsDuplicate(t *testing.T) {
assert.Check(t, is.Equal(uint32(80), portConfigs[0].TargetPort))
}
+func TestUpdateLogDriver(t *testing.T) {
+ tests := []struct {
+ name string
+ flags [][2]string
+ initial *swarm.Driver
+ expected *swarm.Driver
+ }{
+ {
+ name: "no changes",
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ },
+ {
+ name: "set driver and options",
+ flags: [][2]string{{flagLogDriver, "local"}, {flagLogOpt, "max-file=15"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "local", Options: map[string]string{"max-file": "15"}},
+ },
+ {
+ name: "set driver without options",
+ flags: [][2]string{{flagLogDriver, "local"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "local", Options: map[string]string{}},
+ },
+ {
+ name: "set empty driver",
+ flags: [][2]string{{flagLogDriver, ""}, {flagLogOpt, "max-file=15"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ },
+ {
+ name: "set options with existing driver",
+ flags: [][2]string{{flagLogOpt, "max-file=15"}, {flagLogOpt, "max-size=10m"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "15", "max-size": "10m"}},
+ },
+ {
+ name: "set options merges with existing options",
+ flags: [][2]string{{flagLogOpt, "max-size=10m"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5", "max-size": "10m"}},
+ },
+ {
+ name: "set options overrides existing option",
+ flags: [][2]string{{flagLogOpt, "max-file=3"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5", "max-size": "10m"}},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "3", "max-size": "10m"}},
+ },
+ {
+ name: "set options with existing driver without options",
+ flags: [][2]string{{flagLogOpt, "max-file=15"}},
+ initial: &swarm.Driver{Name: "json-file"},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "15"}},
+ },
+ {
+ name: "set driver and options replaces existing options",
+ flags: [][2]string{{flagLogDriver, "json-file"}, {flagLogOpt, "max-size=10m"}},
+ initial: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-file": "5"}},
+ expected: &swarm.Driver{Name: "json-file", Options: map[string]string{"max-size": "10m"}},
+ },
+ {
+ // Options are not applied if no log driver is set on the service,
+ // as they would be applied to the daemon's default log driver,
+ // which may differ between nodes.
+ name: "set options without existing driver",
+ flags: [][2]string{{flagLogOpt, "max-file=15"}},
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ flags := newUpdateCommand(nil).Flags()
+ for _, flag := range tc.flags {
+ assert.NilError(t, flags.Set(flag[0], flag[1]))
+ }
+ var initial *swarm.Driver
+ if tc.initial != nil {
+ initial = &swarm.Driver{Name: tc.initial.Name, Options: maps.Clone(tc.initial.Options)}
+ }
+ taskTemplate := &swarm.TaskSpec{LogDriver: tc.initial}
+ err := updateLogDriver(flags, taskTemplate)
+ assert.NilError(t, err)
+ assert.Check(t, is.DeepEqual(taskTemplate.LogDriver, tc.expected))
+ // The existing log driver must not be modified in place.
+ assert.Check(t, is.DeepEqual(tc.initial, initial))
+ })
+ }
+}
+
func TestUpdateHealthcheckTable(t *testing.T) {
type test struct {
flags [][2]string
diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md
index 64f81b07e80f..d7ae7a139450 100644
--- a/docs/reference/commandline/service_update.md
+++ b/docs/reference/commandline/service_update.md
@@ -51,7 +51,7 @@ Update a service
| `--limit-memory` | `bytes` | `0` | Limit Memory |
| `--limit-pids` | `int64` | `0` | Limit maximum number of processes (default 0 = unlimited) |
| `--log-driver` | `string` | | Logging driver for service |
-| `--log-opt` | `list` | | Logging driver options |
+| [`--log-opt`](#log-opt) | `list` | | Logging driver options |
| `--max-concurrent` | `uint` | | Number of job tasks to run concurrently (default equal to --replicas) |
| `--memory-swap` | `bytes` | `0` | Swap Bytes (-1 for unlimited) |
| `--memory-swappiness` | `int64` | `-1` | Tune memory swappiness (0-100), -1 to reset to default |
@@ -211,6 +211,37 @@ $ docker service update \
myservice
```
+### Update logging driver options (--log-driver, --log-opt)
+
+Use the `--log-opt` flag to update the options of the logging driver that is
+configured for a service. If `--log-driver` is not set, the service keeps its
+current logging driver, and the options that you set are merged with the
+options that are already set for the service. If the service has no logging
+driver configured, `--log-opt` has no effect unless `--log-driver` is also set.
+
+The following example sets the `max-size` option for a service that uses the
+`json-file` logging driver, and keeps its existing `max-file` option:
+
+```console
+$ docker service create --log-driver=json-file --log-opt max-file=5 --name myservice nginx:alpine
+
+$ docker service update --log-opt max-size=10m myservice
+
+$ docker service inspect --format='{{json .Spec.TaskTemplate.LogDriver}}' myservice
+{"Name":"json-file","Options":{"max-file":"5","max-size":"10m"}}
+```
+
+If you set `--log-driver`, all existing options are replaced with the options
+that you set. To remove an option, set the current logging driver again
+together with the options that you want to keep:
+
+```console
+$ docker service update --log-driver=json-file --log-opt max-size=10m myservice
+
+$ docker service inspect --format='{{json .Spec.TaskTemplate.LogDriver}}' myservice
+{"Name":"json-file","Options":{"max-size":"10m"}}
+```
+
### Roll back to the previous version of a service (--rollback)
Use the `--rollback` option to roll back to the previous version of the service.