diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go
index 230ed58269..e472384a95 100644
--- a/contrib/environment-to-ini/environment-to-ini.go
+++ b/contrib/environment-to-ini/environment-to-ini.go
@@ -5,7 +5,6 @@ package main
 
 import (
 	"os"
-	"strings"
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
@@ -13,9 +12,6 @@ import (
 	"github.com/urfave/cli"
 )
 
-// EnvironmentPrefix environment variables prefixed with this represent ini values to write
-const EnvironmentPrefix = "GITEA"
-
 func main() {
 	app := cli.NewApp()
 	app.Name = "environment-to-ini"
@@ -70,15 +66,6 @@ func main() {
 			Value: "",
 			Usage: "Destination file to write to",
 		},
-		cli.BoolFlag{
-			Name:  "clear",
-			Usage: "Clears the matched variables from the environment",
-		},
-		cli.StringFlag{
-			Name:  "prefix, p",
-			Value: EnvironmentPrefix,
-			Usage: "Environment prefix to look for - will be suffixed by __ (2 underscores)",
-		},
 	}
 	app.Action = runEnvironmentToIni
 	err := app.Run(os.Args)
@@ -99,9 +86,7 @@ func runEnvironmentToIni(c *cli.Context) error {
 		log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
 	}
 
-	prefixGitea := c.String("prefix") + "__"
-	suffixFile := "__FILE"
-	changed := setting.EnvironmentToConfig(cfg, prefixGitea, suffixFile, os.Environ())
+	changed := setting.EnvironmentToConfig(cfg, os.Environ())
 
 	// try to save the config file
 	destination := c.String("out")
@@ -116,19 +101,5 @@ func runEnvironmentToIni(c *cli.Context) error {
 		}
 	}
 
-	// clear Gitea's specific environment variables if requested
-	if c.Bool("clear") {
-		for _, kv := range os.Environ() {
-			idx := strings.IndexByte(kv, '=')
-			if idx < 0 {
-				continue
-			}
-			eKey := kv[:idx]
-			if strings.HasPrefix(eKey, prefixGitea) {
-				_ = os.Unsetenv(eKey)
-			}
-		}
-	}
-
 	return nil
 }
diff --git a/docs/content/doc/installation/with-docker-rootless.en-us.md b/docs/content/doc/installation/with-docker-rootless.en-us.md
index b8b40fcbd7..5aa4e46e12 100644
--- a/docs/content/doc/installation/with-docker-rootless.en-us.md
+++ b/docs/content/doc/installation/with-docker-rootless.en-us.md
@@ -288,7 +288,7 @@ docker-compose up -d
 
 In addition to the environment variables above, any settings in `app.ini` can be set
 or overridden with an environment variable of the form: `GITEA__SECTION_NAME__KEY_NAME`.
