diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 1b53732b1..dc1843097 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -991,6 +991,9 @@ LEVEL = Info
 ;; Disable stars feature.
 ;DISABLE_STARS = false
 ;;
+;; Disable repository forking.
+;DISABLE_FORKS = false
+;;
 ;; The default branch name of new repositories
 ;DEFAULT_BRANCH = main
 ;;
diff --git a/modules/context/context.go b/modules/context/context.go
index 66732eaa8..a06ebfb0d 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -198,6 +198,7 @@ func Contexter() func(next http.Handler) http.Handler {
 			// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
 			ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
 			ctx.Data["DisableStars"] = setting.Repository.DisableStars
+			ctx.Data["DisableForks"] = setting.Repository.DisableForks
 			ctx.Data["EnableActions"] = setting.Actions.Enabled
 
 			ctx.Data["ManifestData"] = setting.ManifestData
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 4ab566b7f..34eff196b 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -50,6 +50,7 @@ var (
 		PrefixArchiveFiles                      bool
 		DisableMigrations                       bool
 		DisableStars                            bool `ini:"DISABLE_STARS"`
+		DisableForks                            bool
 		DefaultBranch                           string
 		AllowAdoptionOfUnadoptedRepositories    bool
 		AllowDeleteOfUnadoptedRepositories      bool
@@ -172,6 +173,7 @@ var (
 		PrefixArchiveFiles:                      true,
 		DisableMigrations:                       false,
 		DisableStars:                            false,
+		DisableForks:                            false,
 		DefaultBranch:                           "main",
 		AllowForkWithoutMaximumLimit:            true,
 
diff --git a/modules/structs/settings.go b/modules/structs/settings.go
index e48b1a493..b127b5846 100644
--- a/modules/structs/settings.go
+++ b/modules/structs/settings.go
@@ -9,6 +9,7 @@ type GeneralRepoSettings struct {
 	HTTPGitDisabled      bool `json:"http_git_disabled"`
 	MigrationsDisabled   bool `json:"migrations_disabled"`
 	StarsDisabled        bool `json:"stars_disabled"`
+	ForksDisabled        bool `json:"forks_disabled"`
 	TimeTrackingDisabled bool `json:"time_tracking_disabled"`
 	LFSDisabled          bool `json:"lfs_disabled"`
 }
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 1babccb65..38c0c01a0 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1161,8 +1161,10 @@ func Routes() *web.Route {
 				m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
 				m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
 				m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
-				m.Combo("/forks").Get(repo.ListForks).
-					Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
+				if !setting.Repository.DisableForks {
+					m.Combo("/forks").Get(repo.ListForks).
+						Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
+				}
 				m.Group("/branches", func() {
 					m.Get("", repo.ListBranches)
 					m.Get("/*", repo.GetBranch)
diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go
index 02bda1309..957b839e6 100644
--- a/routers/api/v1/settings/settings.go
+++ b/routers/api/v1/settings/settings.go
@@ -61,6 +61,7 @@ func GetGeneralRepoSettings(ctx *context.APIContext) {
 		HTTPGitDisabled:      setting.Repository.DisableHTTPGit,
 		MigrationsDisabled:   setting.Repository.DisableMigrations,
 		StarsDisabled:        setting.Repository.DisableStars,
+		ForksDisabled:        setting.Repository.DisableForks,
 		TimeTrackingDisabled: !setting.Service.EnableTimetracking,
 		LFSDisabled:          !setting.LFS.StartServer,
 	})
diff --git a/routers/web/web.go b/routers/web/web.go
index 0684b2ac8..06ad3490a 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -968,7 +968,9 @@ func registerRoutes(m *web.Route) {
 		m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
 		m.Get("/migrate", repo.Migrate)
 		m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
-		m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID)
+		if !setting.Repository.DisableForks {
+			m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID)
+		}
 		m.Get("/search", repo.SearchRepo)
 	}, reqSignIn)
 
@@ -1148,8 +1150,10 @@ func registerRoutes(m *web.Route) {
 
 	// Grouping for those endpoints that do require authentication
 	m.Group("/{username}/{reponame}", func() {
-		m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).
-			Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
+		if !setting.Repository.DisableForks {
+			m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).
+				Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
+		}
 		m.Group("/issues", func() {
 			m.Group("/new", func() {
 				m.Combo("").Get(context.RepoRef(), repo.NewIssue).
@@ -1560,9 +1564,11 @@ func registerRoutes(m *web.Route) {
 			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
 		}, repo.SetEditorconfigIfExists)
 
-		m.Group("", func() {
-			m.Get("/forks", repo.Forks)
-		}, context.RepoRef(), reqRepoCodeReader)
+		if !setting.Repository.DisableForks {
+			m.Group("", func() {
+				m.Get("/forks", repo.Forks)
+			}, context.RepoRef(), reqRepoCodeReader)
+		}
 		m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
 	}, ignSignIn, context.RepoAssignment, context.UnitTypes())
 
diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl
index c51dcaa3f..848afd305 100644
--- a/templates/explore/repo_list.tmpl
+++ b/templates/explore/repo_list.tmpl
@@ -39,7 +39,9 @@
 						{{if not $.DisableStars}}
 							<a class="text grey flex-text-inline" href="{{.Link}}/stars">{{svg "octicon-star" 16}}{{.NumStars}}</a>
 						{{end}}
-						<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
+						{{if not $.DisableForks}}
+							<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
+						{{end}}
 					</div>
 				</div>
 				{{$description := .DescriptionHTML $.Context}}
diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl
index eaf2e7a09..573163d55 100644
--- a/templates/explore/repo_search.tmpl
+++ b/templates/explore/repo_search.tmpl
@@ -29,8 +29,10 @@
 				<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
 				<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
 			{{end}}
-			<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
-			<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
+			{{if not .DisableForks}}
+				<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
+				<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
+			{{end}}
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 2a3167f98..ed377e9d1 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -62,55 +62,8 @@
 					{{if not $.DisableStars}}
 					{{template "repo/star_unstar" $}}
 					{{end}}
-					{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
-						<div class="ui labeled button
-							{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
-								disabled
-							{{end}}"
-							{{if not $.IsSigned}}
-								data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
-							{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
-								data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
-							{{end}}
-						>
-							<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
-								{{if not $.CanSignedUserFork}}
-									{{if gt (len $.UserAndOrgForks) 1}}
-										data-modal="#fork-repo-modal"
-									{{else if eq (len $.UserAndOrgForks) 1}}
-										href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
-									{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
-									{{end}}
-								{{else if not $.UserAndOrgForks}}
-									href="{{$.RepoLink}}/fork"
-								{{else}}
-									data-modal="#fork-repo-modal"
-								{{end}}
-							>
-								{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
-							</a>
-							<div class="ui small modal" id="fork-repo-modal">
-								<div class="header">
-									{{ctx.Locale.Tr "repo.already_forked" .Name}}
-								</div>
-								<div class="content gt-text-left">
-									<div class="ui list">
-										{{range $.UserAndOrgForks}}
-											<div class="ui item gt-py-3">
-												<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
-											</div>
-										{{end}}
-									</div>
-									{{if $.CanSignedUserFork}}
-									<div class="divider"></div>
-									<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
-									{{end}}
-								</div>
-							</div>
-							<a class="ui basic label" href="{{.Link}}/forks">
-								{{CountFmt .NumForks}}
-							</a>
-						</div>
+					{{if not $.DisableForks}}
+					{{template "repo/header_fork" $}}
 					{{end}}
 				</div>
 			{{end}}
diff --git a/templates/repo/header_fork.tmpl b/templates/repo/header_fork.tmpl
new file mode 100644
index 000000000..5bce9e0f1
--- /dev/null
+++ b/templates/repo/header_fork.tmpl
@@ -0,0 +1,50 @@
+{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
+	<div class="ui labeled button
+		{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
+			disabled
+		{{end}}"
+		{{if not $.IsSigned}}
+			data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
+		{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
+			data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
+		{{end}}
+	>
+		<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
+			{{if not $.CanSignedUserFork}}
+				{{if gt (len $.UserAndOrgForks) 1}}
+					data-modal="#fork-repo-modal"
+				{{else if eq (len $.UserAndOrgForks) 1}}
+					href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
+				{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
+				{{end}}
+			{{else if not $.UserAndOrgForks}}
+				href="{{$.RepoLink}}/fork"
+			{{else}}
+				data-modal="#fork-repo-modal"
+			{{end}}
+		>
+			{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
+		</a>
+		<div class="ui small modal" id="fork-repo-modal">
+			<div class="header">
+				{{ctx.Locale.Tr "repo.already_forked" .Name}}
+			</div>
+			<div class="content gt-text-left">
+				<div class="ui list">
+					{{range $.UserAndOrgForks}}
+						<div class="ui item gt-py-3">
+							<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
+						</div>
+					{{end}}
+				</div>
+				{{if $.CanSignedUserFork}}
+				<div class="divider"></div>
+				<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
+				{{end}}
+			</div>
+		</div>
+		<a class="ui basic label" href="{{.Link}}/forks">
+			{{CountFmt .NumForks}}
+		</a>
+	</div>
+{{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 0b330a89e..18ab54441 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -20565,6 +20565,10 @@
       "description": "GeneralRepoSettings contains global repository settings exposed by API",
       "type": "object",
       "properties": {
+        "forks_disabled": {
+          "type": "boolean",
+          "x-go-name": "ForksDisabled"
+        },
         "http_git_disabled": {
           "type": "boolean",
           "x-go-name": "HTTPGitDisabled"
diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go
index 7c231415a..87d2a1015 100644
--- a/tests/integration/api_fork_test.go
+++ b/tests/integration/api_fork_test.go
@@ -1,13 +1,18 @@
 // Copyright 2017 The Gogs Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package integration
 
 import (
 	"net/http"
+	"net/url"
 	"testing"
 
+	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/routers"
 	"code.gitea.io/gitea/tests"
 )
 
@@ -16,3 +21,27 @@ func TestCreateForkNoLogin(t *testing.T) {
 	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
 	MakeRequest(t, req, http.StatusUnauthorized)
 }
+
+func TestAPIDisabledForkRepo(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		defer test.MockVariableValue(&setting.Repository.DisableForks, true)()
+		defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
+
+		t.Run("fork listing", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
+			MakeRequest(t, req, http.StatusNotFound)
+		})
+
+		t.Run("forking", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			session := loginUser(t, "user5")
+			token := getTokenForLoggedInUser(t, session)
+
+			req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(token)
+			session.MakeRequest(t, req, http.StatusNotFound)
+		})
+	})
+}
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index c6e3fed7a..6c0cdc433 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -1,4 +1,5 @@
 // Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
 // SPDX-License-Identifier: MIT
 
 package integration
@@ -14,6 +15,9 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/routers"
 	repo_service "code.gitea.io/gitea/services/repository"
 	"code.gitea.io/gitea/tests"
 
@@ -119,6 +123,48 @@ func TestRepoFork(t *testing.T) {
 				session.MakeRequest(t, req, http.StatusNotFound)
 			})
 		})
+
+		t.Run("DISABLE_FORKS", func(t *testing.T) {
+			defer test.MockVariableValue(&setting.Repository.DisableForks, true)()
+			defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
+
+			t.Run("fork button not present", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				// The "Fork" button should not appear on the repo home
+				req := NewRequest(t, "GET", "/user2/repo1")
+				resp := MakeRequest(t, req, http.StatusOK)
+				htmlDoc := NewHTMLParser(t, resp.Body)
+				htmlDoc.AssertElement(t, "[href=/user2/repo1/fork]", false)
+			})
+
+			t.Run("forking by URL", func(t *testing.T) {
+				t.Run("by name", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					// Forking by URL should be Not Found
+					req := NewRequest(t, "GET", "/user2/repo1/fork")
+					session.MakeRequest(t, req, http.StatusNotFound)
+				})
+
+				t.Run("by legacy URL", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					// Forking by legacy URL should be Not Found
+					repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // user2/repo1
+					req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID)
+					session.MakeRequest(t, req, http.StatusNotFound)
+				})
+			})
+
+			t.Run("fork listing", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				// Listing the forks should be Not Found, too
+				req := NewRequest(t, "GET", "/user2/repo1/forks")
+				MakeRequest(t, req, http.StatusNotFound)
+			})
+		})
 	})
 }