diff --git a/.gitignore b/.gitignore
index 804fe87c04..2760a0ad58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,8 +53,6 @@ cpu.out
 /bin
 /dist
 /custom/*
-!/custom/conf
-/custom/conf/*
 !/custom/conf/app.example.ini
 /data
 /indexers
diff --git a/cmd/actions.go b/cmd/actions.go
index 346de5b21a..f52a91bd55 100644
--- a/cmd/actions.go
+++ b/cmd/actions.go
@@ -42,7 +42,7 @@ func runGenerateActionsRunnerToken(c *cli.Context) error {
 	ctx, cancel := installSignals()
 	defer cancel()
 
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 
 	scope := c.String("scope")
 
diff --git a/cmd/cmd.go b/cmd/cmd.go
index b148007fbe..8076acecaa 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -58,7 +58,7 @@ func confirm() (bool, error) {
 }
 
 func initDB(ctx context.Context) error {
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 	setting.LoadDBSetting()
 	setting.InitSQLLoggersForCli(log.INFO)
 
diff --git a/cmd/doctor.go b/cmd/doctor.go
index b596e9ac0c..b79436fc0a 100644
--- a/cmd/doctor.go
+++ b/cmd/doctor.go
@@ -91,7 +91,7 @@ func runRecreateTable(ctx *cli.Context) error {
 	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
 
 	debug := ctx.Bool("debug")
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 	setting.LoadDBSetting()
 
 	if debug {
diff --git a/cmd/dump.go b/cmd/dump.go
index 7dda7fd2b3..0b7c1d32c5 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -182,7 +182,7 @@ func runDump(ctx *cli.Context) error {
 		}
 		fileName += "." + outType
 	}
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 
 	// make sure we are logging to the console no matter what the configuration tells us do to
 	// FIXME: don't use CfgProvider directly
diff --git a/cmd/embedded.go b/cmd/embedded.go
index e51f8477b4..204a623cf7 100644
--- a/cmd/embedded.go
+++ b/cmd/embedded.go
@@ -99,11 +99,6 @@ type assetFile struct {
 func initEmbeddedExtractor(c *cli.Context) error {
 	setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
 
-	// Read configuration file
-	setting.Init(&setting.Options{
-		AllowEmpty: true,
-	})
-
 	patterns, err := compileCollectPatterns(c.Args())
 	if err != nil {
 		return err
diff --git a/cmd/mailer.go b/cmd/mailer.go
index 74bae1ab68..eaa5a1afe1 100644
--- a/cmd/mailer.go
+++ b/cmd/mailer.go
@@ -16,7 +16,7 @@ func runSendMail(c *cli.Context) error {
 	ctx, cancel := installSignals()
 	defer cancel()
 
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 
 	if err := argsSet(c, "title"); err != nil {
 		return err
diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go
index 5a7ede4939..c19e28f13d 100644
--- a/cmd/restore_repo.go
+++ b/cmd/restore_repo.go
@@ -51,7 +51,7 @@ func runRestoreRepository(c *cli.Context) error {
 	ctx, cancel := installSignals()
 	defer cancel()
 
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 	var units []string
 	if s := c.String("units"); s != "" {
 		units = strings.Split(s, ",")
diff --git a/cmd/serv.go b/cmd/serv.go
index 87bf1cce20..01102d3800 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -61,7 +61,7 @@ func setup(ctx context.Context, debug bool) {
 	} else {
 		setupConsoleLogger(log.FATAL, false, os.Stderr)
 	}
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 	if debug {
 		setting.RunMode = "dev"
 	}
diff --git a/cmd/web.go b/cmd/web.go
index 5444c80008..8eb08120c7 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -101,6 +101,110 @@ func createPIDFile(pidPath string) {
 	}
 }
 
+func serveInstall(ctx *cli.Context) error {
+	log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith)
+	log.Info("App path: %s", setting.AppPath)
+	log.Info("Work path: %s", setting.AppWorkPath)
+	log.Info("Custom path: %s", setting.CustomPath)
+	log.Info("Config file: %s", setting.CustomConf)
+	log.Info("Prepare to run install page")
+
+	routers.InitWebInstallPage(graceful.GetManager().HammerContext())
+
+	// Flag for port number in case first time run conflict
+	if ctx.IsSet("port") {
+		if err := setPort(ctx.String("port")); err != nil {
+			return err
+		}
+	}
+	if ctx.IsSet("install-port") {
+		if err := setPort(ctx.String("install-port")); err != nil {
+			return err
+		}
+	}
+	c := install.Routes()
+	err := listen(c, false)
+	if err != nil {
+		log.Critical("Unable to open listener for installer. Is Gitea already running?")
+		graceful.GetManager().DoGracefulShutdown()
+	}
+	select {
+	case <-graceful.GetManager().IsShutdown():
+		<-graceful.GetManager().Done()
+		log.Info("PID: %d Gitea Web Finished", os.Getpid())
+		log.GetManager().Close()
+		return err
+	default:
+	}
+	return nil
+}
+
+func serveInstalled(ctx *cli.Context) error {
+	setting.InitCfgProvider(setting.CustomConf)
+	setting.LoadCommonSettings()
+	setting.MustInstalled()
+
+	log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith)
+	log.Info("App path: %s", setting.AppPath)
+	log.Info("Work path: %s", setting.AppWorkPath)
+	log.Info("Custom path: %s", setting.CustomPath)
+	log.Info("Config file: %s", setting.CustomConf)
+	log.Info("Run mode: %s", setting.RunMode)
+	log.Info("Prepare to run web server")
+
+	if setting.AppWorkPathMismatch {
+		log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+
+			"Only WORK_PATH in config should be set and used. Please remove the other outdated work paths from environment variables and command arguments", setting.CustomConf)
+	}
+
+	rootCfg := setting.CfgProvider
+	if rootCfg.Section("").Key("WORK_PATH").String() == "" {
+		saveCfg, err := rootCfg.PrepareSaving()
+		if err != nil {
+			log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err)
+		} else {
+			rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
+			saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
+			if err = saveCfg.Save(); err != nil {
+				log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err)
+			}
+		}
+	}
+
+	routers.InitWebInstalled(graceful.GetManager().HammerContext())
+
+	// We check that AppDataPath exists here (it should have been created during installation)
+	// We can't check it in `InitWebInstalled`, because some integration tests
+	// use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests.
+	if _, err := os.Stat(setting.AppDataPath); err != nil {
+		log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
+	}
+
+	// Override the provided port number within the configuration
+	if ctx.IsSet("port") {
+		if err := setPort(ctx.String("port")); err != nil {
+			return err
+		}
+	}
+
+	// Set up Chi routes
+	c := routers.NormalRoutes(graceful.GetManager().HammerContext())
+	err := listen(c, true)
+	<-graceful.GetManager().Done()
+	log.Info("PID: %d Gitea Web Finished", os.Getpid())
+	log.GetManager().Close()
+	return err
+}
+
+func servePprof() {
+	http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
+	_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
+	// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
+	log.Info("Starting pprof server on localhost:6060")
+	log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
+	finished()
+}
+
 func runWeb(ctx *cli.Context) error {
 	if ctx.Bool("verbose") {
 		setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout)
@@ -128,75 +232,19 @@ func runWeb(ctx *cli.Context) error {
 		createPIDFile(ctx.String("pid"))
 	}
 
-	// Perform pre-initialization
-	needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext())
-	if needsInstall {
-		// Flag for port number in case first time run conflict
-		if ctx.IsSet("port") {
-			if err := setPort(ctx.String("port")); err != nil {
-				return err
-			}
-		}
-		if ctx.IsSet("install-port") {
-			if err := setPort(ctx.String("install-port")); err != nil {
-				return err
-			}
-		}
-		c := install.Routes()
-		err := listen(c, false)
-		if err != nil {
-			log.Critical("Unable to open listener for installer. Is Gitea already running?")
-			graceful.GetManager().DoGracefulShutdown()
-		}
-		select {
-		case <-graceful.GetManager().IsShutdown():
-			<-graceful.GetManager().Done()
-			log.Info("PID: %d Gitea Web Finished", os.Getpid())
-			log.GetManager().Close()
+	if !setting.InstallLock {
+		if err := serveInstall(ctx); err != nil {
 			return err
-		default:
 		}
 	} else {
 		NoInstallListener()
 	}
 
 	if setting.EnablePprof {
-		go func() {
-			http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
-			_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
-			// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
-			log.Info("Starting pprof server on localhost:6060")
-			log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
-			finished()
-		}()
+		go servePprof()
 	}
 
-	log.Info("Global init")
-	// Perform global initialization
-	setting.Init(&setting.Options{})
-	routers.GlobalInitInstalled(graceful.GetManager().HammerContext())
-
-	// We check that AppDataPath exists here (it should have been created during installation)
-	// We can't check it in `GlobalInitInstalled`, because some integration tests
-	// use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests.
-	if _, err := os.Stat(setting.AppDataPath); err != nil {
-		log.Fatal("Can not find APP_DATA_PATH '%s'", setting.AppDataPath)
-	}
-
-	// Override the provided port number within the configuration
-	if ctx.IsSet("port") {
-		if err := setPort(ctx.String("port")); err != nil {
-			return err
-		}
-	}
-
-	// Set up Chi routes
-	c := routers.NormalRoutes(graceful.GetManager().HammerContext())
-	err := listen(c, true)
-	<-graceful.GetManager().Done()
-	log.Info("PID: %d Gitea Web Finished", os.Getpid())
-	log.GetManager().Close()
-	return err
+	return serveInstalled(ctx)
 }
 
 func setPort(port string) error {
diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go
index 3405d7d429..2cdf4e3943 100644
--- a/contrib/environment-to-ini/environment-to-ini.go
+++ b/contrib/environment-to-ini/environment-to-ini.go
@@ -81,8 +81,6 @@ func main() {
 		},
 	}
 	app.Action = runEnvironmentToIni
-	setting.SetCustomPathAndConf("", "", "")
-
 	err := app.Run(os.Args)
 	if err != nil {
 		log.Fatal("Failed to run app with %s: %v", os.Args, err)
@@ -90,12 +88,13 @@ func main() {
 }
 
 func runEnvironmentToIni(c *cli.Context) error {
-	providedCustom := c.String("custom-path")
-	providedConf := c.String("config")
-	providedWorkPath := c.String("work-path")
-	setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath)
+	setting.InitWorkPathAndCommonConfig(os.Getenv, setting.ArgWorkPathAndCustomConf{
+		WorkPath:   c.String("work-path"),
+		CustomPath: c.String("custom-path"),
+		CustomConf: c.String("config"),
+	})
 
-	cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true})
+	cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
 	if err != nil {
 		log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
 	}
diff --git a/main.go b/main.go
index 49093eb8a7..1c87824c83 100644
--- a/main.go
+++ b/main.go
@@ -33,30 +33,58 @@ var (
 	Tags = ""
 	// MakeVersion holds the current Make version if built with make
 	MakeVersion = ""
-
-	originalAppHelpTemplate        = ""
-	originalCommandHelpTemplate    = ""
-	originalSubcommandHelpTemplate = ""
 )
 
 func init() {
 	setting.AppVer = Version
 	setting.AppBuiltWith = formatBuiltWith()
 	setting.AppStartTime = time.Now().UTC()
+}
 
-	// Grab the original help templates
-	originalAppHelpTemplate = cli.AppHelpTemplate
-	originalCommandHelpTemplate = cli.CommandHelpTemplate
-	originalSubcommandHelpTemplate = cli.SubcommandHelpTemplate
+// cmdHelp is our own help subcommand with more information
+// test cases:
+// ./gitea help
+// ./gitea -h
+// ./gitea web help
+// ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info)
+// ./gitea admin help auth
+// ./gitea -c /tmp/app.ini -h
+// ./gitea -c /tmp/app.ini help
+// ./gitea help -c /tmp/app.ini
+// GITEA_WORK_DIR=/tmp ./gitea help
+// GITEA_WORK_DIR=/tmp ./gitea help --work-path /tmp/other
+// GITEA_WORK_DIR=/tmp ./gitea help --config /tmp/app-other.ini
+var cmdHelp = cli.Command{
+	Name:      "help",
+	Aliases:   []string{"h"},
+	Usage:     "Shows a list of commands or help for one command",
+	ArgsUsage: "[command]",
+	Action: func(c *cli.Context) (err error) {
+		args := c.Args()
+		if args.Present() {
+			err = cli.ShowCommandHelp(c, args.First())
+		} else {
+			err = cli.ShowAppHelp(c)
+		}
+		_, _ = fmt.Fprintf(c.App.Writer, `
+DEFAULT CONFIGURATION:
+   AppPath:    %s
+   WorkPath:   %s
+   CustomPath: %s
+   ConfigFile: %s
+
+`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf)
+		return err
+	},
 }
 
 func main() {
 	app := cli.NewApp()
 	app.Name = "Gitea"
 	app.Usage = "A painless self-hosted Git service"
-	app.Description = `By default, gitea will start serving using the webserver with no
-arguments - which can alternatively be run by running the subcommand web.`
+	app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".`
 	app.Version = Version + formatBuiltWith()
+	app.EnableBashCompletion = true
 	app.Commands = []cli.Command{
 		cmd.CmdWeb,
 		cmd.CmdServ,
@@ -77,118 +105,83 @@ arguments - which can alternatively be run by running the subcommand web.`
 		cmd.CmdRestoreRepository,
 		cmd.CmdActions,
 	}
-	// Now adjust these commands to add our global configuration options
-
-	// First calculate the default paths and set the AppHelpTemplates in this context
-	setting.SetCustomPathAndConf("", "", "")
-	setAppHelpTemplates()
 
 	// default configuration flags
-	defaultFlags := []cli.Flag{
+	globalFlags := []cli.Flag{
+		cli.HelpFlag,
 		cli.StringFlag{
 			Name:  "custom-path, C",
-			Value: setting.CustomPath,
-			Usage: "Custom path file path",
+			Usage: "Set custom path (defaults to '{WorkPath}/custom')",
 		},
 		cli.StringFlag{
 			Name:  "config, c",
 			Value: setting.CustomConf,
-			Usage: "Custom configuration file path",
+			Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
 		},
-		cli.VersionFlag,
 		cli.StringFlag{
 			Name:  "work-path, w",
-			Value: setting.AppWorkPath,
-			Usage: "Set the gitea working path",
+			Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
 		},
 	}
 
 	// Set the default to be equivalent to cmdWeb and add the default flags
+	app.Flags = append(app.Flags, globalFlags...)
 	app.Flags = append(app.Flags, cmd.CmdWeb.Flags...)
-	app.Flags = append(app.Flags, defaultFlags...)
-	app.Action = cmd.CmdWeb.Action
-
-	// Add functions to set these paths and these flags to the commands
-	app.Before = establishCustomPath
+	app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action)
+	app.HideHelp = true // use our own help action to show helps (with more information like default config)
+	app.Commands = append(app.Commands, cmdHelp)
 	for i := range app.Commands {
-		setFlagsAndBeforeOnSubcommands(&app.Commands[i], defaultFlags, establishCustomPath)
+		prepareSubcommands(&app.Commands[i], globalFlags)
 	}
 
-	app.EnableBashCompletion = true
-
 	err := app.Run(os.Args)
 	if err != nil {
-		log.Fatal("Failed to run app with %s: %v", os.Args, err)
+		_, _ = fmt.Fprintf(app.Writer, "\nFailed to run with %s: %v\n", os.Args, err)
 	}
 
 	log.GetManager().Close()
 }
 
-func setFlagsAndBeforeOnSubcommands(command *cli.Command, defaultFlags []cli.Flag, before cli.BeforeFunc) {
+func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) {
 	command.Flags = append(command.Flags, defaultFlags...)
-	command.Before = establishCustomPath
+	command.Action = prepareWorkPathAndCustomConf(command.Action)
+	command.HideHelp = true
+	if command.Name != "help" {
+		command.Subcommands = append(command.Subcommands, cmdHelp)
+	}
 	for i := range command.Subcommands {
-		setFlagsAndBeforeOnSubcommands(&command.Subcommands[i], defaultFlags, before)
+		prepareSubcommands(&command.Subcommands[i], defaultFlags)
 	}
 }
 
-func establishCustomPath(ctx *cli.Context) error {
-	var providedCustom string
-	var providedConf string
-	var providedWorkPath string
-
-	currentCtx := ctx
-	for {
-		if len(providedCustom) != 0 && len(providedConf) != 0 && len(providedWorkPath) != 0 {
-			break
-		}
-		if currentCtx == nil {
-			break
-		}
-		if currentCtx.IsSet("custom-path") && len(providedCustom) == 0 {
-			providedCustom = currentCtx.String("custom-path")
-		}
-		if currentCtx.IsSet("config") && len(providedConf) == 0 {
-			providedConf = currentCtx.String("config")
-		}
-		if currentCtx.IsSet("work-path") && len(providedWorkPath) == 0 {
-			providedWorkPath = currentCtx.String("work-path")
-		}
-		currentCtx = currentCtx.Parent()
-
+// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
+// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
+func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error {
+	if a == nil {
+		return nil
 	}
-	setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath)
-
-	setAppHelpTemplates()
-
-	if ctx.IsSet("version") {
-		cli.ShowVersion(ctx)
-		os.Exit(0)
+	action := a.(func(*cli.Context) error)
+	return func(ctx *cli.Context) error {
+		var args setting.ArgWorkPathAndCustomConf
+		curCtx := ctx
+		for curCtx != nil {
+			if curCtx.IsSet("work-path") && args.WorkPath == "" {
+				args.WorkPath = curCtx.String("work-path")
+			}
+			if curCtx.IsSet("custom-path") && args.CustomPath == "" {
+				args.CustomPath = curCtx.String("custom-path")
+			}
+			if curCtx.IsSet("config") && args.CustomConf == "" {
+				args.CustomConf = curCtx.String("config")
+			}
+			curCtx = curCtx.Parent()
+		}
+		setting.InitWorkPathAndCommonConfig(os.Getenv, args)
+		if ctx.Bool("help") {
+			return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx)
+		}
+		return action(ctx)
 	}
-
-	return nil
-}
-
-func setAppHelpTemplates() {
-	cli.AppHelpTemplate = adjustHelpTemplate(originalAppHelpTemplate)
-	cli.CommandHelpTemplate = adjustHelpTemplate(originalCommandHelpTemplate)
-	cli.SubcommandHelpTemplate = adjustHelpTemplate(originalSubcommandHelpTemplate)
-}
-
-func adjustHelpTemplate(originalTemplate string) string {
-	overridden := ""
-	if _, ok := os.LookupEnv("GITEA_CUSTOM"); ok {
-		overridden = "(GITEA_CUSTOM)"
-	}
-
-	return fmt.Sprintf(`%s
-DEFAULT CONFIGURATION:
-     CustomPath:  %s %s
-     CustomConf:  %s
-     AppPath:     %s
-     AppWorkPath: %s
-
-`, originalTemplate, setting.CustomPath, overridden, setting.CustomConf, setting.AppPath, setting.AppWorkPath)
 }
 
 func formatBuiltWith() string {
diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go
index dd99a1eda2..c3100ba665 100644
--- a/models/migrations/base/tests.go
+++ b/models/migrations/base/tests.go
@@ -147,9 +147,9 @@ func MainTest(m *testing.M) {
 		os.Exit(1)
 	}
 
+	setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
 	setting.AppDataPath = tmpDataPath
 
-	setting.SetCustomPathAndConf("", "", "")
 	unittest.InitSettings()
 	if err = git.InitFull(context.Background()); err != nil {
 		fmt.Printf("Unable to InitFull: %v\n", err)
diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index 5351ff1139..f926a65538 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -42,12 +42,14 @@ func fatalTestError(fmtStr string, args ...interface{}) {
 	os.Exit(1)
 }
 
-// InitSettings initializes config provider and load common setttings for tests
+// InitSettings initializes config provider and load common settings for tests
 func InitSettings(extraConfigs ...string) {
-	setting.Init(&setting.Options{
-		AllowEmpty:  true,
-		ExtraConfig: strings.Join(extraConfigs, "\n"),
-	})
+	if setting.CustomConf == "" {
+		setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
+		_ = os.Remove(setting.CustomConf)
+	}
+	setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n"))
+	setting.LoadCommonSettings()
 
 	if err := setting.PrepareAppDataPath(); err != nil {
 		log.Fatalf("Can not prepare APP_DATA_PATH: %v", err)
@@ -69,7 +71,7 @@ type TestOptions struct {
 // MainTest a reusable TestMain(..) function for unit tests that need to use a
 // test database. Creates the test database, and sets necessary settings.
 func MainTest(m *testing.M, testOpts *TestOptions) {
-	setting.SetCustomPathAndConf("", "", "")
+	setting.CustomPath = filepath.Join(testOpts.GiteaRootPath, "custom")
 	InitSettings()
 
 	var err error
diff --git a/modules/doctor/doctor.go b/modules/doctor/doctor.go
index 10838a7512..ceee322852 100644
--- a/modules/doctor/doctor.go
+++ b/modules/doctor/doctor.go
@@ -28,7 +28,7 @@ type Check struct {
 }
 
 func initDBSkipLogger(ctx context.Context) error {
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 	setting.LoadDBSetting()
 	if err := db.InitEngine(ctx); err != nil {
 		return fmt.Errorf("db.InitEngine: %w", err)
diff --git a/modules/doctor/paths.go b/modules/doctor/paths.go
index 957152349c..3f62d587ab 100644
--- a/modules/doctor/paths.go
+++ b/modules/doctor/paths.go
@@ -66,7 +66,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo
 		return err
 	}
 
-	setting.Init(&setting.Options{})
+	setting.MustInstalled()
 
 	configurationFiles := []configurationFile{
 		{"Configuration File Path", setting.CustomConf, false, true, false},
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 5e5e4fecbb..a8d7ba7948 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -28,9 +29,7 @@ var localMetas = map[string]string{
 }
 
 func TestMain(m *testing.M) {
-	setting.Init(&setting.Options{
-		AllowEmpty: true,
-	})
+	unittest.InitSettings()
 	if err := git.InitSimple(context.Background()); err != nil {
 		log.Fatal("git init failed, err: %v", err)
 	}
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 4bd2ca8d41..f2322b2554 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
@@ -33,9 +34,7 @@ var localMetas = map[string]string{
 }
 
 func TestMain(m *testing.M) {
-	setting.Init(&setting.Options{
-		AllowEmpty: true,
-	})
+	unittest.InitSettings()
 	if err := git.InitSimple(context.Background()); err != nil {
 		log.Fatal("git init failed, err: %v", err)
 	}
diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index deec5cc586..94dd989850 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -55,15 +55,15 @@ type ConfigProvider interface {
 
 	DisableSaving()
 	PrepareSaving() (ConfigProvider, error)
+	IsLoadedFromEmpty() bool
 }
 
 type iniConfigProvider struct {
-	opts *Options
+	file string
 	ini  *ini.File
 
-	disableSaving bool
-
-	newFile bool // whether the file has not existed previously
+	disableSaving   bool // disable the "Save" method because the config options could be polluted
+	loadedFromEmpty bool // whether the file has not existed previously
 }
 
 type iniConfigSection struct {
@@ -182,53 +182,43 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
 	}
 	cfg.NameMapper = ini.SnackCase
 	return &iniConfigProvider{
-		ini:     cfg,
-		newFile: true,
+		ini:             cfg,
+		loadedFromEmpty: true,
 	}, nil
 }
 
-type Options struct {
-	CustomConf  string // the ini file path
-	AllowEmpty  bool   // whether not finding configuration files is allowed
-	ExtraConfig string
-
-	DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()"
-}
-
 // NewConfigProviderFromFile load configuration from file.
 // NOTE: do not print any log except error.
-func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) {
+func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
 	cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "})
-	newFile := true
+	loadedFromEmpty := true
 
-	if opts.CustomConf != "" {
-		isFile, err := util.IsFile(opts.CustomConf)
+	if file != "" {
+		isFile, err := util.IsFile(file)
 		if err != nil {
-			return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", opts.CustomConf, err)
+			return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
 		}
 		if isFile {
-			if err := cfg.Append(opts.CustomConf); err != nil {
-				return nil, fmt.Errorf("failed to load custom conf '%s': %v", opts.CustomConf, err)
+			if err = cfg.Append(file); err != nil {
+				return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
 			}
-			newFile = false
+			loadedFromEmpty = false
 		}
 	}
 
-	if newFile && !opts.AllowEmpty {
-		return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf)
-	}
-
-	if opts.ExtraConfig != "" {
-		if err := cfg.Append([]byte(opts.ExtraConfig)); err != nil {
-			return nil, fmt.Errorf("unable to append more config: %v", err)
+	if len(extraConfigs) > 0 {
+		for _, s := range extraConfigs {
+			if err := cfg.Append([]byte(s)); err != nil {
+				return nil, fmt.Errorf("unable to append more config: %v", err)
+			}
 		}
 	}
 
 	cfg.NameMapper = ini.SnackCase
 	return &iniConfigProvider{
-		opts:    opts,
-		ini:     cfg,
-		newFile: newFile,
+		file:            file,
+		ini:             cfg,
+		loadedFromEmpty: loadedFromEmpty,
 	}, nil
 }
 
@@ -266,20 +256,17 @@ func (p *iniConfigProvider) Save() error {
 	if p.disableSaving {
 		return errDisableSaving
 	}
-	filename := p.opts.CustomConf
+	filename := p.file
 	if filename == "" {
-		if !p.opts.AllowEmpty {
-			return fmt.Errorf("custom config path must not be empty")
-		}
-		return nil
+		return fmt.Errorf("config file path must not be empty")
 	}
-	if p.newFile {
+	if p.loadedFromEmpty {
 		if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
-			return fmt.Errorf("failed to create '%s': %v", filename, err)
+			return fmt.Errorf("failed to create %q: %v", filename, err)
 		}
 	}
 	if err := p.ini.SaveTo(filename); err != nil {
-		return fmt.Errorf("failed to save '%s': %v", filename, err)
+		return fmt.Errorf("failed to save %q: %v", filename, err)
 	}
 
 	// Change permissions to be more restrictive
@@ -313,11 +300,14 @@ func (p *iniConfigProvider) DisableSaving() {
 // it makes the "Save" outputs a lot of garbage options
 // After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped.
 func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) {
-	cfgFile := p.opts.CustomConf
-	if cfgFile == "" {
+	if p.file == "" {
 		return nil, errors.New("no config file to save")
 	}
-	return NewConfigProviderFromFile(p.opts)
+	return NewConfigProviderFromFile(p.file)
+}
+
+func (p *iniConfigProvider) IsLoadedFromEmpty() bool {
+	return p.loadedFromEmpty
 }
 
 func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
@@ -356,8 +346,8 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro
 	}
 	iniFile.BlockMode = false
 	return &iniConfigProvider{
-		ini:     iniFile,
-		newFile: true,
+		ini:             iniFile,
+		loadedFromEmpty: true,
 	}, nil
 }
 
diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go
index c5c5196e04..7e7c6be2bb 100644
--- a/modules/setting/config_provider_test.go
+++ b/modules/setting/config_provider_test.go
@@ -67,13 +67,14 @@ key = 123
 }
 
 func TestNewConfigProviderFromFile(t *testing.T) {
-	_, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false})
-	assert.ErrorContains(t, err, "unable to find configuration file")
+	cfg, err := NewConfigProviderFromFile("no-such.ini")
+	assert.NoError(t, err)
+	assert.True(t, cfg.IsLoadedFromEmpty())
 
 	// load non-existing file and save
 	testFile := t.TempDir() + "/test.ini"
 	testFile1 := t.TempDir() + "/test1.ini"
-	cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
+	cfg, err = NewConfigProviderFromFile(testFile)
 	assert.NoError(t, err)
 
 	sec, _ := cfg.NewSection("foo")
@@ -91,7 +92,7 @@ func TestNewConfigProviderFromFile(t *testing.T) {
 	assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs))
 
 	// load existing file and save
-	cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
+	cfg, err = NewConfigProviderFromFile(testFile)
 	assert.NoError(t, err)
 	assert.Equal(t, "a", cfg.Section("foo").Key("k1").String())
 	sec, _ = cfg.NewSection("bar")
@@ -123,7 +124,7 @@ func TestNewConfigProviderForLocale(t *testing.T) {
 func TestDisableSaving(t *testing.T) {
 	testFile := t.TempDir() + "/test.ini"
 	_ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644)
-	cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
+	cfg, err := NewConfigProviderFromFile(testFile)
 	assert.NoError(t, err)
 
 	cfg.DisableSaving()
diff --git a/modules/setting/path.go b/modules/setting/path.go
new file mode 100644
index 0000000000..91bb2e9bb7
--- /dev/null
+++ b/modules/setting/path.go
@@ -0,0 +1,191 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"errors"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+)
+
+var (
+	// AppPath represents the path to the gitea binary
+	AppPath string
+
+	// AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR.
+	// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
+	// It is used as the base path for several other paths.
+	AppWorkPath string
+	CustomPath  string // Custom directory path. Env: GITEA_CUSTOM
+	CustomConf  string
+
+	appWorkPathBuiltin string
+	customPathBuiltin  string
+	customConfBuiltin  string
+
+	AppWorkPathMismatch bool
+)
+
+func getAppPath() (string, error) {
+	var appPath string
+	var err error
+	if IsWindows && filepath.IsAbs(os.Args[0]) {
+		appPath = filepath.Clean(os.Args[0])
+	} else {
+		appPath, err = exec.LookPath(os.Args[0])
+	}
+	if err != nil {
+		if !errors.Is(err, exec.ErrDot) {
+			return "", err
+		}
+		appPath, err = filepath.Abs(os.Args[0])
+	}
+	if err != nil {
+		return "", err
+	}
+	appPath, err = filepath.Abs(appPath)
+	if err != nil {
+		return "", err
+	}
+	// Note: (legacy code) we don't use path.Dir here because it does not handle case which path starts with two "/" in Windows: "//psf/Home/..."
+	return strings.ReplaceAll(appPath, "\\", "/"), err
+}
+
+func init() {
+	var err error
+	if AppPath, err = getAppPath(); err != nil {
+		log.Fatal("Failed to get app path: %v", err)
+	}
+
+	if AppWorkPath == "" {
+		AppWorkPath = filepath.Dir(AppPath)
+	}
+
+	appWorkPathBuiltin = AppWorkPath
+	customPathBuiltin = CustomPath
+	customConfBuiltin = CustomConf
+}
+
+type ArgWorkPathAndCustomConf struct {
+	WorkPath   string
+	CustomPath string
+	CustomConf string
+}
+
+type stringWithDefault struct {
+	Value string
+	IsSet bool
+}
+
+func (s *stringWithDefault) Set(v string) {
+	s.Value = v
+	s.IsSet = true
+}
+
+// InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings,
+func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) {
+	tryAbsPath := func(paths ...string) string {
+		s := paths[len(paths)-1]
+		for i := len(paths) - 2; i >= 0; i-- {
+			if filepath.IsAbs(s) {
+				break
+			}
+			s = filepath.Join(paths[i], s)
+		}
+		return s
+	}
+
+	var err error
+	tmpWorkPath := stringWithDefault{Value: appWorkPathBuiltin}
+	if tmpWorkPath.Value == "" {
+		tmpWorkPath.Value = filepath.Dir(AppPath)
+	}
+	tmpCustomPath := stringWithDefault{Value: customPathBuiltin}
+	if tmpCustomPath.Value == "" {
+		tmpCustomPath.Value = "custom"
+	}
+	tmpCustomConf := stringWithDefault{Value: customConfBuiltin}
+	if tmpCustomConf.Value == "" {
+		tmpCustomConf.Value = "conf/app.ini"
+	}
+
+	readFromEnv := func() {
+		envWorkPath := getEnvFn("GITEA_WORK_DIR")
+		if envWorkPath != "" {
+			tmpWorkPath.Set(envWorkPath)
+			if !filepath.IsAbs(tmpWorkPath.Value) {
+				log.Fatal("GITEA_WORK_DIR (work path) must be absolute path")
+			}
+		}
+
+		envCustomPath := getEnvFn("GITEA_CUSTOM")
+		if envCustomPath != "" {
+			tmpCustomPath.Set(envCustomPath)
+			if !filepath.IsAbs(tmpCustomPath.Value) {
+				log.Fatal("GITEA_CUSTOM (custom path) must be absolute path")
+			}
+		}
+	}
+
+	readFromArgs := func() {
+		if args.WorkPath != "" {
+			tmpWorkPath.Set(args.WorkPath)
+			if !filepath.IsAbs(tmpWorkPath.Value) {
+				log.Fatal("--work-path must be absolute path")
+			}
+		}
+		if args.CustomPath != "" {
+			tmpCustomPath.Set(args.CustomPath) // if it is not abs, it will be based on work-path, it shouldn't happen
+			if !filepath.IsAbs(tmpCustomPath.Value) {
+				log.Error("--custom-path must be absolute path")
+			}
+		}
+		if args.CustomConf != "" {
+			tmpCustomConf.Set(args.CustomConf)
+			if !filepath.IsAbs(tmpCustomConf.Value) {
+				// the config path can be relative to the real current working path
+				if tmpCustomConf.Value, err = filepath.Abs(tmpCustomConf.Value); err != nil {
+					log.Fatal("Failed to get absolute path of config %q: %v", tmpCustomConf.Value, err)
+				}
+			}
+		}
+	}
+
+	readFromEnv()
+	readFromArgs()
+
+	if !tmpCustomConf.IsSet {
+		tmpCustomConf.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value, tmpCustomConf.Value))
+	}
+
+	// only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready
+	InitCfgProvider(tmpCustomConf.Value)
+	configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH")
+	if configWorkPath != "" {
+		if !filepath.IsAbs(configWorkPath) {
+			log.Fatal("WORK_PATH in %q must be absolute path", configWorkPath)
+		}
+		configWorkPath = filepath.Clean(configWorkPath)
+		if tmpWorkPath.Value != "" && (getEnvFn("GITEA_WORK_DIR") != "" || args.WorkPath != "") {
+			fi1, err1 := os.Stat(tmpWorkPath.Value)
+			fi2, err2 := os.Stat(configWorkPath)
+			if err1 != nil || err2 != nil || !os.SameFile(fi1, fi2) {
+				AppWorkPathMismatch = true
+			}
+		}
+		tmpWorkPath.Set(configWorkPath)
+	}
+
+	tmpCustomPath.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value))
+
+	AppWorkPath = tmpWorkPath.Value
+	CustomPath = tmpCustomPath.Value
+	CustomConf = tmpCustomConf.Value
+
+	LoadCommonSettings()
+}
diff --git a/modules/setting/path_test.go b/modules/setting/path_test.go
new file mode 100644
index 0000000000..fc6a2116dc
--- /dev/null
+++ b/modules/setting/path_test.go
@@ -0,0 +1,151 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type envVars map[string]string
+
+func (e envVars) Getenv(key string) string {
+	return e[key]
+}
+
+func TestInitWorkPathAndCommonConfig(t *testing.T) {
+	testInit := func(defaultWorkPath, defaultCustomPath, defaultCustomConf string) {
+		AppWorkPathMismatch = false
+		AppWorkPath = defaultWorkPath
+		appWorkPathBuiltin = defaultWorkPath
+		CustomPath = defaultCustomPath
+		customPathBuiltin = defaultCustomPath
+		CustomConf = defaultCustomConf
+		customConfBuiltin = defaultCustomConf
+	}
+
+	fp := filepath.Join
+
+	tmpDir := t.TempDir()
+	dirFoo := fp(tmpDir, "foo")
+	dirBar := fp(tmpDir, "bar")
+	dirXxx := fp(tmpDir, "xxx")
+	dirYyy := fp(tmpDir, "yyy")
+
+	t.Run("Default", func(t *testing.T) {
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, fp(dirFoo, "custom"), CustomPath)
+		assert.Equal(t, fp(dirFoo, "custom/conf/app.ini"), CustomConf)
+	})
+
+	t.Run("WorkDir(env)", func(t *testing.T) {
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirBar, AppWorkPath)
+		assert.Equal(t, fp(dirBar, "custom"), CustomPath)
+		assert.Equal(t, fp(dirBar, "custom/conf/app.ini"), CustomConf)
+	})
+
+	t.Run("WorkDir(env,arg)", func(t *testing.T) {
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirXxx})
+		assert.Equal(t, dirXxx, AppWorkPath)
+		assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
+		assert.Equal(t, fp(dirXxx, "custom/conf/app.ini"), CustomConf)
+	})
+
+	t.Run("CustomPath(env)", func(t *testing.T) {
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, fp(dirBar, "custom1"), CustomPath)
+		assert.Equal(t, fp(dirBar, "custom1/conf/app.ini"), CustomConf)
+	})
+
+	t.Run("CustomPath(env,arg)", func(t *testing.T) {
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{CustomPath: "custom2"})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, fp(dirFoo, "custom2"), CustomPath)
+		assert.Equal(t, fp(dirFoo, "custom2/conf/app.ini"), CustomConf)
+	})
+
+	t.Run("CustomConf", func(t *testing.T) {
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: "app1.ini"})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		cwd, _ := os.Getwd()
+		assert.Equal(t, fp(cwd, "app1.ini"), CustomConf)
+
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: fp(dirBar, "app1.ini")})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, fp(dirBar, "app1.ini"), CustomConf)
+	})
+
+	t.Run("CustomConfOverrideWorkPath", func(t *testing.T) {
+		iniWorkPath := fp(tmpDir, "app-workpath.ini")
+		_ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644)
+
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
+		assert.Equal(t, dirXxx, AppWorkPath)
+		assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
+		assert.Equal(t, iniWorkPath, CustomConf)
+		assert.False(t, AppWorkPathMismatch)
+
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
+		assert.Equal(t, dirXxx, AppWorkPath)
+		assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
+		assert.Equal(t, iniWorkPath, CustomConf)
+		assert.True(t, AppWorkPathMismatch)
+
+		testInit(dirFoo, "", "")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirBar, CustomConf: iniWorkPath})
+		assert.Equal(t, dirXxx, AppWorkPath)
+		assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
+		assert.Equal(t, iniWorkPath, CustomConf)
+		assert.True(t, AppWorkPathMismatch)
+	})
+
+	t.Run("Builtin", func(t *testing.T) {
+		testInit(dirFoo, dirBar, dirXxx)
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, dirBar, CustomPath)
+		assert.Equal(t, dirXxx, CustomConf)
+
+		testInit(dirFoo, "custom1", "cfg.ini")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, fp(dirFoo, "custom1"), CustomPath)
+		assert.Equal(t, fp(dirFoo, "custom1/cfg.ini"), CustomConf)
+
+		testInit(dirFoo, "custom1", "cfg.ini")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirYyy}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirYyy, AppWorkPath)
+		assert.Equal(t, fp(dirYyy, "custom1"), CustomPath)
+		assert.Equal(t, fp(dirYyy, "custom1/cfg.ini"), CustomConf)
+
+		testInit(dirFoo, "custom1", "cfg.ini")
+		InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": dirYyy}.Getenv, ArgWorkPathAndCustomConf{})
+		assert.Equal(t, dirFoo, AppWorkPath)
+		assert.Equal(t, dirYyy, CustomPath)
+		assert.Equal(t, fp(dirYyy, "cfg.ini"), CustomConf)
+
+		iniWorkPath := fp(tmpDir, "app-workpath.ini")
+		_ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644)
+		testInit(dirFoo, "custom1", "cfg.ini")
+		InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
+		assert.Equal(t, dirXxx, AppWorkPath)
+		assert.Equal(t, fp(dirXxx, "custom1"), CustomPath)
+		assert.Equal(t, iniWorkPath, CustomConf)
+	})
+}
diff --git a/modules/setting/server.go b/modules/setting/server.go
index d937faca10..7c033bcc6b 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -61,6 +61,7 @@ var (
 	AssetVersion string
 
 	// Server settings
+
 	Protocol                   Scheme
 	UseProxyProtocol           bool // `ini:"USE_PROXY_PROTOCOL"`
 	ProxyProtocolTLSBridging   bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
@@ -324,7 +325,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
 	AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data"))
 	if !filepath.IsAbs(AppDataPath) {
-		log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath)
 		AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
 	}
 
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 6eaddbe2b5..0d69847dbe 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -5,12 +5,8 @@
 package setting
 
 import (
-	"errors"
 	"fmt"
 	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
 	"runtime"
 	"strings"
 	"time"
@@ -28,19 +24,9 @@ var (
 	// AppStartTime store time gitea has started
 	AppStartTime time.Time
 
-	// AppPath represents the path to the gitea binary
-	AppPath string
-	// AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR.
-	// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
-	//
-	// AppWorkPath is used as the base path for several other paths.
-	AppWorkPath string
-
 	// Other global setting objects
 
 	CfgProvider ConfigProvider
-	CustomPath  string // Custom directory path
-	CustomConf  string
 	RunMode     string
 	RunUser     string
 	IsProd      bool
@@ -51,62 +37,6 @@ var (
 	IsInTesting = false
 )
 
-func getAppPath() (string, error) {
-	var appPath string
-	var err error
-	if IsWindows && filepath.IsAbs(os.Args[0]) {
-		appPath = filepath.Clean(os.Args[0])
-	} else {
-		appPath, err = exec.LookPath(os.Args[0])
-	}
-
-	if err != nil {
-		if !errors.Is(err, exec.ErrDot) {
-			return "", err
-		}
-		appPath, err = filepath.Abs(os.Args[0])
-	}
-	if err != nil {
-		return "", err
-	}
-	appPath, err = filepath.Abs(appPath)
-	if err != nil {
-		return "", err
-	}
-	// Note: we don't use path.Dir here because it does not handle case
-	//	which path starts with two "/" in Windows: "//psf/Home/..."
-	return strings.ReplaceAll(appPath, "\\", "/"), err
-}
-
-func getWorkPath(appPath string) string {
-	workPath := AppWorkPath
-
-	if giteaWorkPath, ok := os.LookupEnv("GITEA_WORK_DIR"); ok {
-		workPath = giteaWorkPath
-	}
-	if len(workPath) == 0 {
-		i := strings.LastIndex(appPath, "/")
-		if i == -1 {
-			workPath = appPath
-		} else {
-			workPath = appPath[:i]
-		}
-	}
-	workPath = strings.ReplaceAll(workPath, "\\", "/")
-	if !filepath.IsAbs(workPath) {
-		log.Info("Provided work path %s is not absolute - will be made absolute against the current working directory", workPath)
-
-		absPath, err := filepath.Abs(workPath)
-		if err != nil {
-			log.Error("Unable to absolute %s against the current working directory %v. Will absolute against the AppPath %s", workPath, err, appPath)
-			workPath = filepath.Join(appPath, workPath)
-		} else {
-			workPath = absPath
-		}
-	}
-	return strings.ReplaceAll(workPath, "\\", "/")
-}
-
 func init() {
 	IsWindows = runtime.GOOS == "windows"
 	if AppVer == "" {
@@ -116,12 +46,6 @@ func init() {
 	// We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically
 	// By default set this logger at Info - we'll change it later, but we need to start with something.
 	log.SetConsoleLogger(log.DEFAULT, "console", log.INFO)
-
-	var err error
-	if AppPath, err = getAppPath(); err != nil {
-		log.Fatal("Failed to get app path: %v", err)
-	}
-	AppWorkPath = getWorkPath(AppPath)
 }
 
 // IsRunUserMatchCurrentUser returns false if configured run user does not match
@@ -137,36 +61,6 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
 	return currentUser, runUser == currentUser
 }
 
-// SetCustomPathAndConf will set CustomPath and CustomConf with reference to the
-// GITEA_CUSTOM environment variable and with provided overrides before stepping
-// back to the default
-func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string) {
-	if len(providedWorkPath) != 0 {
-		AppWorkPath = filepath.ToSlash(providedWorkPath)
-	}
-	if giteaCustom, ok := os.LookupEnv("GITEA_CUSTOM"); ok {
-		CustomPath = giteaCustom
-	}
-	if len(providedCustom) != 0 {
-		CustomPath = providedCustom
-	}
-	if len(CustomPath) == 0 {
-		CustomPath = path.Join(AppWorkPath, "custom")
-	} else if !filepath.IsAbs(CustomPath) {
-		CustomPath = path.Join(AppWorkPath, CustomPath)
-	}
-
-	if len(providedConf) != 0 {
-		CustomConf = providedConf
-	}
-	if len(CustomConf) == 0 {
-		CustomConf = path.Join(CustomPath, "conf/app.ini")
-	} else if !filepath.IsAbs(CustomConf) {
-		CustomConf = path.Join(CustomPath, CustomConf)
-		log.Warn("Using 'custom' directory as relative origin for configuration file: '%s'", CustomConf)
-	}
-}
-
 // PrepareAppDataPath creates app data directory if necessary
 func PrepareAppDataPath() error {
 	// FIXME: There are too many calls to MkdirAll in old code. It is incorrect.
@@ -196,20 +90,23 @@ func PrepareAppDataPath() error {
 	return nil
 }
 
-func Init(opts *Options) {
-	if opts.CustomConf == "" {
-		opts.CustomConf = CustomConf
-	}
+func InitCfgProvider(file string, extraConfigs ...string) {
 	var err error
-	CfgProvider, err = NewConfigProviderFromFile(opts)
-	CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
-	if err != nil {
-		log.Fatal("newConfigProviderFromFile[%v]: %v", opts, err)
+	if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil {
+		log.Fatal("Unable to init config provider from %q: %v", file, err)
 	}
-	if !opts.DisableLoadCommonSettings {
-		if err := loadCommonSettingsFrom(CfgProvider); err != nil {
-			log.Fatal("loadCommonSettingsFrom[%v]: %v", opts, err)
-		}
+	CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
+}
+
+func MustInstalled() {
+	if !InstallLock {
+		log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`)
+	}
+}
+
+func LoadCommonSettings() {
+	if err := loadCommonSettingsFrom(CfgProvider); err != nil {
+		log.Fatal("Unable to load settings from config: %v", err)
 	}
 }
 
diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go
index b4275e6005..6a0cee4a29 100644
--- a/modules/testlogger/testlogger.go
+++ b/modules/testlogger/testlogger.go
@@ -90,10 +90,11 @@ func (w *testLoggerWriterCloser) Reset() {
 
 // PrintCurrentTest prints the current test to os.Stdout
 func PrintCurrentTest(t testing.TB, skip ...int) func() {
+	t.Helper()
 	start := time.Now()
 	actualSkip := 1
 	if len(skip) > 0 {
-		actualSkip = skip[0]
+		actualSkip = skip[0] + 1
 	}
 	_, filename, line, _ := runtime.Caller(actualSkip)
 
diff --git a/routers/init.go b/routers/init.go
index 5737ef3dc0..f2c509b368 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -28,7 +28,6 @@ import (
 	"code.gitea.io/gitea/modules/system"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/translation"
-	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	actions_router "code.gitea.io/gitea/routers/api/actions"
 	packages_router "code.gitea.io/gitea/routers/api/packages"
@@ -101,21 +100,16 @@ func syncAppConfForGit(ctx context.Context) error {
 	return nil
 }
 
-// GlobalInitInstalled is for global installed configuration.
-func GlobalInitInstalled(ctx context.Context) {
-	if !setting.InstallLock {
-		log.Fatal("Gitea is not installed")
-	}
+func InitWebInstallPage(ctx context.Context) {
+	translation.InitLocales(ctx)
+	setting.LoadSettingsForInstall()
+	mustInit(svg.Init)
+}
 
+// InitWebInstalled is for global installed configuration.
+func InitWebInstalled(ctx context.Context) {
 	mustInitCtx(ctx, git.InitFull)
-	log.Info("Gitea Version: %s%s", setting.AppVer, setting.AppBuiltWith)
-	log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
-	log.Info("AppPath: %s", setting.AppPath)
-	log.Info("AppWorkPath: %s", setting.AppWorkPath)
-	log.Info("Custom path: %s", setting.CustomPath)
-	log.Info("Log path: %s", setting.Log.RootPath)
-	log.Info("Configuration file: %s", setting.CustomConf)
-	log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode))
+	log.Info("Git version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
 
 	// Setup i18n
 	translation.InitLocales(ctx)
diff --git a/routers/install/install.go b/routers/install/install.go
index b76b7fdc30..f121f31376 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -32,6 +32,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/forms"
 
 	"gitea.com/go-chi/session"
@@ -368,11 +369,16 @@ func SubmitInstall(ctx *context.Context) {
 	}
 
 	// Save settings.
-	cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true})
+	cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
 	if err != nil {
 		log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
 	}
 
+	cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
+	cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
+	cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
+	cfg.Section("").Key("RUN_MODE").SetValue("prod")
+
 	cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String())
 	cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
 	cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
@@ -383,9 +389,7 @@ func SubmitInstall(ctx *context.Context) {
 	cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
 	cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
 
-	cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
 	cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
-	cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
 	cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
 	cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
 	cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
@@ -447,8 +451,6 @@ func SubmitInstall(ctx *context.Context) {
 	cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
 	cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker))
 
-	cfg.Section("").Key("RUN_MODE").SetValue("prod")
-
 	cfg.Section("session").Key("PROVIDER").SetValue("file")
 
 	cfg.Section("log").Key("MODE").MustString("console")
@@ -511,7 +513,13 @@ func SubmitInstall(ctx *context.Context) {
 	// ---- All checks are passed
 
 	// Reload settings (and re-initialize database connection)
-	reloadSettings(ctx)
+	setting.InitCfgProvider(setting.CustomConf)
+	setting.LoadCommonSettings()
+	setting.MustInstalled()
+	setting.LoadDBSetting()
+	if err := common.InitDBEngine(ctx); err != nil {
+		log.Fatal("ORM engine initialization failed: %v", err)
+	}
 
 	// Create admin account
 	if len(form.AdminName) > 0 {
diff --git a/routers/install/setting.go b/routers/install/setting.go
deleted file mode 100644
index c14843d8ee..0000000000
--- a/routers/install/setting.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package install
-
-import (
-	"context"
-
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/svg"
-	"code.gitea.io/gitea/modules/translation"
-	"code.gitea.io/gitea/routers/common"
-)
-
-// PreloadSettings preloads the configuration to check if we need to run install
-func PreloadSettings(ctx context.Context) bool {
-	setting.Init(&setting.Options{
-		AllowEmpty: true,
-	})
-	if !setting.InstallLock {
-		log.Info("AppPath: %s", setting.AppPath)
-		log.Info("AppWorkPath: %s", setting.AppWorkPath)
-		log.Info("Custom path: %s", setting.CustomPath)
-		log.Info("Log path: %s", setting.Log.RootPath)
-		log.Info("Configuration file: %s", setting.CustomConf)
-		log.Info("Prepare to run install page")
-		translation.InitLocales(ctx)
-		if setting.EnableSQLite3 {
-			log.Info("SQLite3 is supported")
-		}
-
-		setting.LoadSettingsForInstall()
-		_ = svg.Init()
-	}
-
-	return !setting.InstallLock
-}
-
-// reloadSettings reloads the existing settings and starts up the database
-func reloadSettings(ctx context.Context) {
-	setting.Init(&setting.Options{})
-	setting.LoadDBSetting()
-	if setting.InstallLock {
-		if err := common.InitDBEngine(ctx); err == nil {
-			log.Info("ORM engine initialization successful!")
-		} else {
-			log.Fatal("ORM engine initialization failed: %v", err)
-		}
-	}
-}
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index be662c22ef..2c6989a71d 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"os"
 	"strconv"
 	"strings"
 
@@ -167,20 +166,6 @@ func Config(ctx *context.Context) {
 	ctx.Data["SessionConfig"] = sessionCfg
 
 	ctx.Data["Git"] = setting.Git
-
-	type envVar struct {
-		Name, Value string
-	}
-
-	envVars := map[string]*envVar{}
-	if len(os.Getenv("GITEA_WORK_DIR")) > 0 {
-		envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")}
-	}
-	if len(os.Getenv("GITEA_CUSTOM")) > 0 {
-		envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")}
-	}
-
-	ctx.Data["EnvVars"] = envVars
 	ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate
 	ctx.Data["LogSQL"] = setting.Database.LogSQL
 
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 2850cc8d37..2ddc0c1ac6 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -46,15 +46,6 @@
 				<dd>{{.ScriptType}}</dd>
 				<dt>{{.locale.Tr "admin.config.reverse_auth_user"}}</dt>
 				<dd>{{.ReverseProxyAuthUser}}</dd>
-
-				{{if .EnvVars}}
-				<div class="ui divider"></div>
-				{{range .EnvVars}}
-				<dt>{{.Name}}</dt>
-				<dd>{{.Value}}</dd>
-				{{end}}
-				{{end}}
-
 			</dl>
 		</div>
 
diff --git a/tests/test_utils.go b/tests/test_utils.go
index 5540d92e92..bf7d1b3fbe 100644
--- a/tests/test_utils.go
+++ b/tests/test_utils.go
@@ -42,7 +42,10 @@ func InitTest(requireGitea bool) {
 	if giteaRoot == "" {
 		exitf("Environment variable $GITEA_ROOT not set")
 	}
+
+	setting.IsInTesting = true
 	setting.AppWorkPath = giteaRoot
+	setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
 	if requireGitea {
 		giteaBinary := "gitea"
 		if setting.IsWindows {
@@ -53,7 +56,6 @@ func InitTest(requireGitea bool) {
 			exitf("Could not find gitea binary at %s", setting.AppPath)
 		}
 	}
-
 	giteaConf := os.Getenv("GITEA_CONF")
 	if giteaConf == "" {
 		// By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger.
@@ -66,16 +68,12 @@ func InitTest(requireGitea bool) {
 			exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`)
 		}
 	}
-
-	setting.IsInTesting = true
-
 	if !path.IsAbs(giteaConf) {
-		setting.CustomConf = path.Join(giteaRoot, giteaConf)
+		setting.CustomConf = filepath.Join(giteaRoot, giteaConf)
 	} else {
 		setting.CustomConf = giteaConf
 	}
 
-	setting.SetCustomPathAndConf("", "", "")
 	unittest.InitSettings()
 	setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
 	_ = util.RemoveAll(repo_module.LocalCopyPath())
@@ -175,7 +173,7 @@ func InitTest(requireGitea bool) {
 		defer db.Close()
 	}
 
-	routers.GlobalInitInstalled(graceful.GetManager().HammerContext())
+	routers.InitWebInstalled(graceful.GetManager().HammerContext())
 }
 
 func PrepareTestEnv(t testing.TB, skip ...int) func() {
@@ -240,10 +238,12 @@ func PrepareTestEnv(t testing.TB, skip ...int) func() {
 }
 
 func PrintCurrentTest(t testing.TB, skip ...int) func() {
-	if len(skip) == 1 {
-		skip = []int{skip[0] + 1}
+	t.Helper()
+	actualSkip := 1
+	if len(skip) > 0 {
+		actualSkip = skip[0] + 1
 	}
-	return testlogger.PrintCurrentTest(t, skip...)
+	return testlogger.PrintCurrentTest(t, actualSkip)
 }
 
 // Printf takes a format and args and prints the string to os.Stdout