Repository: lima-vm/lima
Pull request #436 (2021-11-26 06:45:42)

Implement support for global default.yaml and override.yaml configs
Stored inside the `_config` directory, they provide defaults for all instances stored under the same `LIMA_HOME`. Settings from `override.yaml` take precedence over the instance settings themselves:

```console
$ cat ~/.lima/_config/override.yaml
mounts:
- location: "~"
  writable: true
- location: /Volumes/ThunderBlade
  writable: true

networks:
- lima: shared
```

This make the home mount writable, adds a second mount point (because I have symlinks that point to an SSD array), and adds a vmnet IP address to *every* instance I start, without me having to modify the `examples/*.yaml` file when I create the instance.

The settings could have been in `_config/default.yaml` too, except making `~` writable wouldn't work because we make it explicitly readonly in our sample configs.

This mechanism also allows users of Rancher Desktop or Colima to modify the config of the embedded instance without worrying about getting their customizations wiped during app updates, or instance restarts.

Still needs polishing and more testing (and updating the docs), but otherwise fully functional, so please give some feedback!

AkihiroSuda: > ~/.lima/_config/override.yaml

Maybe we want to support multiple override yamls?
 `~/.lima/_config/override/{00-foo.yaml, 01-bar.yaml, ...}`

jandubois: > Maybe we want to support multiple override yamls?

Can you explain the use case?

AkihiroSuda: > > Maybe we want to support multiple override yamls?
> 
> Can you explain the use case?

I was thinking that splitting the override yaml into multiple files may increase readability of the yamls, but we can revisit that later.

AkihiroSuda: Looks good after adding docs and tests.
Especially we should clarify how the default yaml and the override yaml are applied to `[]string` fields such as `.mounts` and `.containerd.archives`.
jandubois: > I was thinking that splitting the override yaml into multiple files may increase readability of the yamls, but we can revisit that later.

I think the overrides or defaults will typically be very small, so splitting them into multiple files doesn't make that much sense.

The one exception would be provisioning and probe scripts. But I think a better solution for those would be to allow them to be filename references and not require them to be inlined into the YAML file. That would allow you to use a proper "shell" mode for editing them, and also enables running `shellcheck` against them etc.

So yeah, we can revisit later, but I don't expect that there will be a need for this.

However, what we will need eventually is a per-instance override that will be stored inside the instance directory, and can be used to store modified setting specified e.g. via `limactl start alpine --set cpus=2`. But this can wait until we actually have a way to modify settings during restarts.
jandubois: @AkihiroSuda PTAL, I think it is basically done now
jandubois: Sorry, pushed one more update to allow "merging" of networks using `Interface` as the shared key. That will allow the `override.yaml` to change the network type of a named interface e.g. form "shared" to "bridged".

The docs at the bottom of `default.yaml` feel rather verbose, but not sure how to tighten them up.

Patch:
diff --git a/go.mod b/go.mod
index 480b9305..410cecdf 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/diskfs/go-diskfs v1.2.0
 	github.com/docker/go-units v0.4.0
 	github.com/elastic/go-libaudit/v2 v2.2.0
+	github.com/google/go-cmp v0.5.5
 	github.com/gorilla/mux v1.8.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/lima-vm/sshocker v0.2.2
