Installer: Make Private network optional

Change-Id: Ic7a2e5250a42dc03de2416b1e2a0d1bbca3f010c
diff --git a/core/installer/app.go b/core/installer/app.go
index d86c9f3..0c9cf6b 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -115,9 +115,18 @@
 	InfraAdminPublicKey  []byte   `json:"infraAdminPublicKey,omitempty"`
 }
 
+type InfraNetwork struct {
+	Name               string `json:"name,omitempty"`
+	IngressClass       string `json:"ingressClass,omitempty"`
+	CertificateIssuer  string `json:"certificateIssuer,omitempty"`
+	AllocatePortAddr   string `json:"allocatePortAddr,omitempty"`
+	ReservePortAddr    string `json:"reservePortAddr,omitempty"`
+	DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
+}
+
 type InfraApp interface {
 	App
-	Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
+	Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error)
 }
 
 type EnvNetwork struct {
@@ -492,7 +501,7 @@
 	return AppTypeInfra
 }
 
-func (a cueInfraApp) Render(release Release, infra InfraConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error) {
+func (a cueInfraApp) Render(release Release, infra InfraConfig, networks []InfraNetwork, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (InfraAppRendered, error) {
 	if charts == nil {
 		charts = make(map[string]helmv2.HelmChartTemplateSpec)
 	}
@@ -501,6 +510,7 @@
 		"release":     release,
 		"input":       values,
 		"localCharts": charts,
+		"networks":    InfraNetworkMap(networks),
 	})
 	if err != nil {
 		return InfraAppRendered{}, err
@@ -538,3 +548,11 @@
 	}
 	return ret
 }
+
+func InfraNetworkMap(networks []InfraNetwork) map[string]InfraNetwork {
+	ret := make(map[string]InfraNetwork)
+	for _, n := range networks {
+		ret[strings.ToLower(n.Name)] = n
+	}
+	return ret
+}
diff --git a/core/installer/app_configs/app_base.cue b/core/installer/app_configs/app_base.cue
index 34e0a72..6529054 100644
--- a/core/installer/app_configs/app_base.cue
+++ b/core/installer/app_configs/app_base.cue
@@ -26,16 +26,6 @@
   groups: string | *"" // TODO(gio): []string
 }
 
-#Network: {
-	name: string
-	ingressClass: string
-	certificateIssuer: string | *""
-	domain: string
-	allocatePortAddr: string
-	reservePortAddr: string
-	deallocatePortAddr: string
-}
-
 #Image: {
 	registry: string | *release.imageRegistry
 	repository: string
diff --git a/core/installer/app_configs/app_global_env.cue b/core/installer/app_configs/app_global_env.cue
index 9a306f5..f5858e4 100644
--- a/core/installer/app_configs/app_global_env.cue
+++ b/core/installer/app_configs/app_global_env.cue
@@ -11,13 +11,22 @@
 	network: #EnvNetwork
 }
 
-networks: {}
+#Network: {
+	name: string
+	ingressClass: string
+	certificateIssuer: string | *""
+	domain: string
+	allocatePortAddr: string
+	reservePortAddr: string
+	deallocatePortAddr: string
+}
 
-// TODO(gio): remove
-ingressPrivate: "\(global.id)-ingress-private"
-ingressPublic: "\(global.pcloudEnvName)-ingress-public"
-issuerPrivate: "\(global.id)-private"
-issuerPublic: "\(global.id)-public"
+#Networks: {
+	public: #Network
+	...
+}
+
+networks: #Networks
 
 #Ingress: {
 	auth: #Auth
diff --git a/core/installer/app_configs/app_global_infra.cue b/core/installer/app_configs/app_global_infra.cue
index 578a30b..937d490 100644
--- a/core/installer/app_configs/app_global_infra.cue
+++ b/core/installer/app_configs/app_global_infra.cue
@@ -5,8 +5,20 @@
     infraAdminPublicKey: string | *""
 }
 
-// TODO(gio): remove
-ingressPublic: "\(global.pcloudEnvName)-ingress-public"
+#Network: {
+	name: string
+	ingressClass: string
+	certificateIssuer: string | *""
+	allocatePortAddr: string
+	reservePortAddr: string
+	deallocatePortAddr: string
+}
+
+#Networks: {
+	public: #Network
+}
+
+networks: #Networks
 
 ingress: {}
 _ingressValidate: {}
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 7274e03..6cf1da1 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -465,6 +465,7 @@
 	if err := setPortFields(values, portReservations); err != nil {
 		return ReleaseResources{}, err
 	}
+	// TODO(gio): env might not have private domain
 	imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
 	if o.FetchContainerImages {
 		if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
@@ -612,7 +613,6 @@
 	return nil
 }
 
-// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
 func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
 	ret := []Network{
 		{
@@ -624,14 +624,16 @@
 			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
 			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
 		},
-		{
+	}
+	if env.PrivateDomain != "" {
+		ret = append(ret, Network{
 			Name:               "Private",
 			IngressClass:       fmt.Sprintf("%s-ingress-private", env.Id),
 			Domain:             env.PrivateDomain,
 			AllocatePortAddr:   fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
 			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
 			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
-		},
+		})
 	}
 	n, err := m.FindAllAppInstances("network")
 	if err != nil {
@@ -799,7 +801,8 @@
 		RepoAddr:  m.repoIO.FullAddress(),
 		AppDir:    appDir,
 	}
-	rendered, err := app.Render(release, infra, values, nil)
+	networks := m.CreateNetworks(infra)
+	rendered, err := app.Render(release, infra, networks, values, nil)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -808,7 +811,7 @@
 		return ReleaseResources{}, err
 	}
 	localCharts := generateLocalCharts(m.lg, charts)
