diff --git a/agent/app/dto/nginx.go b/agent/app/dto/nginx.go
index 9c61dc45e2d7..6c4076aa8d1c 100644
--- a/agent/app/dto/nginx.go
+++ b/agent/app/dto/nginx.go
@@ -51,13 +51,19 @@ const (
CACHE NginxKey = "cache"
HttpPer NginxKey = "http-per"
ProxyCache NginxKey = "proxy-cache"
+ Brotli NginxKey = "brotli"
)
+// BrotliKeys are served from the panel-managed http.d file rather than
+// nginx.conf, because the module is optional: its directives must disappear
+// together with the module, otherwise nginx refuses to start.
+var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}
+
var ScopeKeyMap = map[NginxKey][]string{
Index: {"index"},
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
SSL: {"ssl_certificate", "ssl_certificate_key"},
- HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
+ HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level", "gzip_types", "gzip_vary", "gzip_proxied"},
}
var StaticFileKeyMap = map[NginxKey]struct {
diff --git a/agent/app/service/app_upgrade.go b/agent/app/service/app_upgrade.go
index 2a768abc03e0..05bafc9444ac 100644
--- a/agent/app/service/app_upgrade.go
+++ b/agent/app/service/app_upgrade.go
@@ -459,6 +459,13 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
}); err != nil {
return err
}
+ // Upgrades deliberately keep the user's nginx.conf, so corrected gzip
+ // defaults shipped with a new version would never reach existing
+ // installations. Rewrite only an untouched factory configuration, and
+ // never fail the upgrade over it.
+ if gzipErr := upgradeStockNginxGzipConfig(u.candidate); gzipErr != nil {
+ t.Logf("WARNING: update stock gzip configuration failed, keeping the current one: %v", gzipErr)
+ }
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err
}
diff --git a/agent/app/service/nginx.go b/agent/app/service/nginx.go
index 063dea7af8da..c992692b803d 100644
--- a/agent/app/service/nginx.go
+++ b/agent/app/service/nginx.go
@@ -63,6 +63,9 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
}
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) {
+ if req.Scope == dto.Brotli {
+ return getNginxBrotliParams()
+ }
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil, nil
@@ -71,6 +74,9 @@ func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.Ng
}
func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
+ if req.Scope == dto.Brotli {
+ return updateNginxBrotliParams(getNginxParams(req.Params, dto.BrotliKeys))
+ }
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil
diff --git a/agent/app/service/nginx_gzip_upgrade.go b/agent/app/service/nginx_gzip_upgrade.go
new file mode 100644
index 000000000000..16a798072569
--- /dev/null
+++ b/agent/app/service/nginx_gzip_upgrade.go
@@ -0,0 +1,165 @@
+package service
+
+import (
+ "os"
+ "path"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/1Panel-dev/1Panel/agent/app/model"
+ "github.com/1Panel-dev/1Panel/agent/constant"
+ "github.com/1Panel-dev/1Panel/agent/global"
+)
+
+// stockNginxGzipDirectives is the gzip block shipped by the OpenResty app
+// since 1.21.4.3. The upgrade only rewrites values when the installed
+// nginx.conf still carries exactly these directives and values, which proves
+// the user never tuned compression. Any deviation aborts the rewrite.
+var stockNginxGzipDirectives = map[string]string{
+ "gzip": "on",
+ "gzip_min_length": "1k",
+ "gzip_buffers": "4 16k",
+ "gzip_http_version": "1.1",
+ "gzip_comp_level": "2",
+ "gzip_types": "text/plain application/javascript application/x-javascript text/javascript text/css application/xml",
+ "gzip_vary": "on",
+ "gzip_proxied": "expired no-cache no-store private auth",
+ "gzip_disable": `"MSIE [1-6]\."`,
+}
+
+// correctedNginxGzipDirectives replaces the stock values in place. gzip lives
+// in the http block of nginx.conf and must stay there: repeating it from an
+// included file would make nginx reject the configuration with a duplicate
+// directive error, and the compression settings page reads and writes these
+// same keys in nginx.conf.
+var correctedNginxGzipDirectives = map[string]string{
+ "gzip_comp_level": "5",
+ "gzip_types": strings.Join(nginxCompressibleTypes, " "),
+ "gzip_proxied": "any",
+}
+
+// obsoleteNginxGzipDirectives are dropped outright.
+var obsoleteNginxGzipDirectives = map[string]struct{}{
+ // A per-request User-Agent regex for browsers with no measurable share.
+ "gzip_disable": {},
+}
+
+var nginxGzipDirectiveRe = regexp.MustCompile(`(?m)^[ \t]*(gzip[a-z_]*)[ \t]+([^;\n]*);[ \t]*$`)
+
+func nginxMainConfigPath(install model.AppInstall) string {
+ return path.Join(install.GetPath(), nginxModuleConfDir, "nginx.conf")
+}
+
+// upgradeStockNginxGzipConfig rewrites the factory gzip defaults in place.
+//
+// Upgrades deliberately preserve the user's nginx.conf, so corrected defaults
+// shipped with a new OpenResty version would otherwise never reach existing
+// installations.
+//
+// The config parser is not used: its dumper regenerates the whole file, drops
+// standalone comments and reorders proxy includes, which would be destructive
+// on a user's main config. Lines are edited individually so everything outside
+// the gzip block stays byte-identical.
+func upgradeStockNginxGzipConfig(install model.AppInstall) error {
+ configPath := nginxMainConfigPath(install)
+ content, err := os.ReadFile(configPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
+ }
+ if !isStockNginxGzipConfig(string(content)) {
+ return nil
+ }
+ updated := rewriteNginxGzipDirectives(string(content))
+ if updated == string(content) {
+ return nil
+ }
+ if err = os.WriteFile(configPath, []byte(updated), constant.FilePerm); err != nil {
+ return err
+ }
+ if err = nginxCheckAndReload(string(content), configPath, install.ContainerName); err != nil {
+ return err
+ }
+ global.LOG.Info("updated the stock OpenResty gzip configuration to the current defaults")
+ return nil
+}
+
+// isStockNginxGzipConfig reports whether every gzip directive in the config
+// matches the factory defaults exactly, with none missing and none extra.
+func isStockNginxGzipConfig(content string) bool {
+ found := make(map[string]string)
+ for _, match := range nginxGzipDirectiveRe.FindAllStringSubmatch(content, -1) {
+ name := match[1]
+ value := strings.Join(strings.Fields(match[2]), " ")
+ if _, ok := found[name]; ok {
+ // A directive repeated in the http block means the config was
+ // edited by hand; leave it alone.
+ return false
+ }
+ found[name] = value
+ }
+ if len(found) != len(stockNginxGzipDirectives) {
+ return false
+ }
+ for name, expected := range stockNginxGzipDirectives {
+ if found[name] != expected {
+ return false
+ }
+ }
+ return true
+}
+
+// rewriteNginxGzipDirectives updates known values in place, drops obsolete
+// directives and appends directives that are missing, preserving the original
+// indentation and leaving every other line untouched.
+func rewriteNginxGzipDirectives(content string) string {
+ lines := strings.Split(content, "\n")
+ result := make([]string, 0, len(lines))
+ seen := make(map[string]struct{})
+ lastGzipIndex := -1
+ indent := " "
+
+ for _, line := range lines {
+ match := nginxGzipDirectiveRe.FindStringSubmatch(line)
+ if match == nil {
+ result = append(result, line)
+ continue
+ }
+ name := match[1]
+ if leading := line[:len(line)-len(strings.TrimLeft(line, " \t"))]; leading != "" {
+ indent = leading
+ }
+ if _, obsolete := obsoleteNginxGzipDirectives[name]; obsolete {
+ continue
+ }
+ seen[name] = struct{}{}
+ if replacement, ok := correctedNginxGzipDirectives[name]; ok {
+ result = append(result, indent+name+" "+replacement+";")
+ } else {
+ result = append(result, line)
+ }
+ lastGzipIndex = len(result) - 1
+ }
+
+ // Directives introduced by a newer default set are appended right after
+ // the existing block so they stay visually grouped.
+ var missing []string
+ for name := range correctedNginxGzipDirectives {
+ if _, ok := seen[name]; !ok {
+ missing = append(missing, name)
+ }
+ }
+ if len(missing) == 0 || lastGzipIndex < 0 {
+ return strings.Join(result, "\n")
+ }
+ sort.Strings(missing)
+ added := make([]string, 0, len(missing))
+ for _, name := range missing {
+ added = append(added, indent+name+" "+correctedNginxGzipDirectives[name]+";")
+ }
+ tail := append(added, result[lastGzipIndex+1:]...)
+ return strings.Join(append(result[:lastGzipIndex+1], tail...), "\n")
+}
diff --git a/agent/app/service/nginx_gzip_upgrade_test.go b/agent/app/service/nginx_gzip_upgrade_test.go
new file mode 100644
index 000000000000..03942e68ab47
--- /dev/null
+++ b/agent/app/service/nginx_gzip_upgrade_test.go
@@ -0,0 +1,147 @@
+package service
+
+import (
+ "strings"
+ "testing"
+)
+
+const stockNginxConf = `user root;
+worker_processes auto;
+
+include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;
+
+events {
+ use epoll;
+}
+
+http {
+ include mime.types;
+ default_type application/octet-stream;
+
+ server_names_hash_bucket_size 512;
+ keepalive_requests 5000;
+
+ gzip on;
+ gzip_min_length 1k;
+ gzip_buffers 4 16k;
+ gzip_http_version 1.1;
+ gzip_comp_level 2;
+ gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
+ gzip_vary on;
+ gzip_proxied expired no-cache no-store private auth;
+ gzip_disable "MSIE [1-6]\.";
+
+ limit_conn_zone $binary_remote_addr zone=perip:10m;
+
+ include /usr/local/openresty/nginx/conf/http.d/*.conf;
+ include /usr/local/openresty/nginx/conf/conf.d/*.conf;
+}
+`
+
+func TestIsStockNginxGzipConfig(t *testing.T) {
+ if !isStockNginxGzipConfig(stockNginxConf) {
+ t.Fatal("factory configuration should be detected as stock")
+ }
+}
+
+func TestIsStockNginxGzipConfigRejectsTunedValues(t *testing.T) {
+ cases := map[string]string{
+ "comp level changed": strings.Replace(stockNginxConf, "gzip_comp_level 2;", "gzip_comp_level 6;", 1),
+ "gzip disabled": strings.Replace(stockNginxConf, "gzip on;", "gzip off;", 1),
+ "types extended": strings.Replace(stockNginxConf,
+ "application/xml;", "application/xml application/json;", 1),
+ "directive removed": strings.Replace(stockNginxConf, " gzip_vary on;\n", "", 1),
+ "directive added": strings.Replace(stockNginxConf, " gzip_vary on;\n",
+ " gzip_vary on;\n gzip_static on;\n", 1),
+ }
+ for name, content := range cases {
+ if isStockNginxGzipConfig(content) {
+ t.Errorf("%s: tuned configuration must not be rewritten", name)
+ }
+ }
+}
+
+func TestIsStockNginxGzipConfigRejectsDuplicateDirective(t *testing.T) {
+ content := strings.Replace(stockNginxConf, " gzip on;\n", " gzip on;\n gzip on;\n", 1)
+ if isStockNginxGzipConfig(content) {
+ t.Fatal("a duplicated directive indicates a hand-edited config")
+ }
+}
+
+func TestRewriteNginxGzipDirectives(t *testing.T) {
+ result := rewriteNginxGzipDirectives(stockNginxConf)
+
+ for _, expected := range []string{
+ " gzip_comp_level 5;",
+ " gzip_proxied any;",
+ " gzip on;",
+ " gzip_vary on;",
+ } {
+ if !strings.Contains(result, expected) {
+ t.Errorf("expected directive missing: %s\n%s", expected, result)
+ }
+ }
+ if !strings.Contains(result, "application/json") {
+ t.Error("gzip_types should now cover application/json")
+ }
+ if strings.Contains(result, "gzip_disable") {
+ t.Error("obsolete gzip_disable should have been dropped")
+ }
+ if strings.Contains(result, "gzip_comp_level 2;") {
+ t.Error("stale comp level should have been replaced")
+ }
+ // Everything outside the gzip block must survive untouched.
+ for _, keep := range []string{
+ "server_names_hash_bucket_size 512;",
+ "keepalive_requests 5000;",
+ "limit_conn_zone $binary_remote_addr zone=perip:10m;",
+ "include /usr/local/openresty/nginx/conf/http.d/*.conf;",
+ "include /usr/local/openresty/nginx/conf/conf.d/*.conf;",
+ "include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;",
+ "user root;",
+ } {
+ if !strings.Contains(result, keep) {
+ t.Errorf("unrelated line was altered or dropped: %s", keep)
+ }
+ }
+ if !strings.HasSuffix(result, "}\n") {
+ t.Error("trailing newline was not preserved")
+ }
+}
+
+func TestRewriteNginxGzipDirectivesIsIdempotent(t *testing.T) {
+ once := rewriteNginxGzipDirectives(stockNginxConf)
+ twice := rewriteNginxGzipDirectives(once)
+ if once != twice {
+ t.Errorf("rewrite is not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", once, twice)
+ }
+}
+
+func TestRewriteNginxGzipDirectivesAppendsMissing(t *testing.T) {
+ // gzip_proxied absent from the source must be appended, not silently lost.
+ content := strings.Replace(stockNginxConf,
+ " gzip_proxied expired no-cache no-store private auth;\n", "", 1)
+ result := rewriteNginxGzipDirectives(content)
+ if !strings.Contains(result, "gzip_proxied any;") {
+ t.Errorf("missing directive was not appended:\n%s", result)
+ }
+ if !strings.Contains(result, "limit_conn_zone $binary_remote_addr zone=perip:10m;") {
+ t.Error("appending must not clobber following lines")
+ }
+}
+
+func TestRewriteNginxGzipDirectivesKeepsGzipLikeNames(t *testing.T) {
+ // gunzip and proxy_set_header must survive: only directives whose name
+ // starts with "gzip" are managed here.
+ content := "http {\n gunzip on;\n gzip on;\n proxy_set_header Accept-Encoding gzip;\n}\n"
+ result := rewriteNginxGzipDirectives(content)
+ if !strings.Contains(result, "gunzip on;") {
+ t.Error("gunzip directive must be preserved")
+ }
+ if !strings.Contains(result, "proxy_set_header Accept-Encoding gzip;") {
+ t.Error("proxy_set_header must be preserved")
+ }
+ if !strings.Contains(result, " gzip on;") {
+ t.Error("gzip directive should be kept in place")
+ }
+}
diff --git a/agent/app/service/nginx_http_config.go b/agent/app/service/nginx_http_config.go
new file mode 100644
index 000000000000..6628a45123e7
--- /dev/null
+++ b/agent/app/service/nginx_http_config.go
@@ -0,0 +1,150 @@
+package service
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/1Panel-dev/1Panel/agent/app/model"
+ "github.com/1Panel-dev/1Panel/agent/constant"
+)
+
+const (
+ // nginxHTTPConfDir holds http-context directives generated by 1Panel.
+ // load_module is a main-context directive and therefore lives in
+ // modules-enabled, which cannot host http-context directives such as
+ // "brotli on". The directory is included by nginx.conf before conf.d so
+ // that per-site configuration keeps overriding these defaults.
+ nginxHTTPConfDir = "http.d"
+
+ nginxHTTPConfigPrefix = "1panel-http-"
+ nginxHTTPConfigHeader = "# Managed by 1Panel. Manual changes will be overwritten.\n"
+)
+
+// nginxHTTPDirective is a single http-context directive rendered into a
+// managed file.
+type nginxHTTPDirective struct {
+ Name string
+ Params []string
+}
+
+func (d nginxHTTPDirective) render() string {
+ if len(d.Params) == 0 {
+ return d.Name + ";"
+ }
+ return d.Name + " " + strings.Join(d.Params, " ") + ";"
+}
+
+// nginxHTTPConfigSupported reports whether the installed OpenResty exposes the
+// managed http.d directory. Installations created before http.d was introduced
+// have no such directory and no include for it, so writing files there would
+// silently have no effect.
+func nginxHTTPConfigSupported(install model.AppInstall) bool {
+ info, err := os.Stat(nginxHTTPConfigDir(install))
+ return err == nil && info.IsDir()
+}
+
+func nginxHTTPConfigDir(install model.AppInstall) string {
+ return path.Join(install.GetPath(), nginxModuleConfDir, nginxHTTPConfDir)
+}
+
+func nginxHTTPConfigFileName(order int, name string) string {
+ return fmt.Sprintf("%s%04d-%s.conf", nginxHTTPConfigPrefix, order, nginxModulePathName(name))
+}
+
+// renderNginxHTTPConfig builds the content of a managed http.d file.
+func renderNginxHTTPConfig(directives []nginxHTTPDirective) []byte {
+ var content strings.Builder
+ content.WriteString(nginxHTTPConfigHeader)
+ for _, directive := range directives {
+ content.WriteString(directive.render())
+ content.WriteString("\n")
+ }
+ return []byte(content.String())
+}
+
+var nginxHTTPDirectiveRe = regexp.MustCompile(`^[ \t]*([a-z_][a-z0-9_]*)[ \t]+([^;]*);[ \t]*$`)
+
+// readNginxHTTPDirectives parses a managed file back into directive values.
+// A missing or unreadable file yields no directives, which makes callers fall
+// back to their defaults.
+func readNginxHTTPDirectives(filePath string) map[string][]string {
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil
+ }
+ directives := make(map[string][]string)
+ for _, line := range strings.Split(string(content), "\n") {
+ match := nginxHTTPDirectiveRe.FindStringSubmatch(line)
+ if match == nil {
+ continue
+ }
+ directives[match[1]] = strings.Fields(match[2])
+ }
+ return directives
+}
+
+// snapshotManagedNginxHTTPConfigs captures every managed file so a failed
+// nginx -t can be rolled back.
+func snapshotManagedNginxHTTPConfigs(configDir string) (nginxModuleConfigSnapshot, error) {
+ snapshot := make(nginxModuleConfigSnapshot)
+ entries, err := os.ReadDir(configDir)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return snapshot, nil
+ }
+ return nil, err
+ }
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
+ continue
+ }
+ content, readErr := os.ReadFile(path.Join(configDir, entry.Name()))
+ if readErr != nil {
+ return nil, readErr
+ }
+ snapshot[entry.Name()] = content
+ }
+ return snapshot, nil
+}
+
+// applyManagedNginxHTTPConfigs writes the desired managed files and removes
+// managed files that are no longer wanted. Files not carrying the managed
+// prefix are never touched.
+func applyManagedNginxHTTPConfigs(configDir string, desired map[string][]byte) error {
+ if err := os.MkdirAll(configDir, constant.DirPerm); err != nil {
+ return err
+ }
+ entries, err := os.ReadDir(configDir)
+ if err != nil {
+ return err
+ }
+ names := make([]string, 0, len(desired))
+ for fileName := range desired {
+ names = append(names, fileName)
+ }
+ sort.Strings(names)
+ for _, fileName := range names {
+ tmpPath := path.Join(configDir, "."+fileName+".tmp")
+ if err = os.WriteFile(tmpPath, desired[fileName], constant.FilePerm); err != nil {
+ return err
+ }
+ if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil {
+ return err
+ }
+ }
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxHTTPConfigPrefix) {
+ continue
+ }
+ if _, ok := desired[entry.Name()]; !ok {
+ if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/agent/app/service/nginx_module.go b/agent/app/service/nginx_module.go
index d299cb6b0a4b..705f61cc23f2 100644
--- a/agent/app/service/nginx_module.go
+++ b/agent/app/service/nginx_module.go
@@ -18,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
+ "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -164,6 +165,51 @@ func nginxModuleDynamicSupported(install model.AppInstall) bool {
fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
}
+// nginxModuleStaticSupported reports whether the install can recompile its own
+// OpenResty image, which is what a static module build needs. Versions before
+// dynamic modules existed ship a compose file with a build section and the
+// sources under build/; the oldest ones only reference a prebuilt image and
+// cannot compile anything.
+func nginxModuleStaticSupported(install model.AppInstall) bool {
+ if !files.NewFileOp().Stat(path.Join(install.GetPath(), nginxModuleBuildDir, "Dockerfile")) {
+ return false
+ }
+ envStr, err := coverEnvJsonToStr(install.Env)
+ if err != nil {
+ return false
+ }
+ project, err := dockerUtils.GetComposeProject(install.Name, install.GetPath(),
+ []byte(install.DockerCompose), []byte(envStr), true)
+ if err != nil {
+ return false
+ }
+ for _, service := range project.AllServices() {
+ if service.Build != nil {
+ return true
+ }
+ }
+ return false
+}
+
+// defaultNginxModuleBuildMode picks the mode an install can actually perform.
+//
+// Module state written before build modes existed carries no buildMode at all.
+// Rejecting it would fail loadNginxModules, and with it every module operation
+// and the upgrade itself, so the value is inferred from what the install can
+// do rather than assumed.
+func defaultNginxModuleBuildMode(install model.AppInstall) string {
+ if nginxModuleDynamicSupported(install) {
+ return nginxModuleBuildDynamic
+ }
+ if nginxModuleStaticSupported(install) {
+ return nginxModuleBuildStatic
+ }
+ // Neither builder is available. Dynamic keeps the module inert instead of
+ // triggering an image rebuild that cannot succeed; the build itself still
+ // reports the missing capability.
+ return nginxModuleBuildDynamic
+}
+
func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
@@ -654,10 +700,37 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
return fmt.Errorf("validate combined dynamic module configuration: %w", err)
}
}
- if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
+
+ // Runtime directives live in http.d because load_module is main-context
+ // while directives such as "brotli on" are http-context. Both sets are
+ // written before nginx -t runs, so nginx only ever observes the final,
+ // consistent state; on failure both are rolled back together.
+ httpConfigDir := nginxHTTPConfigDir(install)
+ httpSupported := nginxHTTPConfigSupported(install)
+ var httpSnapshot nginxModuleConfigSnapshot
+ if httpSupported {
+ if httpSnapshot, err = snapshotManagedNginxHTTPConfigs(httpConfigDir); err != nil {
+ return err
+ }
+ }
+ restore := func() {
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
+ if httpSupported {
+ _ = applyManagedNginxHTTPConfigs(httpConfigDir, httpSnapshot)
+ }
+ }
+
+ if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
+ restore()
return err
}
+ if httpSupported {
+ desiredHTTP := desiredNginxModuleRuntimeConfigs(install, modules, target)
+ if err = applyManagedNginxHTTPConfigs(httpConfigDir, desiredHTTP); err != nil {
+ restore()
+ return err
+ }
+ }
if !reload {
return nil
}
@@ -666,11 +739,11 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
return nil
}
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
- _ = applyManagedNginxModuleConfigs(configDir, snapshot)
+ restore()
return err
}
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
- _ = applyManagedNginxModuleConfigs(configDir, snapshot)
+ restore()
return err
}
return nil
@@ -722,6 +795,14 @@ func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte)
return nil
}
+// hasEnabledStaticNginxModules reports whether a full image rebuild is needed.
+//
+// Module state is the only input on purpose. RESTY_CONFIG_OPTIONS_MORE in .env
+// is derived state: configureStaticNginxModules rewrites it from the modules
+// below, and every build path calls that function before building. Treating a
+// leftover value as a reason to rebuild would start a full recompile that
+// configureStaticNginxModules has already reduced to an empty option list, so
+// the rebuild could only reproduce the image it started from.
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
for _, module := range modules {
normalizeNginxModule(&module)
@@ -732,17 +813,6 @@ func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
return false
}
-func staticNginxBuildRequired(install model.AppInstall, modules []dto.NginxModule) bool {
- if hasEnabledStaticNginxModules(modules) {
- return true
- }
- envs, err := gotenv.Read(install.GetEnvPath())
- if err != nil {
- return false
- }
- return strings.TrimSpace(envs["RESTY_CONFIG_OPTIONS_MORE"]) != ""
-}
-
func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
var params, packages []string
@@ -807,11 +877,21 @@ func executeNginxModuleBuild(install model.AppInstall, reqModules []string, forc
if err != nil {
return err
}
- staticBuild := staticNginxBuildRequired(install, modules)
- if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
- if !nginxModuleDynamicSupported(install) {
- return errors.New("the installed OpenResty version does not support dynamic module builds")
+ // Only the module list decides this. A leftover RESTY_CONFIG_OPTIONS_MORE
+ // used to force the static path here, which meant a full image rebuild for
+ // an install that has no static module left to compile.
+ staticBuild := hasEnabledStaticNginxModules(modules)
+ if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) && !nginxModuleDynamicSupported(install) {
+ // The install predates dynamic modules. Compile the same modules into
+ // the image instead of refusing the build, which is how these versions
+ // have always produced modules.
+ if !nginxModuleStaticSupported(install) {
+ return buserr.New("ErrModuleBuildUnsupported")
+ }
+ if modules, err = convertNginxModulesToStatic(modules, reqModules); err != nil {
+ return err
}
+ staticBuild = true
}
if staticBuild {
return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
@@ -886,7 +966,17 @@ func loadNginxModulesWithCatalog(install model.AppInstall, catalogPath string) (
Builds: state.Builds, LastError: state.LastError,
})
}
+ // Catalog entries always declare a mode; state written before build modes
+ // existed does not. Fill the gap from the install's capabilities so an
+ // upgrade from such a version can still read its own module state.
+ fallbackMode := ""
for i := range modules {
+ if modules[i].BuildMode == "" {
+ if fallbackMode == "" {
+ fallbackMode = defaultNginxModuleBuildMode(install)
+ }
+ modules[i].BuildMode = fallbackMode
+ }
if err = validateNginxModuleBuildMode(modules[i]); err != nil {
return nil, err
}
@@ -1266,6 +1356,43 @@ func normalizeDynamicModuleParams(params string) (string, error) {
return params, nil
}
+// normalizeStaticModuleParams is the inverse of normalizeDynamicModuleParams:
+// a module compiled into the image uses --add-module and the plain form of
+// nginx's built-in switches.
+func normalizeStaticModuleParams(params string) string {
+ params = strings.TrimSpace(params)
+ params = strings.ReplaceAll(params, "--add-dynamic-module=", "--add-module=")
+ return strings.ReplaceAll(params, "=dynamic", "")
+}
+
+// convertNginxModulesToStatic retargets the modules a build was asked to
+// produce so they are compiled into the image. It is used when the install
+// has no dynamic builder, where this is the only way to produce a module.
+//
+// The change is confined to the returned slice: it drives one build and is
+// never persisted, so the catalog's declared mode stays authoritative and the
+// modules go back to dynamic once the install gains a builder.
+func convertNginxModulesToStatic(modules []dto.NginxModule, selected []string) ([]dto.NginxModule, error) {
+ selectedNames := make(map[string]struct{}, len(selected))
+ for _, name := range selected {
+ selectedNames[name] = struct{}{}
+ }
+ converted := cloneNginxModules(modules)
+ for i := range converted {
+ if !nginxModuleNeedsDynamicBuild(converted[i], selectedNames) {
+ continue
+ }
+ params := normalizeStaticModuleParams(converted[i].Params)
+ if params == "" {
+ return nil, fmt.Errorf("OpenResty module %s has no configure options to compile into the image", converted[i].Name)
+ }
+ converted[i].BuildMode = nginxModuleBuildStatic
+ converted[i].Params = params
+ converted[i].Enable = true
+ }
+ return converted, nil
+}
+
func parseDynamicModuleParams(params string) ([]string, error) {
// shellwords silently stops at unquoted shell metacharacters instead of
// reporting them, so reject them on the raw input before parsing.
diff --git a/agent/app/service/nginx_module_runtime.go b/agent/app/service/nginx_module_runtime.go
new file mode 100644
index 000000000000..0353550304b0
--- /dev/null
+++ b/agent/app/service/nginx_module_runtime.go
@@ -0,0 +1,203 @@
+package service
+
+import (
+ "path"
+
+ "github.com/1Panel-dev/1Panel/agent/app/dto"
+ "github.com/1Panel-dev/1Panel/agent/app/dto/response"
+ "github.com/1Panel-dev/1Panel/agent/app/model"
+ "github.com/1Panel-dev/1Panel/agent/buserr"
+ "github.com/1Panel-dev/1Panel/agent/constant"
+)
+
+// nginxCompressibleTypes is shared by gzip_types and brotli_types so both
+// encoders cover the same content. Already compressed formats (images other
+// than SVG, woff/woff2, archives, media) are deliberately excluded:
+// recompressing them costs CPU and usually grows the payload.
+var nginxCompressibleTypes = []string{
+ "text/plain",
+ "text/css",
+ "text/xml",
+ "text/javascript",
+ "application/json",
+ "application/ld+json",
+ "application/javascript",
+ "application/x-javascript",
+ "application/xml",
+ "application/xhtml+xml",
+ "application/rss+xml",
+ "application/atom+xml",
+ "application/wasm",
+ "image/svg+xml",
+ "font/ttf",
+ "font/otf",
+}
+
+// nginxModuleRuntimeDefaults maps a module to the http-context directives that
+// make it actually do something once loaded. Without these, enabling a module
+// only emits load_module, leaving it loaded but inert.
+//
+// brotli_static is intentionally omitted: nginx does not verify that a .br
+// file is newer than its source, so a stale artifact would be served
+// indefinitely with no error.
+var nginxModuleRuntimeDefaults = map[string][]nginxHTTPDirective{
+ "ngx_brotli": {
+ {Name: "brotli", Params: []string{"on"}},
+ // Brotli level 5 reaches roughly gzip level 9 ratio at a fraction of
+ // the cost. The nginx default of 6 is tuned for static assets and is
+ // too expensive for dynamic responses.
+ {Name: "brotli_comp_level", Params: []string{"5"}},
+ {Name: "brotli_min_length", Params: []string{"1k"}},
+ {Name: "brotli_types", Params: nginxCompressibleTypes},
+ },
+}
+
+// nginxModuleRuntimeLoadOrder keeps managed file names stable and ordered
+// independently of the module load order used for load_module.
+var nginxModuleRuntimeLoadOrder = map[string]int{
+ "ngx_brotli": 100,
+}
+
+func nginxModuleRuntimeOrder(name string) int {
+ if order, ok := nginxModuleRuntimeLoadOrder[name]; ok {
+ return order
+ }
+ return 900
+}
+
+// desiredNginxModuleRuntimeConfigs renders the managed http.d files for every
+// enabled module that has a ready build and known runtime defaults.
+//
+// Values the user changed through the compression settings page are read back
+// from the current managed file, so reconciling after an unrelated module
+// change does not silently reset them to the defaults.
+func desiredNginxModuleRuntimeConfigs(install model.AppInstall, modules []dto.NginxModule, target dto.NginxModuleTarget) map[string][]byte {
+ desired := make(map[string][]byte)
+ for _, module := range modules {
+ normalizeNginxModule(&module)
+ directives, ok := nginxModuleRuntimeDefaults[module.Name]
+ if !ok || !module.Enable {
+ continue
+ }
+ if !nginxModuleRuntimeReady(module, target) {
+ continue
+ }
+ fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(module.Name), module.Name)
+ current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
+ desired[fileName] = renderNginxHTTPConfig(mergeNginxRuntimeDirectives(directives, current))
+ }
+ return desired
+}
+
+// mergeNginxRuntimeDirectives keeps the declared directive set and ordering
+// while preferring values already present in the managed file.
+func mergeNginxRuntimeDirectives(defaults []nginxHTTPDirective, current map[string][]string) []nginxHTTPDirective {
+ if len(current) == 0 {
+ return defaults
+ }
+ merged := make([]nginxHTTPDirective, 0, len(defaults))
+ for _, directive := range defaults {
+ if params, ok := current[directive.Name]; ok && len(params) > 0 {
+ directive.Params = params
+ }
+ merged = append(merged, directive)
+ }
+ return merged
+}
+
+// nginxBrotliModuleName is the catalog name of the brotli module.
+const nginxBrotliModuleName = "ngx_brotli"
+
+// getNginxBrotliParams reports the brotli settings currently in effect.
+//
+// Brotli is served from the managed http.d file instead of nginx.conf, so the
+// directives can be removed together with the module. When the module is
+// disabled the declared defaults are returned, which lets the settings page
+// show what would be applied once it is enabled.
+func getNginxBrotliParams() ([]response.NginxParam, error) {
+ install, err := getAppInstallByKey(constant.AppOpenresty)
+ if err != nil {
+ return nil, err
+ }
+ fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
+ current := readNginxHTTPDirectives(path.Join(nginxHTTPConfigDir(install), fileName))
+ res := make([]response.NginxParam, 0, len(dto.BrotliKeys))
+ for _, directive := range mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], current) {
+ res = append(res, response.NginxParam{Name: directive.Name, Params: directive.Params})
+ }
+ return res, nil
+}
+
+// updateNginxBrotliParams persists brotli settings to the managed http.d file.
+//
+// Writing is refused unless the module is enabled and built: the directives
+// would reference a module that is not loaded and nginx would fail to start.
+func updateNginxBrotliParams(params []dto.NginxParam) error {
+ install, err := getAppInstallByKey(constant.AppOpenresty)
+ if err != nil {
+ return err
+ }
+ if !nginxHTTPConfigSupported(install) {
+ return buserr.New("ErrBrotliUnsupported")
+ }
+ modules, err := loadNginxModules(install)
+ if err != nil {
+ return err
+ }
+ values := make(map[string][]string, len(params))
+ for _, param := range params {
+ values[param.Name] = param.Params
+ }
+ for i := range modules {
+ if modules[i].Name != nginxBrotliModuleName {
+ continue
+ }
+ if !modules[i].Enable {
+ return buserr.New("ErrBrotliDisabled")
+ }
+ fileName := nginxHTTPConfigFileName(nginxModuleRuntimeOrder(nginxBrotliModuleName), nginxBrotliModuleName)
+ configDir := nginxHTTPConfigDir(install)
+ snapshot, snapErr := snapshotManagedNginxHTTPConfigs(configDir)
+ if snapErr != nil {
+ return snapErr
+ }
+ merged := mergeNginxRuntimeDirectives(nginxModuleRuntimeDefaults[nginxBrotliModuleName], values)
+ desired := map[string][]byte{fileName: renderNginxHTTPConfig(merged)}
+ for name, content := range snapshot {
+ if name != fileName {
+ desired[name] = content
+ }
+ }
+ if err = applyManagedNginxHTTPConfigs(configDir, desired); err != nil {
+ _ = applyManagedNginxHTTPConfigs(configDir, snapshot)
+ return err
+ }
+ if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
+ _ = applyManagedNginxHTTPConfigs(configDir, snapshot)
+ return err
+ }
+ if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
+ _ = applyManagedNginxHTTPConfigs(configDir, snapshot)
+ return err
+ }
+ return nil
+ }
+ return buserr.New("ErrBrotliDisabled")
+}
+
+// nginxModuleRuntimeReady reports whether the module is actually usable.
+//
+// Dynamic modules need a ready build for the current target, otherwise the
+// .so is missing and nginx would reject the directives. Static modules are
+// compiled into the binary and carry no artifacts, so an enabled static
+// module is considered ready.
+func nginxModuleRuntimeReady(module dto.NginxModule, target dto.NginxModuleTarget) bool {
+ if module.BuildMode == nginxModuleBuildStatic {
+ return true
+ }
+ build := findCurrentNginxModuleBuild(module, target)
+ if build == nil || build.Status != nginxModuleStatusReady {
+ build = findLatestNginxModuleBuild(module, target)
+ }
+ return build != nil && build.Status == nginxModuleStatusReady
+}
diff --git a/agent/app/service/nginx_module_static_test.go b/agent/app/service/nginx_module_static_test.go
new file mode 100644
index 000000000000..18372f5b5c83
--- /dev/null
+++ b/agent/app/service/nginx_module_static_test.go
@@ -0,0 +1,205 @@
+package service
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/1Panel-dev/1Panel/agent/app/dto"
+)
+
+func TestHasEnabledStaticNginxModules(t *testing.T) {
+ cases := []struct {
+ name string
+ modules []dto.NginxModule
+ want bool
+ }{
+ {
+ name: "an enabled static module requires a rebuild",
+ modules: []dto.NginxModule{
+ {Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic},
+ },
+ want: true,
+ },
+ {
+ name: "a disabled static module does not",
+ modules: []dto.NginxModule{
+ {Name: "custom", Enable: false, BuildMode: nginxModuleBuildStatic},
+ },
+ want: false,
+ },
+ {
+ name: "dynamic modules never require a rebuild",
+ modules: []dto.NginxModule{
+ {Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
+ },
+ want: false,
+ },
+ {
+ name: "no modules at all",
+ modules: nil,
+ want: false,
+ },
+ {
+ name: "one enabled static module among dynamic ones is enough",
+ modules: []dto.NginxModule{
+ {Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
+ {Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic},
+ },
+ want: true,
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := hasEnabledStaticNginxModules(tc.modules); got != tc.want {
+ t.Fatalf("expected %v, got %v", tc.want, got)
+ }
+ })
+ }
+}
+
+// A stale RESTY_CONFIG_OPTIONS_MORE used to force the full-rebuild path even
+// with no static module enabled. configureStaticNginxModules derives that value
+// from the module list and runs before every build, so the rebuild it triggered
+// could only ever reproduce the current image. Module state is now the only
+// input; this test pins that down.
+func TestStaticRebuildIgnoresLeftoverBuildOptions(t *testing.T) {
+ modules := []dto.NginxModule{
+ {Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic},
+ }
+ if hasEnabledStaticNginxModules(modules) {
+ t.Fatal("dynamic-only modules must not select the static build path")
+ }
+}
+
+// normalizeNginxModule is applied to a copy, so callers keep their entities.
+func TestHasEnabledStaticNginxModulesDoesNotMutateInput(t *testing.T) {
+ modules := []dto.NginxModule{
+ {Name: "custom", Enable: true, BuildMode: nginxModuleBuildStatic, Packages: []string{"", "libfoo", ""}},
+ }
+ _ = hasEnabledStaticNginxModules(modules)
+ if len(modules[0].Packages) != 3 {
+ t.Fatalf("input was normalized in place: %v", modules[0].Packages)
+ }
+}
+
+func TestNormalizeStaticModuleParams(t *testing.T) {
+ cases := []struct {
+ name string
+ in string
+ want string
+ }{
+ {
+ name: "dynamic add-module is reversed",
+ in: "--add-dynamic-module=/tmp/ngx_http_geoip2_module",
+ want: "--add-module=/tmp/ngx_http_geoip2_module",
+ },
+ {
+ name: "already static params are left alone",
+ in: "--add-module=/usr/local/openresty/modules/ngx_brotli",
+ want: "--add-module=/usr/local/openresty/modules/ngx_brotli",
+ },
+ {
+ name: "built-in switches drop the dynamic suffix",
+ in: "--with-http_image_filter_module=dynamic",
+ want: "--with-http_image_filter_module",
+ },
+ {
+ name: "mixed options are handled together",
+ in: "--with-http_dav_module --add-dynamic-module=/tmp/nginx-dav-ext-module",
+ want: "--with-http_dav_module --add-module=/tmp/nginx-dav-ext-module",
+ },
+ {
+ name: "surrounding whitespace is trimmed",
+ in: " --add-dynamic-module=/tmp/x ",
+ want: "--add-module=/tmp/x",
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := normalizeStaticModuleParams(tc.in); got != tc.want {
+ t.Fatalf("expected %q, got %q", tc.want, got)
+ }
+ })
+ }
+}
+
+// The conversion must round-trip the params the dynamic path produces,
+// otherwise an install without a builder would compile the wrong thing.
+func TestStaticParamsRoundTripFromDynamic(t *testing.T) {
+ // These are the catalog params shipped with the OpenResty app.
+ for _, original := range []string{
+ "--add-module=/usr/local/openresty/modules/ngx_brotli",
+ "--add-module=/tmp/nginx-rtmp-module",
+ "--with-http_dav_module --add-module=/tmp/nginx-dav-ext-module",
+ "--add-module=/tmp/ngx_http_geoip2_module",
+ "--add-module=/tmp/ngx_http_substitutions_filter_module",
+ } {
+ dynamic, err := normalizeDynamicModuleParams(original)
+ if err != nil {
+ t.Fatalf("%s: %v", original, err)
+ }
+ if got := normalizeStaticModuleParams(dynamic); got != original {
+ t.Errorf("round trip changed the options:\n from %q\n via %q\n to %q", original, dynamic, got)
+ }
+ }
+}
+
+func TestConvertNginxModulesToStatic(t *testing.T) {
+ modules := []dto.NginxModule{
+ {Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic,
+ Params: "--add-dynamic-module=/usr/local/openresty/modules/ngx_brotli"},
+ {Name: "rtmp", Enable: false, BuildMode: nginxModuleBuildDynamic,
+ Params: "--add-dynamic-module=/tmp/nginx-rtmp-module"},
+ }
+ converted, err := convertNginxModulesToStatic(modules, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if converted[0].BuildMode != nginxModuleBuildStatic {
+ t.Error("an enabled module should be retargeted to static")
+ }
+ if !strings.Contains(converted[0].Params, "--add-module=") ||
+ strings.Contains(converted[0].Params, "--add-dynamic-module=") {
+ t.Errorf("params were not converted: %q", converted[0].Params)
+ }
+ if converted[1].BuildMode != nginxModuleBuildDynamic {
+ t.Error("a disabled module must be left alone")
+ }
+
+ // The caller's slice must survive untouched: the conversion drives a single
+ // build and is never written back to module.json.
+ if modules[0].BuildMode != nginxModuleBuildDynamic {
+ t.Error("the input slice was mutated")
+ }
+ if modules[0].Params != "--add-dynamic-module=/usr/local/openresty/modules/ngx_brotli" {
+ t.Errorf("input params were mutated: %q", modules[0].Params)
+ }
+}
+
+func TestConvertNginxModulesToStaticHonoursSelection(t *testing.T) {
+ modules := []dto.NginxModule{
+ {Name: "ngx_brotli", Enable: true, BuildMode: nginxModuleBuildDynamic,
+ Params: "--add-dynamic-module=/a"},
+ {Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic,
+ Params: "--add-dynamic-module=/b"},
+ }
+ converted, err := convertNginxModulesToStatic(modules, []string{"rtmp"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if converted[0].BuildMode != nginxModuleBuildDynamic {
+ t.Error("an unselected module must not be converted")
+ }
+ if converted[1].BuildMode != nginxModuleBuildStatic {
+ t.Error("the selected module should be converted")
+ }
+}
+
+func TestConvertNginxModulesToStaticRejectsEmptyParams(t *testing.T) {
+ modules := []dto.NginxModule{
+ {Name: "broken", Enable: true, BuildMode: nginxModuleBuildDynamic, Params: " "},
+ }
+ if _, err := convertNginxModulesToStatic(modules, nil); err == nil {
+ t.Fatal("a module with no configure options cannot be compiled in")
+ }
+}
diff --git a/agent/cmd/server/nginx_conf/gzip.conf b/agent/cmd/server/nginx_conf/gzip.conf
index b277f1f890df..27d27a84f109 100644
--- a/agent/cmd/server/nginx_conf/gzip.conf
+++ b/agent/cmd/server/nginx_conf/gzip.conf
@@ -1,4 +1,8 @@
gzip on;
-gzip_comp_level 6;
+gzip_vary on;
gzip_min_length 1k;
-gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype;
\ No newline at end of file
+gzip_buffers 4 16k;
+gzip_http_version 1.1;
+gzip_comp_level 5;
+gzip_proxied any;
+gzip_types text/plain text/css text/xml text/javascript application/json application/ld+json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/wasm image/svg+xml font/ttf font/otf;
diff --git a/agent/cmd/server/nginx_conf/nginx_conf.go b/agent/cmd/server/nginx_conf/nginx_conf.go
index cf2176146a6d..e540065fddf5 100644
--- a/agent/cmd/server/nginx_conf/nginx_conf.go
+++ b/agent/cmd/server/nginx_conf/nginx_conf.go
@@ -44,6 +44,9 @@ var Upstream []byte
//go:embed sse.conf
var SSE []byte
+//go:embed gzip.conf
+var Gzip []byte
+
//go:embed *.json *.conf
var websitesFiles embed.FS
diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml
index 9af2b721a0df..6f3e4baed004 100644
--- a/agent/i18n/lang/en.yaml
+++ b/agent/i18n/lang/en.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: 'Invalid domain format: {{ .name }}'
ErrDefaultAlias: 'default is reserved; use another alias'
ErrParentWebsite: 'Delete subsite {{ .name }} first'
ErrBuildDirNotFound: 'The build directory does not exist'
+ErrBrotliDisabled: 'The brotli module is not enabled, enable and build it first'
+ErrBrotliUnsupported: 'The installed OpenResty version does not support managed brotli settings'
+ErrModuleBuildUnsupported: 'This OpenResty version cannot build modules, upgrade it first'
ErrImageNotExist: 'Runtime image not found: {{ .name }}'
ErrProxyIsUsed: 'Load balancer is used by reverse proxy'
ErrSSLValid: 'Certificate file is invalid'
diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml
index 586d6466c049..bed77edd84c3 100644
--- a/agent/i18n/lang/es-ES.yaml
+++ b/agent/i18n/lang/es-ES.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: 'El formato del dominio {{ .name }} es incorrecto'
ErrDefaultAlias: 'default es un código reservado, use otro'
ErrParentWebsite: 'Primero debe eliminar el sub-sitio {{ .name }}'
ErrBuildDirNotFound: 'El directorio de compilación no existe'
+ErrBrotliDisabled: 'El módulo brotli no está habilitado, actívelo y compílelo primero'
+ErrBrotliUnsupported: 'La versión instalada de OpenResty no admite configuraciones brotli gestionadas'
+ErrModuleBuildUnsupported: 'Esta versión de OpenResty no puede compilar módulos, actualícela primero'
ErrImageNotExist: 'La imagen del entorno {{ .name }} no existe, edítela de nuevo'
ErrProxyIsUsed: 'El balanceo de carga ya está usado por un proxy reverso, no se puede eliminar'
ErrSSLValid: 'Archivo de certificado anómalo, revise el estado del certificado'
diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml
index 32d1bb561488..f45f4c716f42 100644
--- a/agent/i18n/lang/fa.yaml
+++ b/agent/i18n/lang/fa.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: 'فرمت دامنه نامعتبر است: {{ .name }}'
ErrDefaultAlias: 'default رزرو شده است؛ از نام مستعار دیگری استفاده کنید'
ErrParentWebsite: 'ابتدا زیرسایت {{ .name }} را حذف کنید'
ErrBuildDirNotFound: 'دایرکتوری ساخت وجود ندارد'
+ErrBrotliDisabled: 'ماژول brotli فعال نیست، ابتدا آن را فعال و بیلد کنید'
+ErrBrotliUnsupported: 'نسخه نصبشده OpenResty از تنظیمات مدیریتشده brotli پشتیبانی نمیکند'
+ErrModuleBuildUnsupported: 'این نسخه OpenResty نمیتواند ماژول بسازد، ابتدا آن را ارتقا دهید'
ErrImageNotExist: 'تصویر محیط اجرا یافت نشد: {{ .name }}'
ErrProxyIsUsed: 'تعادل بار توسط پراکسی معکوس استفاده میشود'
ErrSSLValid: 'فایل گواهی نامعتبر است'
diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml
index bb4d1a1a0389..b198968d787e 100644
--- a/agent/i18n/lang/ja.yaml
+++ b/agent/i18n/lang/ja.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: '{{ .name }} ドメイン名の形式が正しくありませ
ErrDefaultAlias: 'デフォルトは予約済みのコードです。別のコードを使用してください'
ErrParentWebsite: 'まずサブサイト {{ .name }} を削除する必要があります'
ErrBuildDirNotFound: 'ビルド ディレクトリが存在しません'
+ErrBrotliDisabled: 'brotli モジュールが有効になっていません。先に有効化してビルドしてください'
+ErrBrotliUnsupported: 'インストールされている OpenResty のバージョンは管理された brotli 設定をサポートしていません'
+ErrModuleBuildUnsupported: 'この OpenResty バージョンではモジュールをビルドできません。先にアップグレードしてください'
ErrImageNotExist: 'オペレーティング環境 {{ .name }} イメージが存在しません。オペレーティング環境を再編集してください'
ErrProxyIsUsed: 'ロードバランシングはリバースプロキシによって使用されているため、削除できません'
ErrSSLValid: '証明書ファイルが異常です、証明書の状態を確認してください!'
diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml
index f47378a22c45..6622ae82398c 100644
--- a/agent/i18n/lang/ko.yaml
+++ b/agent/i18n/lang/ko.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: '{{ .name }} 도메인 이름 형식이 올바르지 않습니
ErrDefaultAlias: '기본값은 예약된 코드입니다. 다른 코드를 사용하세요'
ErrParentWebsite: '먼저 하위 사이트 {{ .name }}을 삭제해야 합니다.'
ErrBuildDirNotFound: '빌드 디렉토리가 존재하지 않습니다'
+ErrBrotliDisabled: 'brotli 모듈이 활성화되지 않았습니다. 먼저 활성화하고 빌드하세요'
+ErrBrotliUnsupported: '설치된 OpenResty 버전은 관리형 brotli 설정을 지원하지 않습니다'
+ErrModuleBuildUnsupported: '현재 OpenResty 버전은 모듈을 빌드할 수 없습니다. 먼저 업그레이드하세요'
ErrImageNotExist: '운영 환경 {{ .name }} 이미지가 존재하지 않습니다. 운영 환경을 다시 편집하세요.'
ErrProxyIsUsed: '로드 밸런싱이 역방향 프록시에 의해 사용되었으므로 삭제할 수 없습니다'
ErrSSLValid: '인증서 파일에 문제가 있습니다. 인증서 상태를 확인하세요'
diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml
index 98394f4784cd..bdd7b43bd42f 100644
--- a/agent/i18n/lang/lo.yaml
+++ b/agent/i18n/lang/lo.yaml
@@ -205,6 +205,9 @@ ErrDomainFormat: 'ຮູບແບບໂດເມນບໍ່ຖືກຕ້ອ
ErrDefaultAlias: 'ຊື່ default ຖືກສະຫງວນໄວ້; ກະລຸນາໃຊ້ຊື່ອື່ນ'
ErrParentWebsite: 'ກະລຸນາລຶບເວັບໄຊຍ່ອຍ {{ .name }} ກ່ອນ'
ErrBuildDirNotFound: 'ບໍ່ມີໂຟນເດີ Build ຢູ່'
+ErrBrotliDisabled: 'ໂມດູນ brotli ຍັງບໍ່ໄດ້ເປີດໃຊ້, ກະລຸນາເປີດໃຊ້ ແລະ ສ້າງກ່ອນ'
+ErrBrotliUnsupported: 'ລຸ້ນ OpenResty ທີ່ຕິດຕັ້ງບໍ່ຮອງຮັບການຕັ້ງຄ່າ brotli ແບບຈັດການ'
+ErrModuleBuildUnsupported: 'ລຸ້ນ OpenResty ນີ້ບໍ່ສາມາດສ້າງໂມດູນໄດ້, ກະລຸນາອັບເກຣດກ່ອນ'
ErrImageNotExist: 'ບໍ່ພົບຮູບພາບ runtime: {{ .name }}'
ErrProxyIsUsed: 'ຕົວຈັດການການໂຫຼດ (Load balancer) ຖືກໃຊ້ງານໂດຍ reverse proxy'
ErrSSLValid: 'ໄຟລ໌ໃບຮັບຮອງບໍ່ຖືກຕ້ອງ'
diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml
index cc9ac8aab68a..a7f0b640edd6 100644
--- a/agent/i18n/lang/ms.yaml
+++ b/agent/i18n/lang/ms.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: 'Format nama domain {{ .name }} tidak betul'
ErrDefaultAlias: 'lalai ialah kod simpanan, sila gunakan kod lain'
ErrParentWebsite: 'Anda perlu memadamkan subtapak {{ .name }} dahulu'
ErrBuildDirNotFound: 'Direktori binaan tidak wujud'
+ErrBrotliDisabled: 'Modul brotli tidak diaktifkan, aktifkan dan bina dahulu'
+ErrBrotliUnsupported: 'Versi OpenResty yang dipasang tidak menyokong tetapan brotli terurus'
+ErrModuleBuildUnsupported: 'Versi OpenResty ini tidak boleh membina modul, naik taraf dahulu'
ErrImageNotExist: 'Imej persekitaran operasi {{ .name }} tidak wujud, sila edit semula persekitaran pengendalian'
ErrProxyIsUsed: 'Pengimbang beban telah digunakan oleh pengganti terbalik, tidak boleh dipadamkan'
ErrSSLValid: 'Fail sijil bermasalah, sila periksa status sijil'
diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml
index 24deaf4eeecd..1ed210831f43 100644
--- a/agent/i18n/lang/pt-BR.yaml
+++ b/agent/i18n/lang/pt-BR.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: 'o formato do nome de domínio {{ .name }} está incorreto'
ErrDefaultAlias: 'padrão é um código reservado, use outro código'
ErrParentWebsite: 'Você precisa excluir o subsite {{ .name }} primeiro'
ErrBuildDirNotFound: 'O diretório de compilação não existe'
+ErrBrotliDisabled: 'O módulo brotli não está ativado, ative-o e compile-o primeiro'
+ErrBrotliUnsupported: 'A versão instalada do OpenResty não suporta configurações brotli gerenciadas'
+ErrModuleBuildUnsupported: 'Esta versão do OpenResty não pode compilar módulos, atualize-a primeiro'
ErrImageNotExist: 'A imagem do ambiente operacional {{ .name }} não existe, edite novamente o ambiente operacional'
ErrProxyIsUsed: 'Balanceamento de carga foi usado por proxy reverso, não pode ser excluído'
ErrSSLValid: 'O arquivo do certificado está anormal, verifique o status do certificado'
diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml
index b9cabd88dfb8..2dafe675100c 100644
--- a/agent/i18n/lang/ru.yaml
+++ b/agent/i18n/lang/ru.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: 'Неверный формат доменного имени {{
ErrDefaultAlias: 'по умолчанию зарезервирован код, используйте другой код'
ErrParentWebsite: 'Сначала вам необходимо удалить дочерний сайт {{ .name }}'
ErrBuildDirNotFound: 'Каталог сборки не существует'
+ErrBrotliDisabled: 'Модуль brotli не включён, сначала включите и соберите его'
+ErrBrotliUnsupported: 'Установленная версия OpenResty не поддерживает управляемые настройки brotli'
+ErrModuleBuildUnsupported: 'Эта версия OpenResty не может собирать модули, сначала обновите её'
ErrImageNotExist: 'Образ операционной среды {{ .name }} не существует, пожалуйста, отредактируйте операционную среду заново'
ErrProxyIsUsed: 'Балансировка нагрузки используется обратным прокси, невозможно удалить'
ErrSSLValid: 'Файл сертификата аномален, проверьте статус сертификата'
diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml
index c71d97454e94..1d041bda47dd 100644
--- a/agent/i18n/lang/tr.yaml
+++ b/agent/i18n/lang/tr.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: '{{ .name }} alan adı formatı yanlış'
ErrDefaultAlias: 'default ayrılmış bir kod, lütfen başka bir kod kullanın'
ErrParentWebsite: 'Önce {{ .name }} alt sitesini silmeniz gerekiyor'
ErrBuildDirNotFound: 'Yapı dizini mevcut değil'
+ErrBrotliDisabled: 'brotli modülü etkin değil, önce etkinleştirin ve derleyin'
+ErrBrotliUnsupported: 'Kurulu OpenResty sürümü yönetilen brotli ayarlarını desteklemiyor'
+ErrModuleBuildUnsupported: 'Bu OpenResty sürümü modül derleyemez, önce yükseltin'
ErrImageNotExist: 'İşletim ortamı {{ .name }} image mevcut değil, lütfen işletim ortamını yeniden düzenleyin'
ErrProxyIsUsed: 'Yük dengeleme ters proxy tarafından kullanıldı, silinemez'
ErrSSLValid: 'Sertifika dosyası anormal, lütfen sertifika durumunu kontrol edin'
diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml
index 4cb9952d1097..d82340ff6b82 100644
--- a/agent/i18n/lang/zh-Hant.yaml
+++ b/agent/i18n/lang/zh-Hant.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: '{{ .name }} 網域格式不正確'
ErrDefaultAlias: 'default 為保留代號,請使用其他代號'
ErrParentWebsite: '需要先移除子網站{{ .name }}'
ErrBuildDirNotFound: '建置目錄不存在'
+ErrBrotliDisabled: 'brotli 模組未啟用,請先啟用並建置'
+ErrBrotliUnsupported: '目前 OpenResty 版本不支援託管 brotli 設定'
+ErrModuleBuildUnsupported: '目前 OpenResty 版本無法建置模組,請先升級'
ErrImageNotExist: '執行環境{{ .name }} 映像不存在,請重新編輯執行環境'
ErrProxyIsUsed: '負載均衡已被反向代理使用,無法刪除'
ErrSSLValid: '憑證檔案異常,請檢查憑證狀態!'
diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml
index 77ec11697cac..4b5f6fb15a3f 100644
--- a/agent/i18n/lang/zh.yaml
+++ b/agent/i18n/lang/zh.yaml
@@ -215,6 +215,9 @@ ErrDomainFormat: "{{ .name }} 域名格式不正确"
ErrDefaultAlias: "default 为保留代号,请使用其他代号"
ErrParentWebsite: "需要先删除子网站 {{ .name }}"
ErrBuildDirNotFound: "构建目录不存在"
+ErrBrotliDisabled: "brotli 模块未启用,请先启用并构建"
+ErrBrotliUnsupported: "当前 OpenResty 版本不支持托管 brotli 配置"
+ErrModuleBuildUnsupported: "当前 OpenResty 版本无法构建模块,请先升级"
ErrImageNotExist: "运行环境 {{ .name }} 镜像不存在,请重新编辑运行环境"
ErrProxyIsUsed: "负载均衡已被反向代理使用,无法删除"
ErrSSLValid: '证书文件异常,请检查证书状态!'
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 49baa422f7c9..633d1e219d2d 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -3904,6 +3904,8 @@ const message = {
gzipMinLengthHelper: 'Minimum Compressed File',
gzipCompLevelHelper: 'Compression Rate',
gzipHelper: 'Enable compression for transmission',
+ brotliHelper: 'Enable brotli compression, usually smaller than gzip',
+ brotliCompLevelHelper: 'Brotli compression rate, 0 to 11',
connections: 'Active connections',
accepts: 'Accepts',
handled: 'Handled',
diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts
index 5a45e87b0046..acf994011189 100644
--- a/frontend/src/lang/modules/es-es.ts
+++ b/frontend/src/lang/modules/es-es.ts
@@ -3951,6 +3951,8 @@ const message = {
gzipMinLengthHelper: 'Tamaño mínimo para comprimir',
gzipCompLevelHelper: 'Nivel de compresión',
gzipHelper: 'Habilitar compresión para transmisión',
+ brotliHelper: 'Habilitar compresión brotli, normalmente menor que gzip',
+ brotliCompLevelHelper: 'Tasa de compresión brotli, de 0 a 11',
connections: 'Conexiones activas',
accepts: 'Aceptadas',
handled: 'Gestionadas',
diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts
index f5dd35614745..5b488f6374bb 100644
--- a/frontend/src/lang/modules/fa.ts
+++ b/frontend/src/lang/modules/fa.ts
@@ -3871,6 +3871,8 @@ const message = {
gzipMinLengthHelper: 'حداقل فایل فشرده',
gzipCompLevelHelper: 'نرخ فشردهسازی',
gzipHelper: 'فعالسازی فشردهسازی برای انتقال',
+ brotliHelper: 'فعالسازی فشردهسازی brotli، معمولاً کوچکتر از gzip',
+ brotliCompLevelHelper: 'نرخ فشردهسازی brotli، از 0 تا 11',
connections: 'اتصالهای فعال',
accepts: 'پذیرشها',
handled: 'مدیریت شده',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index e7cacbc98fa3..c02edc3072ab 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -3894,6 +3894,8 @@ const message = {
gzipMinLengthHelper: '最小圧縮ファイル',
gzipCompLevelHelper: '圧縮率',
gzipHelper: '伝送の圧縮を有効にします',
+ brotliHelper: 'brotli 圧縮を有効にします。通常 gzip より小さくなります',
+ brotliCompLevelHelper: 'brotli 圧縮率、0 から 11',
connections: 'アクティブな接続',
accepts: '受け入れます',
handled: '処理',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index 716aa7bb72dd..07d3bf83f414 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -3823,6 +3823,8 @@ const message = {
gzipMinLengthHelper: '최소 압축 파일 크기',
gzipCompLevelHelper: '압축률',
gzipHelper: '전송을 위한 압축 활성화',
+ brotliHelper: 'brotli 압축 활성화, 일반적으로 gzip보다 작습니다',
+ brotliCompLevelHelper: 'brotli 압축률, 0에서 11까지',
connections: '활성 연결',
accepts: '수락',
handled: '처리됨',
diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts
index ab9080d57ec8..5fee100ccb87 100644
--- a/frontend/src/lang/modules/lo.ts
+++ b/frontend/src/lang/modules/lo.ts
@@ -3798,6 +3798,8 @@ const message = {
gzipMinLengthHelper: 'ຂະໜາດໄຟລ໌ຕ່ຳສຸດທີ່ຈະບີບອັດ',
gzipCompLevelHelper: 'ອັດຕາການບີບອັດ',
gzipHelper: 'ເປີດໃຊ້ການບີບອັດໃນການຮັບສົ່ງຂໍ້ມູນ',
+ brotliHelper: 'ເປີດໃຊ້ການບີບອັດ brotli, ປົກກະຕິນ້ອຍກວ່າ gzip',
+ brotliCompLevelHelper: 'ອັດຕາການບີບອັດ brotli, 0 ຫາ 11',
connections: 'ການເຊື່ອມຕໍ່ທີ່ກຳລັງເຮັດວຽກ',
accepts: 'ຍອມຮັບແລ້ວ',
handled: 'ຈັດການແລ້ວ',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index 0ab85801c796..827cd6aa0138 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -3966,6 +3966,8 @@ const message = {
gzipMinLengthHelper: 'Saiz minimum fail untuk pemampatan',
gzipCompLevelHelper: 'Kadar mampatan',
gzipHelper: 'Aktifkan pemampatan untuk penghantaran',
+ brotliHelper: 'Aktifkan pemampatan brotli, biasanya lebih kecil daripada gzip',
+ brotliCompLevelHelper: 'Kadar pemampatan brotli, 0 hingga 11',
connections: 'Sambungan aktif',
accepts: 'Diterima',
handled: 'Diuruskan',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index 33ba342f4709..bbac49543fb9 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -3986,6 +3986,8 @@ const message = {
gzipMinLengthHelper: 'Tamanho mínimo para compressão',
gzipCompLevelHelper: 'Nível de compressão',
gzipHelper: 'Ativar compressão na transmissão',
+ brotliHelper: 'Ativar compressão brotli, geralmente menor que gzip',
+ brotliCompLevelHelper: 'Taxa de compressão brotli, de 0 a 11',
connections: 'Conexões ativas',
accepts: 'Accepts',
handled: 'Handled',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index 48e2c45b4d7c..3fde397a35e0 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -3954,6 +3954,8 @@ const message = {
gzipMinLengthHelper: 'Минимальный размер сжатого файла',
gzipCompLevelHelper: 'Степень сжатия',
gzipHelper: 'Включить сжатие для передачи',
+ brotliHelper: 'Включить сжатие brotli, обычно меньше, чем gzip',
+ brotliCompLevelHelper: 'Степень сжатия brotli, от 0 до 11',
connections: 'Активные соединения',
accepts: 'Принято',
handled: 'Обработано',
diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts
index bbc41c439d7f..796ec75de3c4 100644
--- a/frontend/src/lang/modules/tr.ts
+++ b/frontend/src/lang/modules/tr.ts
@@ -3969,6 +3969,8 @@ const message = {
gzipMinLengthHelper: 'Minimum Sıkıştırılmış Dosya',
gzipCompLevelHelper: 'Sıkıştırma Oranı',
gzipHelper: 'İletim için sıkıştırmayı etkinleştir',
+ brotliHelper: 'brotli sıkıştırmayı etkinleştir, genellikle gzip ile karşılaştırıldığında daha küçüktür',
+ brotliCompLevelHelper: 'Brotli sıkıştırma oranı, 0 ile 11 arası',
connections: 'Aktif bağlantılar',
accepts: 'Kabul edilenler',
handled: 'İşlenenler',
diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts
index 3e571d962e03..7848724d3ece 100644
--- a/frontend/src/lang/modules/zh-Hant.ts
+++ b/frontend/src/lang/modules/zh-Hant.ts
@@ -3648,6 +3648,8 @@ const message = {
gzipMinLengthHelper: '最小壓縮檔案',
gzipCompLevelHelper: '壓縮率',
gzipHelper: '是否開啟壓縮傳輸',
+ brotliHelper: '開啟 brotli 壓縮,通常比 gzip 體積更小',
+ brotliCompLevelHelper: 'brotli 壓縮率,取值 0 到 11',
connections: '活動連接(Active connections)',
accepts: '總連接次數(accepts)',
handled: '總握手次數(handled)',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index 7ce725430074..4b7fd24485c6 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -3693,6 +3693,8 @@ const message = {
gzipMinLengthHelper: '最小压缩文件',
gzipCompLevelHelper: '压缩率',
gzipHelper: '是否开启压缩传输',
+ brotliHelper: '开启 brotli 压缩,通常比 gzip 体积更小',
+ brotliCompLevelHelper: 'brotli 压缩率,取值 0 到 11',
connections: '活动连接(Active connections)',
accepts: '总连接次数(accepts)',
handled: '总握手次数(handled)',
diff --git a/frontend/src/views/website/website/nginx/performance/index.vue b/frontend/src/views/website/website/nginx/performance/index.vue
index 62719a6311dc..46ebeb9667f0 100644
--- a/frontend/src/views/website/website/nginx/performance/index.vue
+++ b/frontend/src/views/website/website/nginx/performance/index.vue
@@ -13,13 +13,13 @@
- K
+ {{ unitLabel('client_header_buffer_size', 'k') }}
{{ $t('nginx.clientHeaderBufferSizeHelper') }}
- MB
+ {{ unitLabel('client_max_body_size', 'm') }}
{{ $t('nginx.clientMaxBodySizeHelper') }}
@@ -38,7 +38,7 @@
- KB
+ {{ unitLabel('gzip_min_length', 'k') }}
{{ $t('nginx.gzipMinLengthHelper') }}
@@ -48,6 +48,29 @@
+
+
+
+
+
+
+
+ {{ $t('nginx.brotliHelper') }}
+
+
+
+ {{ unitLabel('brotli_min_length', 'k') }}
+
+ {{ $t('nginx.gzipMinLengthHelper') }}
+
+
+
+
+
+ {{ $t('nginx.brotliCompLevelHelper') }}
+
+
+
{{ $t('commons.button.save') }}
@@ -58,7 +81,7 @@