-These settings are applied each time the docker container starts.
+These settings are applied each time the docker container starts, and won't be passed into Gitea's sub-processes.
 Full information [here](https://github.com/go-gitea/gitea/tree/main/contrib/environment-to-ini).
 
 These environment variables can be passed to the docker container in `docker-compose.yml`.
diff --git a/docs/content/doc/installation/with-docker.en-us.md b/docs/content/doc/installation/with-docker.en-us.md
index e70a6ab133..a7a575293d 100644
--- a/docs/content/doc/installation/with-docker.en-us.md
+++ b/docs/content/doc/installation/with-docker.en-us.md
@@ -289,7 +289,7 @@ docker-compose up -d
 
 In addition to the environment variables above, any settings in `app.ini` can be set
 or overridden with an environment variable of the form: `GITEA__SECTION_NAME__KEY_NAME`.
-These settings are applied each time the docker container starts.
+These settings are applied each time the docker container starts, and won't be passed into Gitea's sub-processes.
 Full information [here](https://github.com/go-gitea/gitea/tree/master/contrib/environment-to-ini).
 
 These environment variables can be passed to the docker container in `docker-compose.yml`.
diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go
index d032160a6f..e18a13e4aa 100644
--- a/modules/assetfs/layered.go
+++ b/modules/assetfs/layered.go
@@ -215,6 +215,7 @@ func (l *LayeredFS) WatchLocalChanges(ctx context.Context, callback func()) {
 			log.Error("Unable to list directories for asset local file-system %q: %v", layer.localPath, err)
 			continue
 		}
+		layerDirs = append(layerDirs, ".")
 		for _, dir := range layerDirs {
 			if err = watcher.Add(util.FilePathJoinAbs(layer.localPath, dir)); err != nil {
 				log.Error("Unable to watch directory %s: %v", dir, err)
diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go
index 6348803705..e23b64557f 100644
--- a/modules/setting/config_env.go
+++ b/modules/setting/config_env.go
@@ -12,10 +12,31 @@ import (
 	"code.gitea.io/gitea/modules/log"
 )
 
+const (
+	EnvConfigKeyPrefixGitea = "GITEA__"
+	EnvConfigKeySuffixFile  = "__FILE"
+)
+
 const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
 
 var escapeRegex = regexp.MustCompile(escapeRegexpString)
 
+func CollectEnvConfigKeys() (keys []string) {
+	for _, env := range os.Environ() {
+		if strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
+			k, _, _ := strings.Cut(env, "=")
+			keys = append(keys, k)
+		}
+	}
+	return keys
+}
+
+func ClearEnvConfigKeys() {
+	for _, k := range CollectEnvConfigKeys() {
+		_ = os.Unsetenv(k)
+	}
+}
+
 // decodeEnvSectionKey will decode a portable string encoded Section__Key pair
 // Portable strings are considered to be of the form [A-Z0-9_]*
 // We will encode a disallowed value as the UTF8 byte string preceded by _0X and
@@ -87,7 +108,7 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect
 	return ok, section, key, useFileValue
 }
 
-func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, envs []string) (changed bool) {
+func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
 	for _, kv := range envs {
 		idx := strings.IndexByte(kv, '=')
 		if idx < 0 {
@@ -97,7 +118,7 @@ func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, env
 		// parse the environment variable to config section name and key name
 		envKey := kv[:idx]
 		envValue := kv[idx+1:]
-		ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(prefixGitea, suffixFile, envKey)
+		ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
 		if !ok {
 			continue
 		}
diff --git a/modules/setting/config_env_test.go b/modules/setting/config_env_test.go
index d574554bcc..2c1dd2f5c7 100644
--- a/modules/setting/config_env_test.go
+++ b/modules/setting/config_env_test.go
@@ -72,7 +72,7 @@ func TestDecodeEnvironmentKey(t *testing.T) {
 func TestEnvironmentToConfig(t *testing.T) {
 	cfg, _ := NewConfigProviderFromData("")
 
-	changed := EnvironmentToConfig(cfg, "GITEA__", "__FILE", nil)
+	changed := EnvironmentToConfig(cfg, nil)
 	assert.False(t, changed)
 
 	cfg, err := NewConfigProviderFromData(`
@@ -81,16 +81,16 @@ key = old
 `)
 	assert.NoError(t, err)
 
-	changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"})
+	changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key=new"})
 	assert.True(t, changed)
 	assert.Equal(t, "new", cfg.Section("sec").Key("key").String())
 
-	changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"})
+	changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key=new"})
 	assert.False(t, changed)
 
 	tmpFile := t.TempDir() + "/the-file"
 	_ = os.WriteFile(tmpFile, []byte("value-from-file"), 0o644)
-	changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key__FILE=" + tmpFile})
+	changed = EnvironmentToConfig(cfg, []string{"GITEA__sec__key__FILE=" + tmpFile})
 	assert.True(t, changed)
 	assert.Equal(t, "value-from-file", cfg.Section("sec").Key("key").String())
 }
diff --git a/modules/setting/path.go b/modules/setting/path.go
index 163f1d1590..32ed8d81fa 100644
--- a/modules/setting/path.go
+++ b/modules/setting/path.go
@@ -171,6 +171,9 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP
 
 	// only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready
 	InitCfgProvider(tmpCustomConf.Value)
+	if HasInstallLock(CfgProvider) {
+		ClearEnvConfigKeys() // if the instance has been installed, do not pass the environment variables to sub-processes
+	}
 	configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH")
 	if configWorkPath != "" {
 		if !filepath.IsAbs(configWorkPath) {
diff --git a/modules/setting/security.go b/modules/setting/security.go
index c39eb7f3eb..872f9b53c5 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -102,7 +102,7 @@ func generateSaveInternalToken(rootCfg ConfigProvider) {
 
 func loadSecurityFrom(rootCfg ConfigProvider) {
 	sec := rootCfg.Section("security")
-	InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
+	InstallLock = HasInstallLock(rootCfg)
 	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
 	CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
 	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 0d69847dbe..d444d9a017 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -183,10 +183,14 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
 	}
 }
 
+// HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet.
+func HasInstallLock(rootCfg ConfigProvider) bool {
+	return rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
+}
+
 func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
 	// Does not check run user when the "InstallLock" is off.
-	installLock := rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
-	if installLock {
+	if HasInstallLock(rootCfg) {
 		currentUser, match := IsRunUserMatchCurrentUser(RunUser)
 		if !match {
 			log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1a9f146e02..8aee3bebbc 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -290,6 +290,8 @@ invalid_password_algorithm = Invalid password hash algorithm
 password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. The argon2 algorithm is rather secure but uses a lot of memory and may be inappropriate for small systems.
 enable_update_checker = Enable Update Checker
 enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io.
+env_config_keys = Environment Configuration
+env_config_keys_prompt = The following environment variables will also be applied to your configuration file:
 
 [home]
 uname_holder = Username or Email Address
diff --git a/routers/install/install.go b/routers/install/install.go
index f121f31376..a2e89d3dac 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -56,6 +56,7 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
 func Contexter() func(next http.Handler) http.Handler {
 	rnd := templates.HTMLRenderer()
 	dbTypeNames := getSupportedDbTypeNames()
+	envConfigKeys := setting.CollectEnvConfigKeys()
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
 			base, baseCleanUp := context.NewBaseContext(resp, req)
@@ -70,11 +71,13 @@ func Contexter() func(next http.Handler) http.Handler {
 			ctx.AppendContextValue(context.WebContextKey, ctx)
 			ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
 			ctx.Data.MergeFrom(middleware.ContextData{
-				"locale":        ctx.Locale,
-				"Title":         ctx.Locale.Tr("install.install"),
-				"PageIsInstall": true,
-				"DbTypeNames":   dbTypeNames,
-				"AllLangs":      translation.AllLangs(),
+				"locale":         ctx.Locale,
+				"Title":          ctx.Locale.Tr("install.install"),
+				"PageIsInstall":  true,
+				"DbTypeNames":    dbTypeNames,
+				"EnvConfigKeys":  envConfigKeys,
+				"CustomConfFile": setting.CustomConf,
+				"AllLangs":       translation.AllLangs(),
 
 				"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
 			})
@@ -218,7 +221,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
 			return false
 		}
 
-		log.Info("User confirmed reinstallation of Gitea into a pre-existing database")
+		log.Info("User confirmed re-installation of Gitea into a pre-existing database")
 	}
 
 	if hasPostInstallationUser || dbMigrationVersion > 0 {
@@ -502,6 +505,8 @@ func SubmitInstall(ctx *context.Context) {
 		return
 	}
 
+	setting.EnvironmentToConfig(cfg, os.Environ())
+
 	if err = cfg.SaveTo(setting.CustomConf); err != nil {
 		ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
 		return
@@ -568,6 +573,7 @@ func SubmitInstall(ctx *context.Context) {
 		}
 	}
 
+	setting.ClearEnvConfigKeys()
 	log.Info("First-time run install finished!")
 	InstallDone(ctx)
 
diff --git a/templates/install.tmpl b/templates/install.tmpl
index 0ef1cd88b9..24533e1a5c 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -1,6 +1,6 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content install">
-	<div class="ui middle very relaxed page grid">
+	<div class="ui grid install-config-container">
 		<div class="sixteen wide center aligned centered column">
 			<h3 class="ui top attached header">
 				{{.locale.Tr "install.title"}}
@@ -149,7 +149,7 @@
 					</div>
 					<div class="inline field">
 						<div class="ui checkbox">
-							<label for="enable_update_checker">{{.locale.Tr "install.enable_update_checker"}}</label>
+							<label>{{.locale.Tr "install.enable_update_checker"}}</label>
 							<input name="enable_update_checker" type="checkbox">
 						</div>
 						<span class="help">{{.locale.Tr "install.enable_update_checker_helper"}}</span>
@@ -161,7 +161,7 @@
 
 					<!-- Email -->
 					<details class="optional field">
-						<summary class="title gt-py-3{{if .Err_SMTP}} text red{{end}}">
+						<summary class="right-content gt-py-3{{if .Err_SMTP}} text red{{end}}">
 							{{.locale.Tr "install.email_title"}}
 						</summary>
 						<div class="inline field">
@@ -201,7 +201,7 @@
 
 					<!-- Server and other services -->
 					<details class="optional field">
-						<summary class="title gt-py-3{{if .Err_Services}} text red{{end}}">
+						<summary class="right-content gt-py-3{{if .Err_Services}} text red{{end}}">
 							{{.locale.Tr "install.server_service_title"}}
 						</summary>
 						<div class="inline field">
@@ -299,7 +299,7 @@
 
 					<!-- Admin -->
 					<details class="optional field">
-						<summary class="title gt-py-3{{if .Err_Admin}} text red{{end}}">
+						<summary class="right-content gt-py-3{{if .Err_Admin}} text red{{end}}">
 							{{.locale.Tr "install.admin_title"}}
 						</summary>
 						<p class="center">{{.locale.Tr "install.admin_setting_desc"}}</p>
@@ -321,10 +321,30 @@
 						</div>
 					</details>
 
-					<div class="ui divider"></div>
+					{{if .EnvConfigKeys}}
+					<!-- Environment Config -->
+					<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
 					<div class="inline field">
 						<label></label>
 						<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
+						<div class="right-content">
+							{{.locale.Tr "install.env_config_keys_prompt"}}
+						</div>
+						<div class="right-content gt-mt-3">
+							{{range .EnvConfigKeys}}<span class="ui label">{{.}}</span>{{end}}
+						</div>
+					</div>
+					{{end}}
+
+					<div class="ui divider"></div>
+
+					<div class="inline field">
+						<div class="right-content">
+							These configuration options will be written into: {{.CustomConfFile}}
+						</div>
+						<div class="right-content gt-mt-3">
+							<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
+						</div>
 					</div>
 				</form>
 			</div>
diff --git a/web_src/css/install.css b/web_src/css/install.css
index a65a213fd5..4ac294e902 100644
--- a/web_src/css/install.css
+++ b/web_src/css/install.css
@@ -1,5 +1,6 @@
-.page-content.install {
-  padding-top: 45px;
+.page-content.install .install-config-container {
+  max-width: 900px;
+  margin: auto;
 }
 
 .page-content.install form.ui.form .inline.field > label {
@@ -9,26 +10,20 @@
   margin-right: 0;
 }
 
-.page-content.install form.ui.form .inline.field > .ui.checkbox:first-child {
+.page-content.install .ui.form .field > .help,
+.page-content.install .ui.form .field > .ui.checkbox:first-child,
+.page-content.install .ui.form .field > .right-content {
   margin-left: 30%;
   padding-left: 5px;
-}
-
-.page-content.install form.ui.form .inline.field > .ui.checkbox:first-child label {
   width: auto;
 }
 
-.page-content.install form.ui.form .title {
-  margin-left: 30%;
-  padding-left: 5px;
-}
-
 .page-content.install form.ui.form input {
   width: 60%;
 }
 
 .page-content.install form.ui.form details.optional.field[open] {
-  border-bottom: 1px solid var(--color-secondary);
+  border-bottom: 1px dashed var(--color-secondary);
   padding-bottom: 10px;
 }
 
@@ -44,12 +39,6 @@
   text-align: left;
 }
 
-.page-content.install form.ui.form .field .help {
-  margin-left: 30%;
-  padding-left: 5px;
-  width: 60%;
-}
-
 .page-content.install .ui .reinstall-message {
   width: 70%;
   margin: 20px auto;