@@ -24,6 +25,7 @@ require (
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.2.1
+	github.com/xorcare/pointer v1.1.0
 	github.com/yalue/native_endian v1.0.2
 	golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7
 	gopkg.in/yaml.v2 v2.4.0
@@ -36,7 +38,6 @@ require (
 	github.com/fatih/color v1.10.0 // indirect
 	github.com/fsnotify/fsnotify v1.4.9 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/google/go-cmp v0.5.5 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@@ -53,6 +54,7 @@ require (
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
 	golang.org/x/text v0.3.6 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
 	google.golang.org/grpc v1.39.0-dev.0.20210518002758-2713b77e8526 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
diff --git a/go.sum b/go.sum
index ba5fbcc3..bf24752b 100644
--- a/go.sum
+++ b/go.sum
@@ -816,6 +816,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
 github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xorcare/pointer v1.1.0 h1:sFwXOhRF8QZ0tyVZrtxWGIoVZNEmRzBCaFWdONPQIUM=
+github.com/xorcare/pointer v1.1.0/go.mod h1:6KLhkOh6YbuvZkT4YbxIbR/wzLBjyMxOiNzZhJTor2Y=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yalue/native_endian v1.0.2 h1:e4SxBbaCoOOO4E3axd7FSriUhzc1bIzqZGG5jl6Evbg=
 github.com/yalue/native_endian v1.0.2/go.mod h1:cr+I2WnCwDkkPV0DvgBpGQkJV12CDWR5bAoMtT+56iE=
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index 83303216..b6336d20 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -173,7 +173,7 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort
 		}
 	}
 
-	if guestAgentBinary, err := GuestAgentBinary(y.Arch); err != nil {
+	if guestAgentBinary, err := GuestAgentBinary(*y.Arch); err != nil {
 		return err
 	} else {
 		defer guestAgentBinary.Close()
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 572da999..c0706b1e 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -156,10 +156,10 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt
 }
 
 func determineSSHLocalPort(y *limayaml.LimaYAML, instName string) (int, error) {
-	if y.SSH.LocalPort > 0 {
-		return y.SSH.LocalPort, nil
+	if *y.SSH.LocalPort > 0 {
+		return *y.SSH.LocalPort, nil
 	}
-	if y.SSH.LocalPort < 0 {
+	if *y.SSH.LocalPort < 0 {
 		return 0, fmt.Errorf("invalid ssh local port %d", y.SSH.LocalPort)
 	}
 	switch instName {
diff --git a/pkg/limayaml/default.yaml b/pkg/limayaml/default.yaml
index bbf142b8..a2d3b2b7 100644
--- a/pkg/limayaml/default.yaml
+++ b/pkg/limayaml/default.yaml
@@ -229,6 +229,46 @@ useHostResolver: true
 # - 1.1.1.1
 # - 1.0.0.1
 
+# ===================================================================== #
+# GLOBAL DEFAULTS AND OVERRIDES
+# ===================================================================== #
+
+# The builtin defaults can be changed globally by creating a $LIMA_HOME/_config/default.yaml
+# file. It will be used by ALL instances under the same $LIMA_HOME, and it
+# will be applied on each `limactl start`, so can affect instance restarts.
+
+# A similar mechanism is $LIMA_HOME/_config/override.yaml, which will take
+# precedence even over the settings in an instances lima.yaml file.
+# It too applies to ALL instances under the same $LIMA_HOME, and is applied
+# on each restart. It can be used to globally override settings, e.g. make
+# the mount of the home directory writable.
+
+# On each instance start the config settings are determined: If a value is
+# not set in `lima.yaml`, then the `default.yaml` is used. If that file
+# doesn't exist, or the value is not defined in the file, then the buildin
+# default is used. If `override.yaml` exists and defines the value, then
+# it overrides whatever has been choosen so far.
+
+# For slices (e.g. `mounts`, `provision`) and maps (`env`) the entries are
+# combined instead of replacing each other. Slices are produced from override
+# settings, followed by lima.yaml, followed by defaults.yaml (but NOT from
+# builtin defaults). Maps are produced starting with defaults.yaml values,
+# overwriting with lima.yaml ones, overwriting with override.yaml.
+
+# Exceptions:
+# - `dns` will use the list from the highest priority file; they are not
+#   combined. If override.yaml defines a list of `dns` entries, then the
+#   settings in default.yaml and lima.yaml are ignored.
+#
+# - `mounts` will update the `writable` setting when 2 entries have the
+#   same `location` value. For this reason they are processed in the opposite
+#   order: starting with default, followed by lima, and then override.
+#
+# -`networks` will replace lower priority entries with the same `interface`
+#   name with higher priority definitions. This does not apply if the
+#  `interface` field is empty. `networks` are therefore also processed
+#  in lowest to highest priority order.
+
 # ===================================================================== #
 # END OF TEMPLATE
 # ===================================================================== #
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 2dacf2af..3569275d 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -17,6 +17,7 @@ import (
 	"github.com/lima-vm/lima/pkg/store/dirnames"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
+	"github.com/xorcare/pointer"
 )
 
 func defaultContainerdArchives() []File {
@@ -51,54 +52,154 @@ func MACAddress(uniqueID string) string {
 	return hw.String()
 }
 
-func FillDefault(y *LimaYAML, filePath string) {
-	y.Arch = resolveArch(y.Arch)
+// FillDefault updates undefined fields in y with defaults from d (or built-in default), and overwrites with values from o.
+// Both d and o may be empty.
+//
+// Maps (`Env`) are being merged: first populated from d, overwritten by y, and again overwritten by o.
+// Slices (e.g. `Mounts`, `Provision`) are appended, starting with o, followed by y, and finally d. This
+// makes sure o takes priority over y over d, in cases it matters (e.g. `PortForwards`, where the first
+// matching rule terminates the search).
+//
+// Exceptions:
+// - Mounts are appended in d, y, o order, but "merged" when the Location matches a previous entry;
+//   the highest priority Writable setting wins.
+// - DNS are picked from the highest priority where DNS is not empty.
+func FillDefault(y, d, o *LimaYAML, filePath string) {
+	if y.Arch == nil {
+		y.Arch = d.Arch
+	}
+	if o.Arch != nil {
+		y.Arch = o.Arch
+	}
+	y.Arch = pointer.String(resolveArch(y.Arch))
+
+	y.Images = append(append(o.Images, y.Images...), d.Images...)
 	for i := range y.Images {
 		img := &y.Images[i]
 		if img.Arch == "" {
-			img.Arch = y.Arch
+			img.Arch = *y.Arch
 		}
 	}
-	if y.CPUs == 0 {
-		y.CPUs = 4
+
+	if y.CPUs == nil {
+		y.CPUs = d.CPUs
+	}
+	if o.CPUs != nil {
+		y.CPUs = o.CPUs
+	}
+	if y.CPUs == nil || *y.CPUs == 0 {
+		y.CPUs = pointer.Int(4)
+	}
+
+	if y.Memory == nil {
+		y.Memory = d.Memory
+	}
+	if o.Memory != nil {
+		y.Memory = o.Memory
+	}
+	if y.Memory == nil || *y.Memory == "" {
+		y.Memory = pointer.String("4GiB")
+	}
+
+	if y.Disk == nil {
+		y.Disk = d.Disk
+	}
+	if o.Disk != nil {
+		y.Disk = o.Disk
+	}
+	if y.Disk == nil || *y.Disk == "" {
+		y.Disk = pointer.String("100GiB")
+	}
+
+	if y.Video.Display == nil {
+		y.Video.Display = d.Video.Display
+	}
+	if o.Video.Display != nil {
+		y.Video.Display = o.Video.Display
 	}
-	if y.Memory == "" {
-		y.Memory = "4GiB"
+	if y.Video.Display == nil || *y.Video.Display == "" {
+		y.Video.Display = pointer.String("none")
 	}
-	if y.Disk == "" {
-		y.Disk = "100GiB"
+
+	if y.Firmware.LegacyBIOS == nil {
+		y.Firmware.LegacyBIOS = d.Firmware.LegacyBIOS
+	}
+	if o.Firmware.LegacyBIOS != nil {
+		y.Firmware.LegacyBIOS = o.Firmware.LegacyBIOS
+	}
+	if y.Firmware.LegacyBIOS == nil {
+		y.Firmware.LegacyBIOS = pointer.Bool(false)
+	}
+
+	if y.SSH.LocalPort == nil {
+		y.SSH.LocalPort = d.SSH.LocalPort
+	}
+	if o.SSH.LocalPort != nil {
+		y.SSH.LocalPort = o.SSH.LocalPort
+	}
+	if y.SSH.LocalPort == nil {
+		// y.SSH.LocalPort value is not filled here (filled by the hostagent)
+		y.SSH.LocalPort = pointer.Int(0)
+	}
+	if y.SSH.LoadDotSSHPubKeys == nil {
+		y.SSH.LoadDotSSHPubKeys = d.SSH.LoadDotSSHPubKeys
 	}
-	if y.Video.Display == "" {
-		y.Video.Display = "none"
+	if o.SSH.LoadDotSSHPubKeys != nil {
+		y.SSH.LoadDotSSHPubKeys = o.SSH.LoadDotSSHPubKeys
 	}
-	// y.SSH.LocalPort is not filled here (filled by the hostagent)
 	if y.SSH.LoadDotSSHPubKeys == nil {
-		y.SSH.LoadDotSSHPubKeys = &[]bool{true}[0]
+		y.SSH.LoadDotSSHPubKeys = pointer.Bool(true)
+	}
+
+	if y.SSH.ForwardAgent == nil {
+		y.SSH.ForwardAgent = d.SSH.ForwardAgent
+	}
+	if o.SSH.ForwardAgent != nil {
+		y.SSH.ForwardAgent = o.SSH.ForwardAgent
 	}
 	if y.SSH.ForwardAgent == nil {
-		y.SSH.ForwardAgent = &[]bool{false}[0]
+		y.SSH.ForwardAgent = pointer.Bool(false)
 	}
+
+	y.Provision = append(append(o.Provision, y.Provision...), d.Provision...)
 	for i := range y.Provision {
 		provision := &y.Provision[i]
 		if provision.Mode == "" {
 			provision.Mode = ProvisionModeSystem
 		}
 	}
+
+	if y.Containerd.System == nil {
+		y.Containerd.System = d.Containerd.System
+	}
+	if o.Containerd.System != nil {
+		y.Containerd.System = o.Containerd.System
+	}
 	if y.Containerd.System == nil {
-		y.Containerd.System = &[]bool{false}[0]
+		y.Containerd.System = pointer.Bool(false)
 	}
 	if y.Containerd.User == nil {
-		y.Containerd.User = &[]bool{true}[0]
+		y.Containerd.User = d.Containerd.User
+	}
+	if o.Containerd.User != nil {
+		y.Containerd.User = o.Containerd.User
 	}
+	if y.Containerd.User == nil {
+		y.Containerd.User = pointer.Bool(true)
+	}
+
+	y.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...)
 	if len(y.Containerd.Archives) == 0 {
 		y.Containerd.Archives = defaultContainerdArchives()
 	}
 	for i := range y.Containerd.Archives {
 		f := &y.Containerd.Archives[i]
 		if f.Arch == "" {
-			f.Arch = y.Arch
+			f.Arch = *y.Arch
 		}
 	}
+
+	y.Probes = append(append(o.Probes, y.Probes...), d.Probes...)
 	for i := range y.Probes {
 		probe := &y.Probes[i]
 		if probe.Mode == "" {
@@ -108,16 +209,32 @@ func FillDefault(y *LimaYAML, filePath string) {
 			probe.Description = fmt.Sprintf("user probe %d/%d", i+1, len(y.Probes))
 		}
 	}
+
+	y.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...)
 	instDir := filepath.Dir(filePath)
 	for i := range y.PortForwards {
 		FillPortForwardDefaults(&y.PortForwards[i], instDir)
 		// After defaults processing the singular HostPort and GuestPort values should not be used again.
 	}
+
 	if y.UseHostResolver == nil {
-		y.UseHostResolver = &[]bool{true}[0]
+		y.UseHostResolver = d.UseHostResolver
+	}
+	if o.UseHostResolver != nil {
+		y.UseHostResolver = o.UseHostResolver
+	}
+	if y.UseHostResolver == nil {
+		y.UseHostResolver = pointer.Bool(true)
+	}
+
+	if y.PropagateProxyEnv == nil {
+		y.PropagateProxyEnv = d.PropagateProxyEnv
+	}
+	if o.PropagateProxyEnv != nil {
+		y.PropagateProxyEnv = o.PropagateProxyEnv
 	}
 	if y.PropagateProxyEnv == nil {
-		y.PropagateProxyEnv = &[]bool{true}[0]
+		y.PropagateProxyEnv = pointer.Bool(true)
 	}
 
 	if len(y.Network.VDEDeprecated) > 0 && len(y.Networks) == 0 {
@@ -132,6 +249,38 @@ func FillDefault(y *LimaYAML, filePath string) {
 		}
 		y.Network.migrated = true
 	}
+
+	networks := make([]Network, 0, len(d.Networks)+len(y.Networks)+len(o.Networks))
+	iface := make(map[string]int)
+	for _, nw := range append(append(d.Networks, y.Networks...), o.Networks...) {
+		if i, ok := iface[nw.Interface]; ok {
+			if nw.VNL != "" {
+				networks[i].VNL = nw.VNL
+				networks[i].SwitchPort = nw.SwitchPort
+				networks[i].Lima = ""
+			}
+			if nw.Lima != "" {
+				if nw.VNL != "" {
+					// We can't return an error, so just log it, and prefer `lima` over `vnl`
+					logrus.Errorf("Network %q has both vnl=%q and lima=%q fields; ignoring vnl",
+						nw.Interface, nw.VNL, nw.Lima)
+				}
+				networks[i].Lima = nw.Lima
+				networks[i].VNL = ""
+				networks[i].SwitchPort = 0
+			}
+			if nw.MACAddress != "" {
+				networks[i].MACAddress = nw.MACAddress
+			}
+		} else {
+			// unnamed network definitions are not combined/overwritten
+			if nw.Interface != "" {
+				iface[nw.Interface] = len(networks)
+			}
+			networks = append(networks, nw)
+		}
+	}
+	y.Networks = networks
 	for i := range y.Networks {
 		nw := &y.Networks[i]
 		if nw.MACAddress == "" {
@@ -142,6 +291,40 @@ func FillDefault(y *LimaYAML, filePath string) {
 			nw.Interface = "lima" + strconv.Itoa(i)
 		}
 	}
+
+	// Combine all mounts; highest priority entry determines writable status.
+	// Only works for exact matches; does not normalize case or resolve symlinks.
+	mounts := make([]Mount, 0, len(d.Mounts)+len(y.Mounts)+len(o.Mounts))
+	location := make(map[string]int)
+	for _, mount := range append(append(d.Mounts, y.Mounts...), o.Mounts...) {
+		if i, ok := location[mount.Location]; ok {
+			mounts[i].Writable = mount.Writable
+		} else {
+			location[mount.Location] = len(mounts)
+			mounts = append(mounts, mount)
+		}
+	}
+	y.Mounts = mounts
+
+	// Note: DNS lists are not combined; highest priority setting is picked
+	if len(y.DNS) == 0 {
+		y.DNS = d.DNS
+	}
+	if len(o.DNS) > 0 {
+		y.DNS = o.DNS
+	}
+
+	env := make(map[string]string)
+	for k, v := range d.Env {
+		env[k] = v
+	}
+	for k, v := range y.Env {
+		env[k] = v
+	}
+	for k, v := range o.Env {
+		env[k] = v
+	}
+	y.Env = env
 }
 
 func FillPortForwardDefaults(rule *PortForward, instDir string) {
@@ -217,13 +400,13 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
 	}
 }
 
-func resolveArch(s string) Arch {
-	if s == "" || s == "default" {
+func resolveArch(s *string) Arch {
+	if s == nil || *s == "" || *s == "default" {
 		if runtime.GOARCH == "amd64" {
 			return X8664
 		} else {
 			return AARCH64
 		}
 	}
-	return s
+	return *s
 }
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index d68bde23..1793ef11 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -7,11 +7,11 @@ import (
 )
 
 type LimaYAML struct {
-	Arch              Arch              `yaml:"arch,omitempty" json:"arch,omitempty"`
+	Arch              *Arch             `yaml:"arch,omitempty" json:"arch,omitempty"`
 	Images            []File            `yaml:"images" json:"images"` // REQUIRED
-	CPUs              int               `yaml:"cpus,omitempty" json:"cpus,omitempty"`
-	Memory            string            `yaml:"memory,omitempty" json:"memory,omitempty"` // go-units.RAMInBytes
-	Disk              string            `yaml:"disk,omitempty" json:"disk,omitempty"`     // go-units.RAMInBytes
+	CPUs              *int              `yaml:"cpus,omitempty" json:"cpus,omitempty"`
+	Memory            *string           `yaml:"memory,omitempty" json:"memory,omitempty"` // go-units.RAMInBytes
+	Disk              *string           `yaml:"disk,omitempty" json:"disk,omitempty"`     // go-units.RAMInBytes
 	Mounts            []Mount           `yaml:"mounts,omitempty" json:"mounts,omitempty"`
 	SSH               SSH               `yaml:"ssh,omitempty" json:"ssh,omitempty"` // REQUIRED (FIXME)
 	Firmware          Firmware          `yaml:"firmware,omitempty" json:"firmware,omitempty"`
@@ -47,7 +47,7 @@ type Mount struct {
 }
 
 type SSH struct {
-	LocalPort int `yaml:"localPort,omitempty" json:"localPort,omitempty"`
+	LocalPort *int `yaml:"localPort,omitempty" json:"localPort,omitempty"`
 
 	// LoadDotSSHPubKeys loads ~/.ssh/*.pub in addition to $LIMA_HOME/_config/user.pub .
 	LoadDotSSHPubKeys *bool `yaml:"loadDotSSHPubKeys,omitempty" json:"loadDotSSHPubKeys,omitempty"` // default: true
@@ -57,12 +57,12 @@ type SSH struct {
 type Firmware struct {
 	// LegacyBIOS disables UEFI if set.
 	// LegacyBIOS is ignored for aarch64.
-	LegacyBIOS bool `yaml:"legacyBIOS,omitempty" json:"legacyBIOS,omitempty"`
+	LegacyBIOS *bool `yaml:"legacyBIOS,omitempty" json:"legacyBIOS,omitempty"`
 }
 
 type Video struct {
 	// Display is a QEMU display string
-	Display string `yaml:"display,omitempty" json:"display,omitempty"`
+	Display *string `yaml:"display,omitempty" json:"display,omitempty"`
 }
 
 type ProvisionMode = string
diff --git a/pkg/limayaml/load.go b/pkg/limayaml/load.go
index 1b0df078..f34985df 100644
--- a/pkg/limayaml/load.go
+++ b/pkg/limayaml/load.go
@@ -1,6 +1,13 @@
 package limayaml
 
 import (
+	"errors"
+	"os"
+	"path/filepath"
+
+	"github.com/lima-vm/lima/pkg/store/dirnames"
+	"github.com/lima-vm/lima/pkg/store/filenames"
+	"github.com/sirupsen/logrus"
 	"gopkg.in/yaml.v2"
 )
 
@@ -8,10 +15,38 @@ import (
 //
 // Load does not validate. Use Validate for validation.
 func Load(b []byte, filePath string) (*LimaYAML, error) {
-	var y LimaYAML
+	var y, d, o LimaYAML
+
 	if err := yaml.Unmarshal(b, &y); err != nil {
 		return nil, err
 	}
-	FillDefault(&y, filePath)
+	configDir, err := dirnames.LimaConfigDir()
+	if err != nil {
+		return nil, err
+	}
+
+	defaultPath := filepath.Join(configDir, filenames.Default)
+	bytes, err := os.ReadFile(defaultPath)
+	if err == nil {
+		logrus.Debugf("Mixing %q into %q", defaultPath, filePath)
+		if err := yaml.Unmarshal(bytes, &d); err != nil {
+			return nil, err
+		}
+	} else if !errors.Is(err, os.ErrNotExist) {
+		return nil, err
+	}
+
+	overridePath := filepath.Join(configDir, filenames.Override)
+	bytes, err = os.ReadFile(overridePath)
+	if err == nil {
+		logrus.Debugf("Mixing %q into %q", overridePath, filePath)
+		if err := yaml.Unmarshal(bytes, &o); err != nil {
+			return nil, err
+		}
+	} else if !errors.Is(err, os.ErrNotExist) {
+		return nil, err
+	}
+
+	FillDefault(&y, &d, &o, filePath)
 	return &y, nil
 }
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index e710950c..14b7f0ad 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -19,10 +19,10 @@ import (
 )
 
 func Validate(y LimaYAML, warn bool) error {
-	switch y.Arch {
+	switch *y.Arch {
 	case X8664, AARCH64:
 	default:
-		return fmt.Errorf("field `arch` must be %q or %q , got %q", X8664, AARCH64, y.Arch)
+		return fmt.Errorf("field `arch` must be %q or %q , got %q", X8664, AARCH64, *y.Arch)
 	}
 
 	if len(y.Images) == 0 {
@@ -50,15 +50,15 @@ func Validate(y LimaYAML, warn bool) error {
 		}
 	}
 
-	if y.CPUs == 0 {
+	if *y.CPUs == 0 {
 		return errors.New("field `cpus` must be set")
 	}
 
-	if _, err := units.RAMInBytes(y.Memory); err != nil {
+	if _, err := units.RAMInBytes(*y.Memory); err != nil {
 		return fmt.Errorf("field `memory` has an invalid value: %w", err)
 	}
 
-	if _, err := units.RAMInBytes(y.Disk); err != nil {
+	if _, err := units.RAMInBytes(*y.Disk); err != nil {
 		return fmt.Errorf("field `memory` has an invalid value: %w", err)
 	}
 
@@ -95,8 +95,8 @@ func Validate(y LimaYAML, warn bool) error {
 		}
 	}
 
-	if y.SSH.LocalPort != 0 {
-		if err := validatePort("ssh.localPort", y.SSH.LocalPort); err != nil {
+	if *y.SSH.LocalPort != 0 {
+		if err := validatePort("ssh.localPort", *y.SSH.LocalPort); err != nil {
 			return err
 		}
 	}
@@ -297,7 +297,7 @@ func validateNetwork(y LimaYAML, warn bool) error {
 			return fmt.Errorf("field `%s.interface` must not be set to %q because it is reserved for slirp", field, qemu.SlirpNICName)
 		}
 		if prev, ok := interfaceName[nw.Interface]; ok {
-			return fmt.Errorf("field `%s.interface` value %q has already been used by field `network.vde[%d].name`", field, nw.Interface, prev)
+			return fmt.Errorf("field `%s.interface` value %q has already been used by field `networks[%d].interface`", field, nw.Interface, prev)
 		}
 		interfaceName[nw.Interface] = i
 	}
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 71d633b6..5181de2b 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -43,7 +43,7 @@ func EnsureDisk(cfg Config) error {
 		var ensuredBaseDisk bool
 		errs := make([]error, len(cfg.LimaYAML.Images))
 		for i, f := range cfg.LimaYAML.Images {
-			if f.Arch != cfg.LimaYAML.Arch {
+			if f.Arch != *cfg.LimaYAML.Arch {
 				errs[i] = fmt.Errorf("unsupported arch: %q", f.Arch)
 				continue
 			}
@@ -73,7 +73,7 @@ func EnsureDisk(cfg Config) error {
 				len(cfg.LimaYAML.Images), errs)
 		}
 	}
-	diskSize, _ := units.RAMInBytes(cfg.LimaYAML.Disk)
+	diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk)
 	if diskSize == 0 {
 		return nil
 	}
@@ -186,7 +186,7 @@ func inspectFeatures(exe string) (*features, error) {
 
 func Cmdline(cfg Config) (string, []string, error) {
 	y := cfg.LimaYAML
-	exe, args, err := getExe(y.Arch)
+	exe, args, err := getExe(*y.Arch)
 	if err != nil {
 		return "", nil, err
 	}
@@ -197,26 +197,26 @@ func Cmdline(cfg Config) (string, []string, error) {
 	}
 
 	// Architecture
-	accel := getAccel(y.Arch)
+	accel := getAccel(*y.Arch)
 	if !strings.Contains(string(features.AccelHelp), accel) {
 		errStr := fmt.Sprintf("accelerator %q is not supported by %s", accel, exe)
-		if accel == "hvf" && y.Arch == limayaml.AARCH64 {
+		if accel == "hvf" && *y.Arch == limayaml.AARCH64 {
 			errStr += " ( Hint: as of August 2021, qemu-system-aarch64 on ARM Mac needs to be patched for enabling hvf accelerator,"
 			errStr += " see https://gist.github.com/nrjdalal/e70249bb5d2e9d844cc203fd11f74c55 )"
 		}
 		return "", nil, errors.New(errStr)
 	}
-	switch y.Arch {
+	switch *y.Arch {
 	case limayaml.X8664:
 		cpu := "Haswell-v4"
-		if isNativeArch(y.Arch) {
+		if isNativeArch(*y.Arch) {
 			cpu = "host"
 		}
 		args = appendArgsIfNoConflict(args, "-cpu", cpu)
 		args = appendArgsIfNoConflict(args, "-machine", "q35,accel="+accel)
 	case limayaml.AARCH64:
 		cpu := "cortex-a72"
-		if isNativeArch(y.Arch) {
+		if isNativeArch(*y.Arch) {
 			cpu = "host"
 		}
 		args = appendArgsIfNoConflict(args, "-cpu", cpu)
@@ -225,23 +225,23 @@ func Cmdline(cfg Config) (string, []string, error) {
 
 	// SMP
 	args = appendArgsIfNoConflict(args, "-smp",
-		fmt.Sprintf("%d,sockets=1,cores=%d,threads=1", y.CPUs, y.CPUs))
+		fmt.Sprintf("%d,sockets=1,cores=%d,threads=1", *y.CPUs, *y.CPUs))
 
 	// Memory
-	memBytes, err := units.RAMInBytes(y.Memory)
+	memBytes, err := units.RAMInBytes(*y.Memory)
 	if err != nil {
 		return "", nil, err
 	}
 	args = appendArgsIfNoConflict(args, "-m", strconv.Itoa(int(memBytes>>20)))
 
 	// Firmware
-	legacyBIOS := y.Firmware.LegacyBIOS
-	if legacyBIOS && y.Arch != limayaml.X8664 {
-		logrus.Warnf("field `firmware.legacyBIOS` is not supported for architecture %q, ignoring", y.Arch)
+	legacyBIOS := *y.Firmware.LegacyBIOS
+	if legacyBIOS && *y.Arch != limayaml.X8664 {
+		logrus.Warnf("field `firmware.legacyBIOS` is not supported for architecture %q, ignoring", *y.Arch)
 		legacyBIOS = false
 	}
 	if !legacyBIOS {
-		firmware, err := getFirmware(exe, y.Arch)
+		firmware, err := getFirmware(exe, *y.Arch)
 		if err != nil {
 			return "", nil, err
 		}
@@ -260,7 +260,7 @@ func Cmdline(cfg Config) (string, []string, error) {
 	} else {
 		args = appendArgsIfNoConflict(args, "-boot", "order=c,splash-time=0,menu=on")
 	}
-	if diskSize, _ := units.RAMInBytes(cfg.LimaYAML.Disk); diskSize > 0 {
+	if diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk); diskSize > 0 {
 		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio", diffDisk))
 	} else if !isBaseDiskCDROM {
 		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio", baseDisk))
@@ -313,10 +313,10 @@ func Cmdline(cfg Config) (string, []string, error) {
 	args = append(args, "-device", "virtio-rng-pci")
 
 	// Graphics
-	if y.Video.Display != "" {
-		args = appendArgsIfNoConflict(args, "-display", y.Video.Display)
+	if *y.Video.Display != "" {
+		args = appendArgsIfNoConflict(args, "-display", *y.Video.Display)
 	}
-	switch y.Arch {
+	switch *y.Arch {
 	case limayaml.X8664:
 		args = append(args, "-device", "virtio-vga")
 		args = append(args, "-device", "virtio-keyboard-pci")
diff --git a/pkg/start/start.go b/pkg/start/start.go
index f60d53ce..9c9d05cc 100644
--- a/pkg/start/start.go
+++ b/pkg/start/start.go
@@ -43,7 +43,7 @@ func ensureNerdctlArchiveCache(y *limayaml.LimaYAML) (string, error) {
 	errs := make([]error, len(y.Containerd.Archives))
 	for i := range y.Containerd.Archives {
 		f := &y.Containerd.Archives[i]
-		if f.Arch != y.Arch {
+		if f.Arch != *y.Arch {
 			errs[i] = fmt.Errorf("unsupported arch: %q", f.Arch)
 			continue
 		}
diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go
index 358b6acb..68e7f99e 100644
--- a/pkg/store/filenames/filenames.go
+++ b/pkg/store/filenames/filenames.go
@@ -18,6 +18,8 @@ const (
 	UserPrivateKey = "user"
 	UserPublicKey  = UserPrivateKey + ".pub"
 	NetworksConfig = "networks.yaml"
+	Default        = "default.yaml"
+	Override       = "override.yaml"
 )
 
 // Filenames that may appear under an instance directory
diff --git a/pkg/store/instance.go b/pkg/store/instance.go
index f305d505..f09ce7f6 100644
--- a/pkg/store/instance.go
+++ b/pkg/store/instance.go
@@ -71,18 +71,18 @@ func Inspect(instName string) (*Instance, error) {
 		return inst, nil
 	}
 	inst.Dir = instDir
-	inst.Arch = y.Arch
-	inst.CPUs = y.CPUs
-	memory, err := units.RAMInBytes(y.Memory)
+	inst.Arch = *y.Arch
+	inst.CPUs = *y.CPUs
+	memory, err := units.RAMInBytes(*y.Memory)
 	if err == nil {
 		inst.Memory = memory
 	}
-	disk, err := units.RAMInBytes(y.Disk)
+	disk, err := units.RAMInBytes(*y.Disk)
 	if err == nil {
 		inst.Disk = disk
 	}
 	inst.Networks = y.Networks
-	inst.SSHLocalPort = y.SSH.LocalPort // maybe 0
+	inst.SSHLocalPort = *y.SSH.LocalPort // maybe 0
 
 	inst.HostAgentPID, err = ReadPIDFile(filepath.Join(instDir, filenames.HostAgentPID))
 	if err != nil {
@@ -153,7 +153,7 @@ func ReadPIDFile(path string) (int, error) {
 	err = proc.Signal(syscall.Signal(0))
 	if err != nil {
 		if errors.Is(err, os.ErrProcessDone) {
-			os.Remove(path)
+			_ = os.Remove(path)
 			return 0, nil
 		}
 		// We may not have permission to send the signal (e.g. to network daemon running as root).


Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
new file mode 100644
index 00000000..7c5cc10a
--- /dev/null
+++ b/pkg/limayaml/defaults_test.go
@@ -0,0 +1,376 @@
+package limayaml
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/lima-vm/lima/pkg/guestagent/api"
+	"github.com/lima-vm/lima/pkg/osutil"
+	"github.com/lima-vm/lima/pkg/store/dirnames"
+	"github.com/lima-vm/lima/pkg/store/filenames"
+	"github.com/xorcare/pointer"
+	"gotest.tools/v3/assert"
+)
+
+func TestFillDefault(t *testing.T) {
+	var d, y, o LimaYAML
+
+	opts := []cmp.Option{
+		// Ignore internal NetworkDeprecated.migrated field
+		cmpopts.IgnoreUnexported(NetworkDeprecated{}),
+		// Consider nil slices and empty slices to be identical
+		cmpopts.EquateEmpty(),
+	}
+
+	arch := AARCH64
+	if runtime.GOARCH == "amd64" {
+		arch = X8664
+	}
+
+	hostHome, err := os.UserHomeDir()
+	assert.NilError(t, err)
+	limaHome, err := dirnames.LimaDir()
+	assert.NilError(t, err)
+	user, err := osutil.LimaUser(false)
+	assert.NilError(t, err)
+
+	guestHome := fmt.Sprintf("/home/%s.linux", user.Username)
+	instName := "instance"
+	instDir := filepath.Join(limaHome, instName)
+	filePath := filepath.Join(instDir, filenames.LimaYAML)
+
+	// Builtin default values
+	builtin := LimaYAML{
+		Arch:   pointer.String(arch),
+		CPUs:   pointer.Int(4),
+		Memory: pointer.String("4GiB"),
+		Disk:   pointer.String("100GiB"),
+		Containerd: Containerd{
+			System:   pointer.Bool(false),
+			User:     pointer.Bool(true),
+			Archives: defaultContainerdArchives(),
+		},
+		SSH: SSH{
+			LocalPort:         pointer.Int(0),
+			LoadDotSSHPubKeys: pointer.Bool(true),
+			ForwardAgent:      pointer.Bool(false),
+		},
+		Firmware: Firmware{
+			LegacyBIOS: pointer.Bool(false),
+		},
+		Video: Video{
+			Display: pointer.String("none"),
+		},
+		UseHostResolver:   pointer.Bool(true),
+		PropagateProxyEnv: pointer.Bool(true),
+	}
+
+	defaultPortForward := PortForward{
+		GuestIP:        api.IPv4loopback1,
+		GuestPortRange: [2]int{1, 65535},
+		HostIP:         api.IPv4loopback1,
+		HostPortRange:  [2]int{1, 65535},
+		Proto:          TCP,
+	}
+
+	// ------------------------------------------------------------------------------------
+	// Builtin defaults are set when y is (mostly) empty
+
+	// All these slices and maps are empty in "builtin". Add minimal entries here to see that
+	// their values are retained and defaults for their fields are applied correctly.
+	y = LimaYAML{
+		Mounts: []Mount{
+			{Location: "/tmp"},
+		},
+		Provision: []Provision{
+			{Script: "#!/bin/true"},
+		},
+		Probes: []Probe{
+			{Script: "#!/bin/false"},
+		},
+		Networks: []Network{
+			{Lima: "shared"},
+		},
+		DNS: []net.IP{
+			net.ParseIP("1.0.1.0"),
+		},
+		PortForwards: []PortForward{
+			{},
+			{GuestPort: 80},
+			{GuestPort: 8080, HostPort: 8888},
+			{
+				GuestSocket: "{{.Home}} | {{.UID}} | {{.User}}",
+				HostSocket:  "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}}",
+			},
+		},
+		Env: map[string]string{
+			"ONE": "Eins",
+		},
+	}
+
+	expect := builtin
+	expect.Mounts = y.Mounts
+	// Only missing Mounts field is Writable, and the default value is also the null value: false
+
+	expect.Provision = y.Provision
+	expect.Provision[0].Mode = ProvisionModeSystem
+
+	expect.Probes = y.Probes
+	expect.Probes[0].Mode = ProbeModeReadiness
+	expect.Probes[0].Description = "user probe 1/1"
+
+	expect.Networks = y.Networks
+	expect.Networks[0].MACAddress = MACAddress(fmt.Sprintf("%s#%d", filePath, 0))
+	expect.Networks[0].Interface = "lima0"
+
+	expect.DNS = y.DNS
+	expect.PortForwards = []PortForward{
+		defaultPortForward,
+		defaultPortForward,
+		defaultPortForward,
+		defaultPortForward,
+	}
+	// Setting GuestPort and HostPort for DeepEqual(), but they are not supposed to be used
+	// after FillDefault() has been called and the ...PortRange fields have been set.
+	expect.PortForwards[1].GuestPort = 80
+	expect.PortForwards[1].GuestPortRange = [2]int{80, 80}
+	expect.PortForwards[1].HostPortRange = expect.PortForwards[1].GuestPortRange
+
+	expect.PortForwards[2].GuestPort = 8080
+	expect.PortForwards[2].GuestPortRange = [2]int{8080, 8080}
+	expect.PortForwards[2].HostPort = 8888
+	expect.PortForwards[2].HostPortRange = [2]int{8888, 8888}
+
+	expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s", guestHome, user.Uid, user.Username)
+	expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username)
+
+	expect.Env = y.Env
+
+	FillDefault(&y, &LimaYAML{}, &LimaYAML{}, filePath)
+	assert.DeepEqual(t, &y, &expect, opts...)
+
+	filledDefaults := y
+
+	// ------------------------------------------------------------------------------------
+	// User-provided defaults should override any builtin defaults
+
+	// Choose values that are different from the "builtin" defaults
+	d = LimaYAML{
+		Arch:   pointer.String("unknown"),
+		CPUs:   pointer.Int(7),
+		Memory: pointer.String("5GiB"),
+		Disk:   pointer.String("105GiB"),
+		Containerd: Containerd{
+			System: pointer.Bool(true),
+			User:   pointer.Bool(false),
+			Archives: []File{
+				{Location: "/tmp/nerdctl.tgz"},
+			},
+		},
+		SSH: SSH{
+			LocalPort:         pointer.Int(888),
+			LoadDotSSHPubKeys: pointer.Bool(false),
+			ForwardAgent:      pointer.Bool(true),
+		},
+		Firmware: Firmware{
+			LegacyBIOS: pointer.Bool(true),
+		},
+		Video: Video{
+			Display: pointer.String("cocoa"),
+		},
+		UseHostResolver:   pointer.Bool(false),
+		PropagateProxyEnv: pointer.Bool(false),
+
+		Mounts: []Mount{
+			{
+				Location: "/var/log",
+				Writable: false,
+			},
+		},
+		Provision: []Provision{
+			{
+				Script: "#!/bin/true",
+				Mode:   ProvisionModeUser,
+			},
+		},
+		Probes: []Probe{
+			{
+				Script:      "#!/bin/false",
+				Mode:        ProbeModeReadiness,
+				Description: "User Probe",
+			},
+		},
+		Networks: []Network{
+			{
+				VNL:        "/tmp/vde.ctl",
+				SwitchPort: 65535,
+				MACAddress: "11:22:33:44:55:66",
+				Interface:  "def0",
+			},
+		},
+		DNS: []net.IP{
+			net.ParseIP("1.1.1.1"),
+		},
+		PortForwards: []PortForward{{
+			GuestIP:        api.IPv4loopback1,
+			GuestPort:      80,
+			GuestPortRange: [2]int{80, 80},
+			HostIP:         api.IPv4loopback1,
+			HostPort:       80,
+			HostPortRange:  [2]int{80, 80},
+			Proto:          TCP,
+		}},
+		Env: map[string]string{
+			"ONE": "one",
+			"TWO": "two",
+		},
+	}
+
+	expect = d
+	// Also verify that archive arch is filled in
+	expect.Containerd.Archives[0].Arch = *d.Arch
+
+	y = LimaYAML{}
+	FillDefault(&y, &d, &LimaYAML{}, filePath)
+	assert.DeepEqual(t, &y, &expect, opts...)
+
+	// ------------------------------------------------------------------------------------
+	// User-provided defaults should not override user-provided config values
+
+	y = filledDefaults
+	y.DNS = []net.IP{net.ParseIP("8.8.8.8")}
+
+	expect = y
+
+	expect.Provision = append(y.Provision, d.Provision...)
+	expect.Probes = append(y.Probes, d.Probes...)
+	expect.PortForwards = append(y.PortForwards, d.PortForwards...)
+	expect.Containerd.Archives = append(y.Containerd.Archives, d.Containerd.Archives...)
+
+	// Mounts and Networks start with lowest priority first, so higher priority entries can overwrite
+	expect.Mounts = append(d.Mounts, y.Mounts...)
+	expect.Networks = append(d.Networks, y.Networks...)
+
+	// d.DNS will be ignored, and not appended to y.DNS
+
+	// "TWO" does not exist in filledDefaults.Env, so is set from d.Env
+	expect.Env["TWO"] = d.Env["TWO"]
+
+	FillDefault(&y, &d, &LimaYAML{}, filePath)
+	assert.DeepEqual(t, &y, &expect, opts...)
+
+	// ------------------------------------------------------------------------------------
+	// User-provided overrides should override user-provided config settings
+
+	o = LimaYAML{
+		Arch:   pointer.String(arch),
+		CPUs:   pointer.Int(12),
+		Memory: pointer.String("7GiB"),
+		Disk:   pointer.String("117GiB"),
+		Containerd: Containerd{
+			System: pointer.Bool(true),
+			User:   pointer.Bool(false),
+			Archives: []File{
+				{
+					Arch:     arch,
+					Location: "/tmp/nerdctl.tgz",
+					Digest:   "$DIGEST",
+				},
+			},
+		},
+		SSH: SSH{
+			LocalPort:         pointer.Int(4433),
+			LoadDotSSHPubKeys: pointer.Bool(true),
+			ForwardAgent:      pointer.Bool(true),
+		},
+		Firmware: Firmware{
+			LegacyBIOS: pointer.Bool(true),
+		},
+		Video: Video{
+			Display: pointer.String("cocoa"),
+		},
+		UseHostResolver:   pointer.Bool(false),
+		PropagateProxyEnv: pointer.Bool(false),
+
+		Mounts: []Mount{
+			{
+				Location: "/var/log",
+				Writable: true,
+			},
+		},
+		Provision: []Provision{
+			{
+				Script: "#!/bin/true",
+				Mode:   ProvisionModeSystem,
+			},
+		},
+		Probes: []Probe{
+			{
+				Script:      "#!/bin/false",
+				Mode:        ProbeModeReadiness,
+				Description: "Another Probe",
+			},
+		},
+		Networks: []Network{
+			{
+				Lima:       "shared",
+				MACAddress: "10:20:30:40:50:60",
+				Interface:  "def1",
+			},
+			{
+				Lima:      "bridged",
+				Interface: "def0",
+			},
+		},
+		DNS: []net.IP{
+			net.ParseIP("2.2.2.2"),
+		},
+		PortForwards: []PortForward{{
+			GuestIP:        api.IPv4loopback1,
+			GuestPort:      88,
+			GuestPortRange: [2]int{88, 88},
+			HostIP:         api.IPv4loopback1,
+			HostPort:       8080,
+			HostPortRange:  [2]int{8080, 8080},
+			Proto:          TCP,
+		}},
+		Env: map[string]string{
+			"TWO":   "deux",
+			"THREE": "trois",
+		},
+	}
+
+	y = filledDefaults
+
+	expect = o
+
+	expect.Provision = append(append(o.Provision, y.Provision...), d.Provision...)
+	expect.Probes = append(append(o.Probes, y.Probes...), d.Probes...)
+	expect.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...)
+	expect.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...)
+
+	// o.Mounts just makes d.Mounts[0] writable because the Location matches
+	expect.Mounts = append(d.Mounts, y.Mounts...)
+	expect.Mounts[0].Writable = true
+
+	// o.Networks[1] is overriding the d.Networks[0].Lima entry for the "def0" interface
+	expect.Networks = append(append(d.Networks, y.Networks...), o.Networks[0])
+	expect.Networks[0].Lima = o.Networks[1].Lima
+	expect.Networks[0].VNL = ""
+	expect.Networks[0].SwitchPort = 0
+
+	// Only highest prio DNS are retained
+	expect.DNS = o.DNS
+
+	// ONE remains from filledDefaults.Env; the rest are set from o
+	expect.Env["ONE"] = y.Env["ONE"]
+
+	FillDefault(&y, &d, &o, filePath)
+	assert.DeepEqual(t, &y, &expect, opts...)
+}