Repository: kurtosis-tech/kurtosis (go)

Issue:
allow us to specify permissions user:group for files artifact and persistent mounts
### Background & motivation

different services need different files with different permissions and user:group:owner settings; Kurtosis doesn't allow that

have to hack in a lot of chmod chown in the entrypoints

Ran into this with @barnabasbusa 

### Desired behaviour

the files artifact mount api allows permission specification

### How important is this to you?

Critical; Kurtosis is unusable for me without it.

### What area of the product does this pertain to?

CLI: the Command Line Interface

Pull request description:
feat: add ability to set uid and gid
## Description:
This allows users to change the user & group with which a container starts

## Is this change user facing?
YES

## References (if applicable):
Closes #2000

Note this is a workaround to solve #2000 while https://github.com/moby/moby/issues/2259 remains open

Patch:
diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/user_services_functions/start_user_services.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/user_services_functions/start_user_services.go
index d04897e59..1bde0965b 100644
--- a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/user_services_functions/start_user_services.go
+++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/user_services_functions/start_user_services.go
@@ -538,6 +538,7 @@ func createStartServiceOperation(
 		cpuAllocationMillicpus := serviceConfig.GetCPUAllocationMillicpus()
 		memoryAllocationMegabytes := serviceConfig.GetMemoryAllocationMegabytes()
 		privateIPAddrPlaceholder := serviceConfig.GetPrivateIPAddrPlaceholder()
+		user := serviceConfig.GetUser()
 
 		// We replace the placeholder value with the actual private IP address
 		privateIPAddrStr := privateIpAddr.String()
@@ -691,7 +692,7 @@ func createStartServiceOperation(
 			fluentdLoggingDriverCnfg,
 		).WithRestartPolicy(
 			restartPolicy,
-		)
+		).WithUser(user)
 
 		if entrypointArgs != nil {
 			createAndStartArgsBuilder.WithEntrypointArgs(entrypointArgs)
diff --git a/container-engine-lib/lib/backend_impls/docker/docker_manager/create_and_start_container_args.go b/container-engine-lib/lib/backend_impls/docker/docker_manager/create_and_start_container_args.go
index dd2eed632..9a1f40c96 100644
--- a/container-engine-lib/lib/backend_impls/docker/docker_manager/create_and_start_container_args.go
+++ b/container-engine-lib/lib/backend_impls/docker/docker_manager/create_and_start_container_args.go
@@ -1,6 +1,7 @@
 package docker_manager
 
 import (
+	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_user"
 	"net"
 
 	"github.com/docker/go-connections/nat"
@@ -32,6 +33,7 @@ type CreateAndStartContainerArgs struct {
 	containerInitEnabled                     bool
 	restartPolicy                            RestartPolicy
 	imageDownloadMode                        image_download_mode.ImageDownloadMode
+	user                                     *service_user.ServiceUser
 }
 
 // Builder for creating CreateAndStartContainerArgs object
@@ -59,6 +61,7 @@ type CreateAndStartContainerArgsBuilder struct {
 	containerInitEnabled                     bool
 	restartPolicy                            RestartPolicy
 	imageDownloadMode                        image_download_mode.ImageDownloadMode
+	user                                     *service_user.ServiceUser
 }
 
 /*
@@ -93,6 +96,7 @@ func NewCreateAndStartContainerArgsBuilder(dockerImage string, name string, netw
 		containerInitEnabled:                     false,
 		restartPolicy:                            NoRestart,
 		imageDownloadMode:                        image_download_mode.ImageDownloadMode_Missing,
+		user:                                     nil,
 	}
 }
 
@@ -121,6 +125,7 @@ func (builder *CreateAndStartContainerArgsBuilder) Build() *CreateAndStartContai
 		containerInitEnabled:                     builder.containerInitEnabled,
 		restartPolicy:                            builder.restartPolicy,
 		imageDownloadMode:                        builder.imageDownloadMode,
+		user:                                     builder.user,
 	}
 }
 
@@ -265,3 +270,8 @@ func (builder *CreateAndStartContainerArgsBuilder) WithFetchingLatestImageIfMiss
 	builder.imageDownloadMode = image_download_mode.ImageDownloadMode_Missing
 	return builder
 }
+
+func (builder *CreateAndStartContainerArgsBuilder) WithUser(user *service_user.ServiceUser) *CreateAndStartContainerArgsBuilder {
+	builder.user = user
+	return builder
+}
diff --git a/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go
index 4a0d99f6c..6bcdcdcdb 100644
--- a/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go
+++ b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go
@@ -557,6 +557,11 @@ func (manager *DockerManager) CreateAndStartContainer(
 		usedPortsSet[port] = struct{}{}
 	}
 
+	var userStr string
+	if args.user != nil {
+		userStr = args.user.GetUIDGIDPairAsStr()
+	}
+
 	containerConfigPtr, err := manager.getContainerCfg(
 		dockerImage,
 		isInteractiveMode,
@@ -565,6 +570,7 @@ func (manager *DockerManager) CreateAndStartContainer(
 		args.cmdArgs,
 		args.envVariables,
 		args.labels,
+		userStr,
 	)
 	if err != nil {
 		return "", nil, stacktrace.Propagate(err, "Failed to configure container from service.")
@@ -1798,7 +1804,8 @@ func (manager *DockerManager) getContainerCfg(
 	entrypointArgs []string,
 	cmdArgs []string,
 	envVariables map[string]string,
-	labels map[string]string) (config *container.Config, err error) {
+	labels map[string]string,
+	user string) (config *container.Config, err error) {
 
 	envVariablesSlice := make([]string, 0, len(envVariables))
 	for key, val := range envVariables {
@@ -1808,7 +1815,7 @@ func (manager *DockerManager) getContainerCfg(
 	nodeConfigPtr := &container.Config{
 		Hostname:        "",
 		Domainname:      "",
-		User:            "",
+		User:            user,
 		AttachStdin:     isInteractiveMode, // Analogous to `-a STDIN` option to `docker run`
 		AttachStdout:    isInteractiveMode, // Analogous to `-a STDOUT` option to `docker run`
 		AttachStderr:    isInteractiveMode, // Analogous to `-a STDERR` option to `docker run`
diff --git a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go
index a9027be49..8d85648b9 100644
--- a/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go
+++ b/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/user_services_functions/start_user_services.go
@@ -3,6 +3,7 @@ package user_services_functions
 import (
 	"context"
 	"fmt"
+	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_user"
 	"strings"
 
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/kubernetes/kubernetes_kurtosis_backend/consts"
@@ -306,6 +307,7 @@ func createStartServiceOperation(
 		privateIPAddrPlaceholder := serviceConfig.GetPrivateIPAddrPlaceholder()
 		minCpuAllocationMilliCpus := serviceConfig.GetMinCPUAllocationMillicpus()
 		minMemoryAllocationMegabytes := serviceConfig.GetMinMemoryAllocationMegabytes()
+		user := serviceConfig.GetUser()
 
 		matchingObjectAndResources, found := servicesObjectsAndResources[serviceUuid]
 		if !found {
@@ -396,6 +398,7 @@ func createStartServiceOperation(
 			memoryAllocationMegabytes,
 			minCpuAllocationMilliCpus,
 			minMemoryAllocationMegabytes,
+			user,
 		)
 		if err != nil {
 			return nil, stacktrace.Propagate(err, "An error occurred creating the container specs for the user service pod with image '%v'", containerImageName)
@@ -632,6 +635,7 @@ func getUserServicePodContainerSpecs(
 	memoryAllocationMegabytes uint64,
 	minCpuAllocationMilliCpus uint64,
 	minMemoryAllocationMegabytes uint64,
+	user *service_user.ServiceUser,
 ) ([]apiv1.Container, error) {
 
 	var containerEnvVars []apiv1.EnvVar
@@ -692,6 +696,22 @@ func getUserServicePodContainerSpecs(
 		},
 	}
 
+	if user != nil {
+		uid := int64(user.GetUID())
+		// nolint: exhaustruct
+		securityContext := &apiv1.SecurityContext{
+			RunAsUser: &uid,
+		}
+
+		gid, gidIsSet := user.GetGID()
+		if gidIsSet {
+			gidAsInt64 := int64(gid)
+			securityContext.RunAsGroup = &gidAsInt64
+		}
+
+		containers[0].SecurityContext = securityContext
+	}
+
 	return containers, nil
 }
 
diff --git a/container-engine-lib/lib/backend_interface/objects/service/service_config.go b/container-engine-lib/lib/backend_interface/objects/service/service_config.go
index 94a684aa8..7952b76fc 100644
--- a/container-engine-lib/lib/backend_interface/objects/service/service_config.go
+++ b/container-engine-lib/lib/backend_interface/objects/service/service_config.go
@@ -5,6 +5,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/image_build_spec"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/port_spec"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_directory"
+	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_user"
 	"github.com/kurtosis-tech/stacktrace"
 )
 
@@ -48,6 +49,8 @@ type privateServiceConfig struct {
 	MinMemoryAllocationMegabytes uint64
 
 	Labels map[string]string
+
+	User *service_user.ServiceUser
 }
 
 func CreateServiceConfig(
@@ -66,6 +69,7 @@ func CreateServiceConfig(
 	minCpuMilliCores uint64,
 	minMemoryMegaBytes uint64,
 	labels map[string]string,
+	user *service_user.ServiceUser,
 ) (*ServiceConfig, error) {
 
 	if err := ValidateServiceConfigLabels(labels); err != nil {
@@ -89,6 +93,7 @@ func CreateServiceConfig(
 		MinCpuAllocationMilliCpus:    minCpuMilliCores,
 		MinMemoryAllocationMegabytes: minMemoryMegaBytes,
 		Labels:                       labels,
+		User:                         user,
 	}
 	return &ServiceConfig{internalServiceConfig}, nil
 }
@@ -151,6 +156,10 @@ func (serviceConfig *ServiceConfig) GetMinMemoryAllocationMegabytes() uint64 {
 	return serviceConfig.privateServiceConfig.MinMemoryAllocationMegabytes
 }
 
+func (serviceConfig *ServiceConfig) GetUser() *service_user.ServiceUser {
+	return serviceConfig.privateServiceConfig.User
+}
+
 func (serviceConfig *ServiceConfig) GetLabels() map[string]string {
 	return serviceConfig.privateServiceConfig.Labels
 }
diff --git a/container-engine-lib/lib/backend_interface/objects/service_user/service_user.go b/container-engine-lib/lib/backend_interface/objects/service_user/service_user.go
new file mode 100644
index 000000000..521903046
--- /dev/null
+++ b/container-engine-lib/lib/backend_interface/objects/service_user/service_user.go
@@ -0,0 +1,46 @@
+package service_user
+
+import (
+	"fmt"
+)
+
+type UID int64
+type GID int64
+
+const (
+	defaultGidValue = -1
+	gidIsNotSet     = false
+)
+
+type ServiceUser struct {
+	uid      UID
+	gid      GID
+	isGIDSet bool
+}
+
+func NewServiceUser(uid UID) *ServiceUser {
+	return &ServiceUser{uid: uid, gid: defaultGidValue, isGIDSet: gidIsNotSet}
+}
+
+func (su *ServiceUser) GetUID() UID {
+	return su.uid
+}
+
+func (su *ServiceUser) GetGID() (GID, bool) {
+	if !su.isGIDSet {
+		return 0, false
+	}
+	return su.gid, true
+}
+
+func (su *ServiceUser) SetGID(gid GID) {
+	su.gid = gid
+	su.isGIDSet = true
+}
+
+func (su *ServiceUser) GetUIDGIDPairAsStr() string {
+	if su.isGIDSet {
+		return fmt.Sprintf("%v:%v", su.uid, su.gid)
+	}
+	return fmt.Sprintf("%v", su.uid)
+}
diff --git a/core/server/api_container/server/docker_compose_transpiler/docker_compose_transpiler.go b/core/server/api_container/server/docker_compose_transpiler/docker_compose_transpiler.go
index 04e46a6ca..1ff690e47 100644
--- a/core/server/api_container/server/docker_compose_transpiler/docker_compose_transpiler.go
+++ b/core/server/api_container/server/docker_compose_transpiler/docker_compose_transpiler.go
@@ -178,6 +178,7 @@ func createStarlarkScript(
 	return script, nil
 }
 
+// TODO add support for User here
 // Turns DockerCompose Service into Kurtosis ServiceConfigs and returns info needed for creating a valid starlark script
 func convertComposeServicesToStarlarkInfo(composeServices types.Services) (
 	map[string]StarlarkServiceConfig, // Map of service names to Kurtosis ServiceConfig's
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_builtins.go b/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
index 807e0bc14..9c8e72867 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_builtins.go
@@ -112,5 +112,6 @@ func KurtosisTypeConstructors() []*starlark.Builtin {
 		starlark.NewBuiltin(service_config.ServiceConfigTypeName, service_config.NewServiceConfigType().CreateBuiltin()),
 		starlark.NewBuiltin(service_config.ReadyConditionTypeName, service_config.NewReadyConditionType().CreateBuiltin()),
 		starlark.NewBuiltin(service_config.ImageBuildSpecTypeName, service_config.NewImageBuildSpecType().CreateBuiltin()),
+		starlark.NewBuiltin(service_config.UserTypeName, service_config.NewUserType().CreateBuiltin()),
 	}
 }
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go
index 5b35126d8..1505bbce3 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared.go
@@ -212,6 +212,7 @@ func replaceMagicStrings(
 		serviceConfig.GetMinCPUAllocationMillicpus(),
 		serviceConfig.GetMinMemoryAllocationMegabytes(),
 		serviceConfig.GetLabels(),
+		serviceConfig.GetUser(),
 	)
 	if err != nil {
 		return "", nil, stacktrace.Propagate(err, "An error occurred creating a service config")
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go
index f10d1c7cd..85dc263b0 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/tasks/tasks_shared.go
@@ -270,6 +270,7 @@ func getServiceConfig(image string, filesArtifactExpansion *service_directory.Fi
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	if err != nil {
 		return nil, stacktrace.Propagate(err, "An error occurred creating service config")
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go
index 298c12f65..df8511012 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_config.go
@@ -6,6 +6,7 @@ import (
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/port_spec"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_directory"
+	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/service_user"
 	"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/uuid_generator"
 	"github.com/kurtosis-tech/kurtosis/core/files_artifacts_expander/args"
 	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/service_network"
@@ -42,6 +43,7 @@ const (
 	MaxCpuMilliCoresAttr            = "max_cpu"
 	MaxMemoryMegaBytesAttr          = "max_memory"
 	LabelsAttr                      = "labels"
+	UserAttr                        = "user"
 
 	DefaultPrivateIPAddrPlaceholder = "KURTOSIS_IP_ADDR_PLACEHOLDER"
 
@@ -180,6 +182,12 @@ func NewServiceConfigType() *kurtosis_type_constructor.KurtosisTypeConstructor {
 						return builtin_argument.ServiceConfigLabels(value, LabelsAttr)
 					},
 				},
+				{
+					Name:              UserAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[*User],
+					Validator:         nil,
+				},
 			},
 		},
 
@@ -424,6 +432,26 @@ func (config *ServiceConfig) ToKurtosisType(
 		}
 	}
 
+	var serviceUser *service_user.ServiceUser
+	user, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[*User](config.KurtosisValueTypeDefault, UserAttr)
+	if interpretationErr != nil {
+		return nil, interpretationErr
+	}
+	if found {
+		uid, interpretationErr := user.GetUID()
+		if interpretationErr != nil {
+			return nil, interpretationErr
+		}
+		serviceUser = service_user.NewServiceUser(service_user.UID(uid))
+		gid, gidFound, interpretationErr := user.GetGIDIfSet()
+		if interpretationErr != nil {
+			return nil, interpretationErr
+		}
+		if gidFound {
+			serviceUser.SetGID(service_user.GID(gid))
+		}
+	}
+
 	serviceConfig, err := service.CreateServiceConfig(
 		imageName,
 		maybeImageBuildSpec,
@@ -440,6 +468,7 @@ func (config *ServiceConfig) ToKurtosisType(
 		minCpu,
 		minMemory,
 		labels,
+		serviceUser,
 	)
 	if err != nil {
 		return nil, startosis_errors.WrapWithInterpretationError(err, "An error occurred creating a service config")
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_user.go b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_user.go
new file mode 100644
index 000000000..caac65058
--- /dev/null
+++ b/core/server/api_container/server/startosis_engine/kurtosis_types/service_config/service_user.go
@@ -0,0 +1,103 @@
+package service_config
+
+import (
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/builtin_argument"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/kurtosis_type_constructor"
+	"github.com/kurtosis-tech/kurtosis/core/server/api_container/server/startosis_engine/startosis_errors"
+	"go.starlark.net/starlark"
+	"math"
+)
+
+const (
+	UserTypeName = "User"
+
+	UIDAttr = "uid"
+	GIDAttr = "gid"
+
+	idIsAtLeast0 = 0
+)
+
+func NewUserType() *kurtosis_type_constructor.KurtosisTypeConstructor {
+	return &kurtosis_type_constructor.KurtosisTypeConstructor{
+		KurtosisBaseBuiltin: &kurtosis_starlark_framework.KurtosisBaseBuiltin{
+			Name: UserTypeName,
+			Arguments: []*builtin_argument.BuiltinArgument{
+				{
+					Name:              UIDAttr,
+					IsOptional:        false,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.Int],
+					Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
+						return builtin_argument.Int64InRange(value, UIDAttr, idIsAtLeast0, math.MaxInt64)
+					},
+				},
+				{
+					Name:              GIDAttr,
+					IsOptional:        true,
+					ZeroValueProvider: builtin_argument.ZeroValueProvider[starlark.Int],
+					Validator: func(value starlark.Value) *startosis_errors.InterpretationError {
+						return builtin_argument.Int64InRange(value, GIDAttr, idIsAtLeast0, math.MaxInt64)
+					},
+				},
+			},
+			Deprecation: nil,
+		},
+		Instantiate: instantiate,
+	}
+}
+
+func instantiate(arguments *builtin_argument.ArgumentValuesSet) (builtin_argument.KurtosisValueType, *startosis_errors.InterpretationError) {
+	kurtosisValueType, interpretationErr := kurtosis_type_constructor.CreateKurtosisStarlarkTypeDefault(UserTypeName, arguments)
+	if interpretationErr != nil {
+		return nil, interpretationErr
+	}
+	return &User{
+		kurtosisValueType,
+	}, nil
+}
+
+type User struct {
+	*kurtosis_type_constructor.KurtosisValueTypeDefault
+}
+
+func (user *User) Copy() (builtin_argument.KurtosisValueType, error) {
+	copiedValueType, err := user.KurtosisValueTypeDefault.Copy()
+	if err != nil {
+		return nil, err
+	}
+	return &User{
+		KurtosisValueTypeDefault: copiedValueType,
+	}, nil
+}
+
+func (user *User) GetUID() (int64, *startosis_errors.InterpretationError) {
+	uidValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.Int](
+		user.KurtosisValueTypeDefault, UIDAttr)
+	if interpretationErr != nil {
+		return 0, interpretationErr
+	}
+	if !found {
+		return 0, startosis_errors.NewInterpretationError("Required attribute '%v' couldn't be found on '%v' type", UIDAttr, UserTypeName)
+	}
+	uid, ok := uidValue.Int64()
+	if !ok {
+		return 0, startosis_errors.NewInterpretationError("Couldn't convert uid '%v' to int64", uidValue)
+	}
+	return uid, nil
+}
+
+func (user *User) GetGIDIfSet() (int64, bool, *startosis_errors.InterpretationError) {
+	gidValue, found, interpretationErr := kurtosis_type_constructor.ExtractAttrValue[starlark.Int](
+		user.KurtosisValueTypeDefault, GIDAttr)
+	if interpretationErr != nil {
+		return 0, false, interpretationErr
+	}
+	if !found {
+		return 0, false, nil
+	}
+	gid, ok := gidValue.Int64()
+	if !ok {
+		return 0, false, startosis_errors.NewInterpretationError("Couldn't convert gid '%v' to int64", gidValue)
+	}
+	return gid, true, nil
+}
diff --git a/docs/docs/api-reference/starlark-reference/service-config.md b/docs/docs/api-reference/starlark-reference/service-config.md
index 0805b1451..7f8158165 100644
--- a/docs/docs/api-reference/starlark-reference/service-config.md
+++ b/docs/docs/api-reference/starlark-reference/service-config.md
@@ -131,6 +131,13 @@ config = ServiceConfig(
         # Examples
         "name": "alice",
     }
+
+    # The user id and group id to start the container with
+    # Some containers are configured to start with users other than root by default; this allows you to override to root or other
+    # users if you choose to.
+    # Note that the `gid` field is optional
+    # OPTIONAL
+    user = User(uid=0, gid=0),
 )
 ```
 Note that `ImageBuildSpec` can only be used in packages and not standalone scripts as it relies on build context in package.
@@ -179,10 +186,13 @@ labels:
 ```
 :::
 
+The `user` field expects a `User`[user] object being passed.
+
 <!--------------- ONLY LINKS BELOW THIS POINT ---------------------->
 [add-service-reference]: ./plan.md#add_service
 [directory]: ./directory.md
 [port-spec]: ./port-spec.md
 [ready-condition]: ./ready-condition.md
 [locators]: ../../advanced-concepts/locators.md
-[package]: ../../advanced-concepts/packages.md
\ No newline at end of file
+[package]: ../../advanced-concepts/packages.md
+[user]: ./user.md
\ No newline at end of file
diff --git a/docs/docs/api-reference/starlark-reference/user.md b/docs/docs/api-reference/starlark-reference/user.md
new file mode 100644
index 000000000..15391fbb5
--- /dev/null
+++ b/docs/docs/api-reference/starlark-reference/user.md
@@ -0,0 +1,19 @@
+---
+title: User
+sidebar_label: User
+---
+
+The `User` constructor creates a `User` object that represents a `uid` and `gid` that the service starts with(see `ServiceConfig`[service-config] object)
+
+```python
+user = User(uid=0)
+
+# or
+
+user = User(uid=0, gid=0)
+```
+
+Note that the `gid` is completely optional.
+
+<!--------------- ONLY LINKS BELOW THIS POINT ---------------------->
+[service-config]: ./service-config.md
\ No newline at end of file


Test patch:
diff --git a/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go b/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go
index d5a6ad0fb..92fecd44c 100644
--- a/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go
+++ b/container-engine-lib/lib/backend_interface/objects/service/service_config_test.go
@@ -76,6 +76,7 @@ func getServiceConfigForTest(t *testing.T, imageName string) *ServiceConfig {
 			"test-label-key":        "test-label-value",
 			"test-second-label-key": "test-second-label-value",
 		},
+		nil,
 	)
 	require.NoError(t, err)
 	return serviceConfig
diff --git a/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go b/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go
index b7201e363..880425a76 100644
--- a/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go
+++ b/container-engine-lib/lib/database_accessors/enclave_db/service_registration/repository_test.go
@@ -321,6 +321,7 @@ func getServiceConfigForTest(t *testing.T, imageName string) *service.ServiceCon
 			"test-label-key":        "test-label-value",
 			"test-second-label-key": "test-second-label-value",
 		},
+		nil,
 	)
 	require.NoError(t, err)
 	return serviceConfig
diff --git a/core/server/api_container/server/service_network/default_service_network_test.go b/core/server/api_container/server/service_network/default_service_network_test.go
index 7199520e8..4cf738dee 100644
--- a/core/server/api_container/server/service_network/default_service_network_test.go
+++ b/core/server/api_container/server/service_network/default_service_network_test.go
@@ -1220,6 +1220,7 @@ func testServiceConfig(t *testing.T, imageName string) *service.ServiceConfig {
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 	return serviceConfig
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go
index caa4f96d3..b5c39f0b1 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_instruction/add_service/add_service_shared_test.go
@@ -52,6 +52,7 @@ func TestAddServiceShared_EntryPointArgsRuntimeValueAreReplaced(t *testing.T) {
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 
@@ -94,6 +95,7 @@ func TestAddServiceShared_CmdArgsRuntimeValueAreReplaced(t *testing.T) {
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 
@@ -138,6 +140,7 @@ func TestAddServiceShared_EnvVarsWithRuntimeValueAreReplaced(t *testing.T) {
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 
@@ -183,6 +186,7 @@ func TestAddServiceShared_ServiceNameWithRuntimeValuesAreReplaced(t *testing.T)
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go
index 8684f78f3..ffa912ae4 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_service_framework_test.go
@@ -48,6 +48,7 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddService() {
 				0,
 				0,
 				map[string]string{},
+				nil,
 			)
 			require.NoError(suite.T(), err)
 
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go
index 2462fb363..2f76d1eb8 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/add_services_framework_test.go
@@ -64,6 +64,7 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddServices() {
 				0,
 				0,
 				map[string]string{},
+				nil,
 			)
 			require.NoError(suite.T(), err)
 
@@ -86,6 +87,7 @@ func (suite *KurtosisPlanInstructionTestSuite) TestAddServices() {
 				0,
 				0,
 				map[string]string{},
+				nil,
 			)
 			require.NoError(suite.T(), err)
 
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go
index fc124e519..3a7932369 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_image_build_spec_test.go
@@ -83,6 +83,7 @@ func (t *serviceConfigImageBuildSpecTestCase) Assert(typeValue builtin_argument.
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 	require.Equal(t, expectedServiceConfig, serviceConfig)
diff --git a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go
index 6972a07ef..e48ea2098 100644
--- a/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go
+++ b/core/server/api_container/server/startosis_engine/kurtosis_starlark_framework/test_engine/service_config_minimal_framework_test.go
@@ -60,6 +60,7 @@ func (t *serviceConfigMinimalTestCase) Assert(typeValue builtin_argument.Kurtosi
 		0,
 		0,
 		map[string]string{},
+		nil,
 	)
 	require.NoError(t, err)
 	require.Equal(t, expectedServiceConfig, serviceConfig)
diff --git a/internal_testsuites/golang/testsuite/startlark_user_passing_test/starlark_user_passing_test.go b/internal_testsuites/golang/testsuite/startlark_user_passing_test/starlark_user_passing_test.go
new file mode 100644
index 000000000..b41104f7e
--- /dev/null
+++ b/internal_testsuites/golang/testsuite/startlark_user_passing_test/starlark_user_passing_test.go
@@ -0,0 +1,75 @@
+package startlark_user_passing_test
+
+import (
+	"context"
+	"github.com/kurtosis-tech/kurtosis-cli/golang_internal_testsuite/test_helpers"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+const (
+	testName                = "starlark-user-passing-test"
+	noOverrideServiceName   = "no-override"
+	userOverrideServiceName = "user-override"
+
+	starlarkScriptWithUserIdPassed = `
+IMAGE = "hyperledger/besu:latest"
+def run(plan, args):
+	no_override = plan.add_service(
+		name = "` + noOverrideServiceName + `",
+		config = ServiceConfig(
+			image = IMAGE,
+			cmd = ["tail", "-f", "/dev/null"],
+		)
+	)
+
+	plan.exec(service_name = no_override.name, recipe = ExecRecipe(command = ["whoami"]))
+
+	root_override = plan.add_service(
+		name = "` + userOverrideServiceName + `",
+		config = ServiceConfig(
+			image = IMAGE,
+			cmd = ["tail", "-f", "/dev/null"],
+			user = User(uid=0, gid=0),
+		)
+	)
+
+	plan.exec(service_name = root_override.name, recipe = ExecRecipe(command = ["whoami"]))
+`
+)
+
+func TestUserIDOverridesWork(t *testing.T) {
+	ctx := context.Background()
+	// ------------------------------------- ENGINE SETUP ----------------------------------------------
+	enclaveCtx, _, destroyEnclaveFunc, err := test_helpers.CreateEnclave(t, ctx, testName)
+	require.NoError(t, err, "An error occurred creating an enclave")
+	defer func() {
+		err = destroyEnclaveFunc()
+		require.NoError(t, err, "An error occurred destroying the enclave after the test finished")
+	}()
+
+	// ------------------------------------- TEST RUN ----------------------------------------------
+	scriptRunResult, err := test_helpers.RunScriptWithDefaultConfig(ctx, enclaveCtx, starlarkScriptWithUserIdPassed)
+	logrus.Infof("Test Output: %v", scriptRunResult)
+	require.NoError(t, err, "Unexpected error executing starlark script")
+	require.Nil(t, scriptRunResult.InterpretationError, "Unexpected interpretation error")
+	require.Empty(t, scriptRunResult.ValidationErrors, "Unexpected validation error")
+	require.Nil(t, scriptRunResult.ExecutionError, "Unexpected execution error")
+	expectedOutput := `Service '` + noOverrideServiceName + `' added with service UUID '[a-z0-9]{32}'
+Command returned with exit code '0' and the following output:
+--------------------
+besu
+
+--------------------
+Service '` + userOverrideServiceName + `' added with service UUID '[a-z0-9]{32}'
+Command returned with exit code '0' and the following output:
+--------------------
+root
+
+--------------------
+`
+
+	require.Regexp(t, expectedOutput, scriptRunResult.RunOutput)
+
+}