-	rendered, err = app.Render(release, infra, values, localCharts)
+	rendered, err = app.Render(release, infra, networks, values, localCharts)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -831,7 +834,7 @@
 	if err := m.repoIO.Pull(); err != nil {
 		return ReleaseResources{}, err
 	}
-	env, err := m.Config()
+	infra, err := m.Config()
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -853,7 +856,8 @@
 	if err != nil {
 		return ReleaseResources{}, err
 	}
-	rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
+	networks := m.CreateNetworks(infra)
+	rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -867,6 +871,19 @@
 	}, nil
 }
 
+func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
+	return []InfraNetwork{
+		{
+			Name:               "Public",
+			IngressClass:       fmt.Sprintf("%s-ingress-public", infra.Name),
+			CertificateIssuer:  fmt.Sprintf("%s-public", infra.Name),
+			AllocatePortAddr:   fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
+			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
+			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
+		},
+	}
+}
+
 func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
 	ret := make(map[string]string)
 	for name, chart := range charts.Git {
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 5051cc9..53962fe 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -27,6 +27,17 @@
 		},
 	}
 
+	infraNetworks = []InfraNetwork{
+		{
+			Name:               "Public",
+			IngressClass:       fmt.Sprintf("%s-ingress-public", env.InfraName),
+			CertificateIssuer:  fmt.Sprintf("%s-public", env.Id),
+			AllocatePortAddr:   fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
+			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
+			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
+		},
+	}
+
 	networks = []Network{
 		{
 			Name:               "Public",
@@ -122,6 +133,7 @@
 		Namespace: "foo",
 	}
 	values := map[string]any{
+		"network":    "Public",
 		"authGroups": "foo,bar",
 	}
 	rendered, err := a.Render(release, env, networks, values, nil)
@@ -209,7 +221,7 @@
 	values := map[string]any{
 		"sshPrivateKey": "private",
 	}
-	rendered, err := a.Render(release, infra, values, nil)
+	rendered, err := a.Render(release, infra, infraNetworks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -311,7 +323,7 @@
 	values := map[string]any{
 		"servers": []EnvDNS{EnvDNS{"v1.dodo.cloud", "10.0.1.2"}},
 	}
-	rendered, err := app.Render(release, infra, values, nil)
+	rendered, err := app.Render(release, infra, infraNetworks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 6f02fa3..130a89a 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -33,17 +33,22 @@
 }
 
 func SetupInfra(env installer.EnvConfig, st *state) Task {
-	return newConcurrentParentTask(
-		"Setup core services",
-		true,
+	tasks := []Task{
 		SetupNetwork(env, st),
 		SetupCertificateIssuers(env, st),
 		SetupAuth(env, st),
 		SetupGroupMemberships(env, st),
-		SetupHeadscale(env, st),
 		SetupWelcome(env, st),
 		SetupAppStore(env, st),
 		SetupLauncher(env, st),
+	}
+	if env.PrivateDomain != "" {
+		tasks = append(tasks, SetupHeadscale(env, st))
+	}
+	return newConcurrentParentTask(
+		"Setup core services",
+		true,
+		tasks...,
 	)
 }
 
@@ -94,7 +99,7 @@
 }
 
 func SetupNetwork(env installer.EnvConfig, st *state) Task {
-	t := newLeafTask("Setup private and public networks", func() error {
+	t := newLeafTask("Setup networks", func() error {
 		{
 			app, err := installer.FindEnvApp(st.appsRepo, "metallb-ipaddresspool")
 			if err != nil {
@@ -143,7 +148,7 @@
 				}
 			}
 		}
-		{
+		if env.PrivateDomain != "" {
 			keys, err := installer.NewSSHKeyPair("port-allocator")
 			if err != nil {
 				return err
@@ -187,25 +192,31 @@
 		instanceId := app.Slug()
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
-		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
+		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network": "Public",
+		}); err != nil {
 			return err
 		}
 		return nil
 	})
-	priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
-		app, err := installer.FindEnvApp(st.appsRepo, "certificate-issuer-private")
-		if err != nil {
-			return err
-		}
-		instanceId := app.Slug()
-		appDir := fmt.Sprintf("/apps/%s", instanceId)
-		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
-		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
-			return err
-		}
-		return nil
-	})
-	return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
+	tasks := []Task{&pub}
+	if env.PrivateDomain != "" {
+		priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
+			app, err := installer.FindEnvApp(st.appsRepo, "certificate-issuer-private")
+			if err != nil {
+				return err
+			}
+			instanceId := app.Slug()
+			appDir := fmt.Sprintf("/apps/%s", instanceId)
+			namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
+			if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{}); err != nil {
+				return err
+			}
+			return nil
+		})
+		tasks = append(tasks, &priv)
+	}
+	return newSequentialParentTask("Configure TLS certificate issuers", false, tasks...)
 }
 
 func SetupAuth(env installer.EnvConfig, st *state) Task {
@@ -218,6 +229,7 @@
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
 		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network":   "Public",
 			"subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
 		}); err != nil {
 			return err
@@ -241,7 +253,12 @@
 		instanceId := app.Slug()
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
+		network := "Public"
+		if env.PrivateDomain != "" {
+			network = "Private"
+		}
 		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network":    network,
 			"authGroups": strings.Join(initGroups, ","),
 		}); err != nil {
 			return err
@@ -277,6 +294,7 @@
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
 		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network":       "Public",
 			"repoAddr":      st.ssClient.GetRepoAddress("config"),
 			"sshPrivateKey": string(keys.RawPrivateKey()),
 		}); err != nil {
@@ -302,6 +320,7 @@
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
 		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network":   "Public",
 			"subdomain": "headscale",
 			"ipSubnet":  fmt.Sprintf("%s/24", env.Network.DNS.String()),
 		}); err != nil {
@@ -338,6 +357,7 @@
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
 		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network":       "Public",
 			"repoAddr":      st.ssClient.GetRepoAddress("config"),
 			"sshPrivateKey": string(keys.RawPrivateKey()),
 		}); err != nil {
@@ -373,7 +393,12 @@
 		instanceId := app.Slug()
 		appDir := fmt.Sprintf("/apps/%s", instanceId)
 		namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
+		network := "Public"
+		if env.PrivateDomain != "" {
+			network = "Private"
+		}
 		if _, err := st.appManager.Install(app, instanceId, appDir, namespace, map[string]any{
+			"network":       network,
 			"repoAddr":      st.ssClient.GetRepoAddress("config"),
 			"sshPrivateKey": string(keys.RawPrivateKey()),
 			"authGroups":    strings.Join(initGroups, ","),
diff --git a/core/installer/values-tmpl/appmanager.cue b/core/installer/values-tmpl/appmanager.cue
index 7ce72e2..fd162f1 100644
--- a/core/installer/values-tmpl/appmanager.cue
+++ b/core/installer/values-tmpl/appmanager.cue
@@ -3,9 +3,10 @@
 )
 
 input: {
-	repoAddr: string
-	sshPrivateKey: string
-	authGroups: string
+	network: #Network @name(Network)
+	repoAddr: string @name(Repository Address)
+	sshPrivateKey: string @name(SSH Private Key)
+	authGroups: string @name(Allowed Groups)
 }
 
 name: "App Manager"
@@ -15,7 +16,7 @@
 _subdomain: "apps"
 _httpPortName: "http"
 
-_domain: "\(_subdomain).\(networks.private.domain)"
+_domain: "\(_subdomain).\(input.network.domain)"
 url: "https://\(_domain)"
 
 ingress: {
@@ -24,7 +25,7 @@
 			enabled: true
 			groups: input.authGroups
 		}
-		network: networks.private
+		network: input.network
 		subdomain: _subdomain
 		service: {
 			name: "appmanager"
@@ -58,7 +59,7 @@
 			repoAddr: input.repoAddr
 			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
 			ingress: {
-				className: networks.private.ingressClass
+				className: input.network.ingressClass
 				domain: _domain
 				certificateIssuer: ""
 			}
diff --git a/core/installer/values-tmpl/cert-manager.cue b/core/installer/values-tmpl/cert-manager.cue
index 4b6154a..40f39c0 100644
--- a/core/installer/values-tmpl/cert-manager.cue
+++ b/core/installer/values-tmpl/cert-manager.cue
@@ -53,7 +53,7 @@
 		chart: charts.certManager
 		dependsOn: [{
 			name: "ingress-public"
-			namespace: ingressPublic
+			namespace: "\(global.pcloudEnvName)-ingress-public"
 		}]
 		values: {
 			fullnameOverride: "\(global.pcloudEnvName)-cert-manager"
diff --git a/core/installer/values-tmpl/certificate-issuer-custom.cue b/core/installer/values-tmpl/certificate-issuer-custom.cue
index 382e8fa..2cc7ef7 100644
--- a/core/installer/values-tmpl/certificate-issuer-custom.cue
+++ b/core/installer/values-tmpl/certificate-issuer-custom.cue
@@ -12,7 +12,7 @@
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 48 48'><g fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='4'><path d='M4 34h8v8H4zM8 6h32v12H8zm16 28V18'/><path d='M8 34v-8h32v8m-4 0h8v8h-8zm-16 0h8v8h-8zm-6-22h2'/></g></svg>"
 
 charts: {
-	"certificate-issuer-public": {
+	"certificate-issuer": {
 		kind: "GitRepository"
 		address: "https://github.com/giolekva/pcloud.git"
 		branch: "main"
@@ -21,7 +21,7 @@
 }
 
 helm: {
-	"certificate-issuer-public": {
+	"certificate-issuer": {
 		chart: charts["certificate-issuer-public"]
 		dependsOn: [{
 			name: "ingress-nginx"
@@ -31,10 +31,9 @@
 			issuer: {
 				name: input.name
 				server: "https://acme-v02.api.letsencrypt.org/directory"
-				// server: "https://acme-staging-v02.api.letsencrypt.org/directory"
 				domain: input.domain
 				contactEmail: global.contactEmail
-				ingressClass: ingressPublic
+				ingressClass: networks.public.ingressClass
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/certificate-issuer-private.cue b/core/installer/values-tmpl/certificate-issuer-private.cue
index eef76d3..4707fa1 100644
--- a/core/installer/values-tmpl/certificate-issuer-private.cue
+++ b/core/installer/values-tmpl/certificate-issuer-private.cue
@@ -23,9 +23,8 @@
 		}]
 		values: {
 			issuer: {
-				name: issuerPrivate
+				name: "\(global.id)-private"
 				server: "https://acme-v02.api.letsencrypt.org/directory"
-				// server: "https://acme-staging-v02.api.letsencrypt.org/directory"
 				domain: global.privateDomain
 				contactEmail: global.contactEmail
 			}
diff --git a/core/installer/values-tmpl/certificate-issuer-public.cue b/core/installer/values-tmpl/certificate-issuer-public.cue
index 35242bf..725c3b2 100644
--- a/core/installer/values-tmpl/certificate-issuer-public.cue
+++ b/core/installer/values-tmpl/certificate-issuer-public.cue
@@ -1,4 +1,6 @@
-input: {}
+input: {
+	network: #Network
+}
 
 images: {}
 
@@ -23,12 +25,11 @@
 		}]
 		values: {
 			issuer: {
-				name: issuerPublic
+				name: input.network.certificateIssuer
 				server: "https://acme-v02.api.letsencrypt.org/directory"
-				// server: "https://acme-staging-v02.api.letsencrypt.org/directory"
-				domain: global.domain
+				domain: input.network.domain
 				contactEmail: global.contactEmail
-				ingressClass: ingressPublic
+				ingressClass: input.network.ingressClass
 			}
 		}
 	}
diff --git a/core/installer/values-tmpl/core-auth.cue b/core/installer/values-tmpl/core-auth.cue
index e2f05c4..9f6157a 100644
--- a/core/installer/values-tmpl/core-auth.cue
+++ b/core/installer/values-tmpl/core-auth.cue
@@ -1,4 +1,5 @@
 input: {
+	network: #Network
 	subdomain: string
 }
 
@@ -154,39 +155,24 @@
 					}
 				}
 				ingress: {
-					admin: {
-						enabled: true
-						className: ingressPrivate
-						hosts: [{
-							host: "kratos.\(global.privateDomain)"
-							paths: [{
-								path: "/"
-								pathType: "Prefix"
-							}]
-						}]
-						tls: [{
-							hosts: [
-								"kratos.\(global.privateDomain)"
-						]
-						}]
-					}
+					admin: enabled: false
 					public: {
 						enabled: true
-						className: ingressPublic
+						className: input.network.ingressClass
 						annotations: {
 							"acme.cert-manager.io/http01-edit-in-place": "true"
-							"cert-manager.io/cluster-issuer": issuerPublic
+							"cert-manager.io/cluster-issuer": input.network.certificateIssuer
 						}
 						hosts: [{
-							host: "accounts.\(global.domain)"
+							host: "accounts.\(input.network.domain)"
 							paths: [{
 								path: "/"
 								pathType: "Prefix"
 							}]
 						}]
 						tls: [{
-							hosts: ["accounts.\(global.domain)"]
-							secretName: "cert-accounts.\(global.domain)"
+							hosts: ["accounts.\(input.network.domain)"]
+							secretName: "cert-accounts.\(input.network.domain)"
 						}]
 					}
 				}
@@ -206,25 +192,26 @@
 						dsn: "postgres://kratos:kratos@postgres.\(global.namespacePrefix)core-auth.svc:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4"
 						serve: {
 							public: {
-								base_url: "https://accounts.\(global.domain)"
+								base_url: "https://accounts.\(input.network.domain)"
 								cors: {
 									enabled: true
 									debug: false
 									allow_credentials: true
 									allowed_origins: [
-										"https://\(global.domain)",
-										"https://*.\(global.domain)",
+										"https://\(input.network.domain)",
+										"https://*.\(input.network.domain)",
 								]
 								}
 							}
 							admin: {
-								base_url: "https://kratos.\(global.privateDomain)/"
+								base_url: "https://kratos-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
 							}
 						}
 						selfservice: {
-							default_browser_return_url: "https://accounts-ui.\(global.domain)"
+							default_browser_return_url: "https://accounts-ui.\(input.network.domain)"
 							allowed_return_urls: [
-								"https://*.\(global.domain)/",
+								"https://*.\(input.network.domain)/",
+								// TODO(gio): replace with input.network.privateSubdomain
 								"https://*.\(global.privateDomain)",
 						    ]
 							methods: {
@@ -234,10 +221,10 @@
 							}
 							flows: {
 								error: {
-									ui_url: "https://accounts-ui.\(global.domain)/error"
+									ui_url: "https://accounts-ui.\(input.network.domain)/error"
 								}
 								settings: {
-									ui_url: "https://accounts-ui.\(global.domain)/settings"
+									ui_url: "https://accounts-ui.\(input.network.domain)/settings"
 									privileged_session_max_age: "15m"
 								}
 								recovery: {
@@ -248,27 +235,27 @@
 								}
 								logout: {
 									after: {
-										default_browser_return_url: "https://accounts-ui.\(global.domain)/login"
+										default_browser_return_url: "https://accounts-ui.\(input.network.domain)/login"
 									}
 								}
 								login: {
-									ui_url: "https://accounts-ui.\(global.domain)/login"
+									ui_url: "https://accounts-ui.\(input.network.domain)/login"
 									lifespan: "10m"
 									after: {
 										password: {
-											default_browser_return_url: "https://accounts-ui.\(global.domain)/"
+											default_browser_return_url: "https://accounts-ui.\(input.network.domain)/"
 										}
 									}
 								}
 								registration: {
 									lifespan: "10m"
-									ui_url: "https://accounts-ui.\(global.domain)/register"
+									ui_url: "https://accounts-ui.\(input.network.domain)/register"
 									after: {
 										password: {
 											hooks: [{
 												hook: "session"
 											}]
-											default_browser_return_url: "https://accounts-ui.\(global.domain)/"
+											default_browser_return_url: "https://accounts-ui.\(input.network.domain)/"
 										}
 									}
 								}
@@ -282,7 +269,7 @@
 						cookies: {
 							path: "/"
 							same_site: "None"
-							domain: global.domain
+							domain: input.network.domain
 						}
 						secrets: {
 							cookie: ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
@@ -305,7 +292,7 @@
 						}
 						courier: {
 							smtp: {
-								connection_uri: "smtps://test-z1VmkYfYPjgdPRgPFgmeZ31esT9rUgS%40\(global.domain):iW%213Kk%5EPPLFrZa%24%21bbpTPN9Wv3b8mvwS6ZJvMLtce%23A2%2A4MotD@mx1.\(global.domain)"
+								connection_uri: "smtps://test-z1VmkYfYPjgdPRgPFgmeZ31esT9rUgS%40\(input.network.domain):iW%213Kk%5EPPLFrZa%24%21bbpTPN9Wv3b8mvwS6ZJvMLtce%23A2%2A4MotD@mx1.\(input.network.domain)"
 							}
 						}
 					}
@@ -336,37 +323,24 @@
 					}
 				}
 				ingress: {
-					admin: {
-						enabled: true
-						className: ingressPrivate
-						hosts: [{
-							host: "hydra.\(global.privateDomain)"
-							paths: [{
-								path: "/"
-								pathType: "Prefix"
-							}]
-							   }]
-						tls: [{
-							hosts: ["hydra.\(global.privateDomain)"]
-						}]
-					}
+					admin: enabled: false
 					public: {
 						enabled: true
-						className: ingressPublic
+						className: input.network.ingressClass
 						annotations: {
 							"acme.cert-manager.io/http01-edit-in-place": "true"
-							"cert-manager.io/cluster-issuer": issuerPublic
+							"cert-manager.io/cluster-issuer": input.network.certificateIssuer
 						}
 						hosts: [{
-							host: "hydra.\(global.domain)"
+							host: "hydra.\(input.network.domain)"
 							paths: [{
 								path: "/"
 								pathType: "Prefix"
 							}]
 						}]
 						tls: [{
-							hosts: ["hydra.\(global.domain)"]
-							secretName: "cert-hydra.\(global.domain)"
+							hosts: ["hydra.\(input.network.domain)"]
+							secretName: "cert-hydra.\(input.network.domain)"
 						}]
 					}
 				}
@@ -393,15 +367,15 @@
 									debug: false
 									allow_credentials: true
 									allowed_origins: [
-										"https://\(global.domain)",
-										"https://*.\(global.domain)"
+										"https://\(input.network.domain)",
+										"https://*.\(input.network.domain)"
 								]
 								}
 							}
 							admin: {
 								cors: {
 									allowed_origins: [
-										"https://hydra.\(global.privateDomain)"
+										"https://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
 								]
 								}
 								tls: {
@@ -422,12 +396,12 @@
 						}
 						urls: {
 							self: {
-								public: "https://hydra.\(global.domain)"
-								issuer: "https://hydra.\(global.domain)"
+								public: "https://hydra.\(input.network.domain)"
+								issuer: "https://hydra.\(input.network.domain)"
 							}
-							consent: "https://accounts-ui.\(global.domain)/consent"
-							login: "https://accounts-ui.\(global.domain)/login"
-							logout: "https://accounts-ui.\(global.domain)/logout"
+							consent: "https://accounts-ui.\(input.network.domain)/consent"
+							login: "https://accounts-ui.\(input.network.domain)/login"
+							logout: "https://accounts-ui.\(input.network.domain)/logout"
 						}
 						secrets: {
 							system: ["youReallyNeedToChangeThis"]
@@ -451,10 +425,9 @@
 				}
 			}
 			ui: {
-				certificateIssuer: issuerPublic
-				ingressClassName: ingressPublic
-				domain: global.domain
-				internalDomain: global.privateDomain
+				certificateIssuer: input.network.certificateIssuer
+				ingressClassName: input.network.ingressClass
+				domain: input.network.domain
 				hydra: "hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
 				enableRegistration: false
 				image: {
diff --git a/core/installer/values-tmpl/env-dns.cue b/core/installer/values-tmpl/env-dns.cue
index 99941be..13cc217 100644
--- a/core/installer/values-tmpl/env-dns.cue
+++ b/core/installer/values-tmpl/env-dns.cue
@@ -154,7 +154,7 @@
 			}
 			config: "coredns.conf"
 			db: "records.db"
-			zone: global.domain
+			zone: networks.public.domain
 			publicIP: strings.Join(global.publicIP, ",")
 			privateIP: global.network.ingress
 			nameserverIP: strings.Join(global.nameserverIP, ",")
diff --git a/core/installer/values-tmpl/gerrit.cue b/core/installer/values-tmpl/gerrit.cue
index a18cc10..e7925a8 100644
--- a/core/installer/values-tmpl/gerrit.cue
+++ b/core/installer/values-tmpl/gerrit.cue
@@ -157,7 +157,7 @@
       userNameToLowerCase = true
       userNameCaseInsensitive = true
     [plugin "gerrit-oauth-provider-pcloud-oauth"]
-      root-url = https://hydra.\(global.domain)
+      root-url = https://hydra.\(networks.public.domain)
       client-id = "{{ .client_id }}"
       client-secret = "{{ .client_secret }}"
       link-to-existing-openid-accounts = true
@@ -182,7 +182,7 @@
       timeout = 120 s
     [user]
       name = Gerrit Code Review
-      email = gerrit@\(global.domain)
+      email = gerrit@\(networks.public.domain)
       anonymousCoward = Unnamed User
     [cache]
       directory = cache
diff --git a/core/installer/values-tmpl/headscale.cue b/core/installer/values-tmpl/headscale.cue
index 726acd8..6dd5609 100644
--- a/core/installer/values-tmpl/headscale.cue
+++ b/core/installer/values-tmpl/headscale.cue
@@ -1,4 +1,5 @@
 input: {
+	network: #Network
 	subdomain: string
 	ipSubnet: string
 }
@@ -37,7 +38,7 @@
 	}
 }
 
-_domain: "\(input.subdomain).\(global.domain)"
+_domain: "\(input.subdomain).\(input.network.domain)"
 _oauth2ClientSecretName: "oauth2-client"
 
 helm: {
@@ -71,14 +72,14 @@
 				pullPolicy: images.headscale.pullPolicy
 			}
 			storage: size: "5Gi"
-			ingressClassName: ingressPublic
-			certificateIssuer: issuerPublic
+			ingressClassName: input.network.ingressClass
+			certificateIssuer: input.network.certificateIssuer
 			domain: _domain
-			publicBaseDomain: global.domain
+			publicBaseDomain: input.network.domain
 			ipAddressPool: "\(global.id)-headscale"
 			oauth2: {
 				secretName: _oauth2ClientSecretName
-				issuer: "https://hydra.\(global.domain)"
+				issuer: "https://hydra.\(input.network.domain)"
 			}
 			api: {
 				port: 8585
@@ -108,10 +109,10 @@
 	contents: "After installing the client application you need to configure it to use https://\(_domain) as a login URL, so you can login to the VPN network with your dodo: account"
 	children: [{
 		title: "macOS"
-		contents: "[https://headscale.\(global.domain)/apple](https://headscale.\(global.domain)/apple)"
+		contents: "[https://headscale.\(input.network.domain)/apple](https://headscale.\(input.network.domain)/apple)"
 	}, {
 		title: "iOS"
-		contents: "[https://headscale.\(global.domain)/apple](https://headscale.\(global.domain)/apple)"
+		contents: "[https://headscale.\(input.network.domain)/apple](https://headscale.\(input.network.domain)/apple)"
 	}, {
 		title: "Windows"
 		contents: "[https://tailscale.com/kb/1318/windows-mdm](https://tailscale.com/kb/1318/windows-mdm)"
diff --git a/core/installer/values-tmpl/ingress-public.cue b/core/installer/values-tmpl/ingress-public.cue
index f0827e5..619f15a 100644
--- a/core/installer/values-tmpl/ingress-public.cue
+++ b/core/installer/values-tmpl/ingress-public.cue
@@ -44,7 +44,7 @@
 	"ingress-public": {
 		chart: charts.ingressNginx
 		values: {
-			fullnameOverride: ingressPublic
+			fullnameOverride: "\(global.pcloudEnvName)-ingress-public"
 			controller: {
 				kind: "DaemonSet"
 				hostNetwork: true
@@ -52,10 +52,10 @@
 				service: enabled: false
 				ingressClassByName: true
 				ingressClassResource: {
-					name: ingressPublic
+					name: networks.public.ingressClass
 					enabled: true
 					default: false
-					controllerValue: "k8s.io/\(ingressPublic)"
+					controllerValue: "k8s.io/\(networks.public.ingressClass)"
 				}
 				config: {
 					"proxy-body-size": "200M" // TODO(giolekva): configurable
diff --git a/core/installer/values-tmpl/jenkins.cue b/core/installer/values-tmpl/jenkins.cue
index 0202f4e..74f3ebf 100644
--- a/core/installer/values-tmpl/jenkins.cue
+++ b/core/installer/values-tmpl/jenkins.cue
@@ -17,7 +17,7 @@
 ingress: {
 	jenkins: {
 		auth: enabled: false
-		network: networks.private
+		network: input.network
 		subdomain: input.subdomain
 		service: {
 			name: "jenkins"
@@ -108,7 +108,7 @@
 oic:
   clientId: "${\(_oauth2ClientCredentials)-\(_oauth2ClientId)}"
   clientSecret: "${\(_oauth2ClientCredentials)-\(_oauth2ClientSecret)}"
-  wellKnownOpenIDConfigurationUrl: "https://hydra.\(global.domain)/.well-known/openid-configuration"
+  wellKnownOpenIDConfigurationUrl: "https://hydra.\(networks.public.domain)/.well-known/openid-configuration"
   userNameField: "email"
 """
 				}
diff --git a/core/installer/values-tmpl/launcher.cue b/core/installer/values-tmpl/launcher.cue
index 12e2246..bbd5f1a 100644
--- a/core/installer/values-tmpl/launcher.cue
+++ b/core/installer/values-tmpl/launcher.cue
@@ -3,12 +3,13 @@
 )
 
 input: {
+	network: #Network
     repoAddr: string
 	sshPrivateKey: string
 }
 
 _subdomain: "launcher"
-_domain: "\(_subdomain).\(networks.public.domain)"
+_domain: "\(_subdomain).\(input.network.domain)"
 
 name: "Launcher"
 namespace: "launcher"
@@ -21,7 +22,7 @@
 ingress: {
 	launcher: {
 		auth: enabled: true
-		network: networks.public
+		network: input.network
 		subdomain: _subdomain
 		service: {
 			name: "launcher"
@@ -60,7 +61,7 @@
             portName: _httpPortName
             repoAddr: input.repoAddr
             sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
-            logoutUrl: "https://accounts-ui.\(global.domain)/logout"
+            logoutUrl: "https://accounts-ui.\(networks.public.domain)/logout"
 			repoAddr: input.repoAddr
 			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
         }
diff --git a/core/installer/values-tmpl/matrix.cue b/core/installer/values-tmpl/matrix.cue
index 348d190..36971b3 100644
--- a/core/installer/values-tmpl/matrix.cue
+++ b/core/installer/values-tmpl/matrix.cue
@@ -71,11 +71,11 @@
 		chart: charts.matrix
 		info: "Installing Synapse server"
 		values: {
-			domain: global.domain
+			domain: input.network.domain
 			subdomain: input.subdomain
 			oauth2: {
 				secretName: "oauth2-client"
-				issuer: "https://hydra.\(global.domain)"
+				issuer: "https://hydra.\(input.network.domain)"
 			}
 			postgresql: {
 				host: "postgres"
@@ -84,8 +84,8 @@
 				user: "matrix"
 				password: "matrix"
 			}
-			certificateIssuer: issuerPublic
-			ingressClassName: ingressPublic
+			certificateIssuer: input.network.certificateIssuer
+			ingressClassName: input.network.ingressClass
 			configMerge: {
 				configName: "config-to-merge"
 				fileName: "to-merge.yaml"
diff --git a/core/installer/values-tmpl/memberships.cue b/core/installer/values-tmpl/memberships.cue
index 9bf9b57..0f2a039 100644
--- a/core/installer/values-tmpl/memberships.cue
+++ b/core/installer/values-tmpl/memberships.cue
@@ -1,9 +1,10 @@
 input: {
-	authGroups: string
+	network: #Network @name(Network)
+	authGroups: string @name(Allowed Groups)
 }
 
 _subdomain: "memberships"
-_domain: "\(_subdomain).\(global.privateDomain)"
+_domain: "\(_subdomain).\(input.network.domain)"
 url: "https://\(_domain)"
 
 name: "Memberships"
@@ -20,7 +21,7 @@
 			enabled: true
 			groups: input.authGroups
 		}
-		network: networks.private
+		network: input.network
 		subdomain: _subdomain
 		service: {
 			name: "memberships"
diff --git a/core/installer/values-tmpl/open-project.cue b/core/installer/values-tmpl/open-project.cue
index 8c2da74..1badd08 100644
--- a/core/installer/values-tmpl/open-project.cue
+++ b/core/installer/values-tmpl/open-project.cue
@@ -92,7 +92,7 @@
 					password: "admin"
 					password_reset: false
 					name: "admin"
-					mail: "op-admin@\(global.domain)"
+					mail: "op-admin@\(networks.public.domain)"
 				}
 			}
 			persistence: {
diff --git a/core/installer/values-tmpl/penpot.cue b/core/installer/values-tmpl/penpot.cue
index cad8227..82caba7 100644
--- a/core/installer/values-tmpl/penpot.cue
+++ b/core/installer/values-tmpl/penpot.cue
@@ -144,7 +144,7 @@
 				providers: {
 					oidc: {
 						enabled: true
-						baseURI: "https://hydra.\(global.domain)"
+						baseURI: "https://hydra.\(networks.public.domain)"
 						clientID: ""
 						clientSecret: ""
 						authURI: ""
diff --git a/core/installer/values-tmpl/private-network.cue b/core/installer/values-tmpl/private-network.cue
index fe78f32..0536b50 100644
--- a/core/installer/values-tmpl/private-network.cue
+++ b/core/installer/values-tmpl/private-network.cue
@@ -57,6 +57,8 @@
 	}
 }
 
+_ingressPrivate: "\(global.id)-ingress-private"
+
 helm: {
 	"ingress-nginx": {
 		chart: charts["ingress-nginx"]
@@ -67,15 +69,15 @@
 					enabled: true
 					type: "LoadBalancer"
 					annotations: {
-						"metallb.universe.tf/address-pool": ingressPrivate
+						"metallb.universe.tf/address-pool": _ingressPrivate
 					}
 				}
 				ingressClassByName: true
 				ingressClassResource: {
-					name: ingressPrivate
+					name: _ingressPrivate
 					enabled: true
 					default: false
-					controllerValue: "k8s.io/\(ingressPrivate)"
+					controllerValue: "k8s.io/\(_ingressPrivate)"
 				}
 				config: {
 					"proxy-body-size": "200M" // TODO(giolekva): configurable
@@ -85,7 +87,7 @@
 					"""
 				}
 				extraArgs: {
-					"default-ssl-certificate": "\(ingressPrivate)/cert-wildcard.\(global.privateDomain)"
+					"default-ssl-certificate": "\(_ingressPrivate)/cert-wildcard.\(global.privateDomain)"
 				}
 				admissionWebhooks: {
 					enabled: false
@@ -104,7 +106,7 @@
 		values: {
 			hostname: input.privateNetwork.hostname
 			apiServer: "http://headscale-api.\(global.namespacePrefix)app-headscale.svc.cluster.local"
-			loginServer: "https://headscale.\(global.domain)" // TODO(gio): take headscale subdomain from configuration
+			loginServer: "https://headscale.\(networks.public.domain)" // TODO(gio): take headscale subdomain from configuration
 			ipSubnet: input.privateNetwork.ipSubnet
 			username: input.privateNetwork.username // TODO(gio): maybe install headscale-user chart separately?
 			preAuthKeySecret: "headscale-preauth-key"
diff --git a/core/installer/values-tmpl/welcome.cue b/core/installer/values-tmpl/welcome.cue
index 2abd8b2..55f4e14 100644
--- a/core/installer/values-tmpl/welcome.cue
+++ b/core/installer/values-tmpl/welcome.cue
@@ -3,6 +3,7 @@
 )
 
 input: {
+	network: #Network
 	repoAddr: string
 	sshPrivateKey: string
 }
@@ -35,12 +36,12 @@
 			repoAddr: input.repoAddr
 			sshPrivateKey: base64.Encode(null, input.sshPrivateKey)
 			createAccountAddr: "http://api.\(global.namespacePrefix)core-auth.svc.cluster.local/identities"
-			loginAddr: "https://launcher.\(global.domain)"
+			loginAddr: "https://launcher.\(networks.public.domain)"
 			membershipsInitAddr: "http://memberships-api.\(global.namespacePrefix)core-auth-memberships.svc.cluster.local/api/init"
 			ingress: {
-				className: ingressPublic
-				domain: "welcome.\(global.domain)"
-				certificateIssuer: issuerPublic
+				className: input.network.ingressClass
+				domain: "welcome.\(input.network.domain)"
+				certificateIssuer: input.network.certificateIssuer
 			}
 			clusterRoleName: "\(global.id)-welcome"
 			image: {
diff --git a/core/installer/welcome/env-manager-tmpl/form.html b/core/installer/welcome/env-manager-tmpl/form.html
index c0f0d4a..297a426 100644
--- a/core/installer/welcome/env-manager-tmpl/form.html
+++ b/core/installer/welcome/env-manager-tmpl/form.html
@@ -11,7 +11,7 @@
 	<div style="border-width: 1px; border-right-style: solid;">
 		As part of provisioning new dodo instance you will have to update DNS records at your domain registrar, so that it points to the nameservers running on your newly created dodo. Please first get familiar with your domain registrar documentation, and only then proceed with provisioning.
 		<label for="accept" style="padding-top: 1rem;">
-			<input type="checkbox" name="accept" id="accept" form="create-form" required tabindex="5">
+			<input type="checkbox" name="accept" id="accept" form="create-form" required tabindex="6">
 			<strong>I understand</strong>
 		</label>
 	</div>
@@ -28,6 +28,16 @@
 					tabindex="1"
 				/>
 			</label>
+			<label for="private-network">
+				private network subdomain (optional)
+				<input
+					type="text"
+					id="private-network-subdomain"
+					name="private-network-subdomain"
+					placeholder="configure to create private network"
+					tabindex="2"
+				/>
+			</label>
 			<label for="contact-email">
 				contact email
 				<input
@@ -35,7 +45,7 @@
 					id="contact-email"
 					name="contact-email"
 					required
-					tabindex="2"
+					tabindex="3"
 				/>
 			</label>
 			<label for="admin-public-key">
@@ -45,7 +55,7 @@
 					id="admin-public-key"
 					name="admin-public-key"
 					required
-					tabindex="3"
+					tabindex="4"
 				/> <!-- TODO(gio): remove-->
 			</label>
 			<label for="secret-token">
@@ -54,7 +64,7 @@
 					id="secret-token"
 					name="secret-token"
 					required
-					tabindex="4"
+					tabindex="5"
 				></textarea>
 			</label>
 			<button type="submit" tabindex="6">provision</button>
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 949cbe0..4c084cd 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -239,11 +239,12 @@
 }
 
 type createEnvReq struct {
-	Name           string
-	ContactEmail   string `json:"contactEmail"`
-	Domain         string `json:"domain"`
-	AdminPublicKey string `json:"adminPublicKey"`
-	SecretToken    string `json:"secretToken"`
+	Name                    string
+	ContactEmail            string `json:"contactEmail"`
+	Domain                  string `json:"domain"`
+	PrivateNetworkSubdomain string `json:"privateNetworkSubdomain"`
+	AdminPublicKey          string `json:"adminPublicKey"`
+	SecretToken             string `json:"secretToken"`
 }
 
 func (s *EnvServer) readInvitations() ([]invitation, error) {
@@ -295,6 +296,9 @@
 		if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
 			return err
 		}
+		if req.PrivateNetworkSubdomain, err = getFormValue(r.PostForm, "private-network-subdomain"); err != nil {
+			return err
+		}
 		if req.ContactEmail, err = getFormValue(r.PostForm, "contact-email"); err != nil {
 			return err
 		}
@@ -385,11 +389,15 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	privateDomain := ""
+	if req.PrivateNetworkSubdomain != "" {
+		privateDomain = fmt.Sprintf("%s.%s", req.PrivateNetworkSubdomain, req.Domain)
+	}
 	env := installer.EnvConfig{
 		Id:              req.Name,
 		InfraName:       infra.Name,
 		Domain:          req.Domain,
-		PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
+		PrivateDomain:   privateDomain,
 		ContactEmail:    req.ContactEmail,
 		AdminPublicKey:  req.AdminPublicKey,
 		PublicIP:        infra.PublicIP,
diff --git a/core/installer/welcome/env_test.go b/core/installer/welcome/env_test.go
index 4acc576..1c18470 100644
--- a/core/installer/welcome/env_test.go
+++ b/core/installer/welcome/env_test.go
@@ -297,11 +297,12 @@
 	go s.Start()
 	time.Sleep(1 * time.Second) // Let server start
 	req := createEnvReq{
-		Name:           "test",
-		ContactEmail:   "test@test.t",
-		Domain:         "test.t",
-		AdminPublicKey: "test",
-		SecretToken:    "test",
+		Name:                    "test",
+		ContactEmail:            "test@test.t",
+		Domain:                  "test.t",
+		PrivateNetworkSubdomain: "p",
+		AdminPublicKey:          "test",
+		SecretToken:             "test",
 	}
 	var buf bytes.Buffer
 	if err := json.NewEncoder(&buf).Encode(req); err != nil {