fix(docker): upgrade persisted bundled nginx-ui.conf safely (#1696)

* test(self_check): add fixed-default bundled nginx-ui.conf fixture

* test(self_check): add unfixed-default bundled nginx-ui.conf fixture

* test(self_check): add customized and partial-fix fixtures

* feat(self_check): add error codes for bundled nginx-ui.conf upgrade

* feat(self_check): add CheckBundledNginxUIConf

* feat(self_check): add idempotent applyBundledConfPatch

* feat(self_check): add transactional patch-on-disk with backup restore

* feat(self_check): wire FixBundledNginxUIConf with verify+reload

* feat(self_check): register bundled nginx-ui.conf WS-fix task

* test(docker): add init-config bats fixtures

* feat(docker): hash-whitelist sync for bundled nginx-ui.conf

* feat(docker): seed nginx-ui.conf known-hashes list

* chore(docker): add maintainer script for nginx-ui.conf hash list

* ci(docker): add bats + hash-consistency workflow for init-config.sh

* feat(docker): ship nginx-ui.conf known-hashes inside the image

* feat(self-check): banner button shows Fix when all failures are fixable

* docs: add docker websocket fix guide (en)

* docs: add docker websocket fix guide (zh_CN, zh_TW)

* docs: link docker-websocket-fix page in all locale sidebars

* docs(readme): link docker websocket fix guide

* docs(readme): link docker websocket fix guide (translations)

* fix(self_check): WS-fix check independent of NGINX_UI_IGNORE_DOCKER_SOCKET

* fix(docker): respect bundled nginx host mode

Keep bundled nginx-ui.conf self-checks aligned with Docker host mode and prevent config sync failures from blocking startup.

* fix(docker): tighten bundled conf review fixes

Co-authored-by: Jacky <me@jackyu.cn>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jacky <me@jackyu.cn>
This commit is contained in:
Hintay
2026-05-24 10:48:19 +09:00
committed by GitHub
parent 69cfa82b1d
commit f6992d8789
32 changed files with 1125 additions and 7 deletions
@@ -0,0 +1,62 @@
name: docker init-config tests
on:
push:
paths:
- 'resources/docker/init-config.sh'
- 'resources/docker/nginx-ui.conf'
- 'resources/docker/nginx-ui.conf.known-hashes'
- 'resources/docker/scripts/**'
- 'resources/docker/tests/**'
- 'internal/helper/docker.go'
- 'internal/self_check/**'
- 'Dockerfile'
- 'demo.Dockerfile'
- '.github/workflows/docker-init-config-test.yml'
pull_request:
paths:
- 'resources/docker/init-config.sh'
- 'resources/docker/nginx-ui.conf'
- 'resources/docker/nginx-ui.conf.known-hashes'
- 'resources/docker/scripts/**'
- 'resources/docker/tests/**'
- 'internal/helper/docker.go'
- 'internal/self_check/**'
- 'Dockerfile'
- 'demo.Dockerfile'
- '.github/workflows/docker-init-config-test.yml'
jobs:
hash-consistency:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Verify last known-hash equals current template
run: |
expected=$(sha256sum resources/docker/nginx-ui.conf | awk '{print $1}')
latest=$(grep -vE '^[[:space:]]*(#|$)' resources/docker/nginx-ui.conf.known-hashes \
| awk '{print $1}' | tail -n1)
if [ "$expected" != "$latest" ]; then
echo "::error::nginx-ui.conf changed without updating known-hashes file."
echo "Run: bash resources/docker/scripts/update-nginx-ui-conf-hash.sh"
exit 1
fi
bats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install bats
run: sudo apt-get update && sudo apt-get install -y bats
- name: Run bats tests
run: bats resources/docker/tests/
go-self-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run bundled config self-check tests
run: go test -tags=unembed ./internal/helper ./internal/self_check -count=1
+1
View File
@@ -59,6 +59,7 @@ RUN echo 'longrun' > /etc/s6-overlay/s6-rc.d/nginx-ui/type && \
# copy nginx config
COPY resources/docker/nginx.conf /usr/local/etc/nginx/nginx.conf
COPY resources/docker/nginx-ui.conf /usr/local/etc/nginx/conf.d/nginx-ui.conf
COPY resources/docker/nginx-ui.conf.known-hashes /usr/local/share/nginx-ui/nginx-ui.conf.known-hashes
# copy nginx-ui executable binary
COPY nginx-ui-$TARGETOS-$TARGETARCH$TARGETVARIANT/nginx-ui /usr/local/bin/nginx-ui
+1
View File
@@ -246,6 +246,7 @@ you can easily make the switch.
##### Note
1. When using this container for the first time, ensure that the volume mapped to /etc/nginx is empty.
2. If you want to host static files, you can map directories to container.
3. If you are upgrading from an older image, see the [Docker WebSocket fix guide](https://nginxui.com/guide/docker-websocket-fix.html) for required `conf.d/nginx-ui.conf` updates.
<details>
<summary><b>Deploy with Docker</b></summary>
@@ -11,7 +11,7 @@ const props = defineProps<{
const router = useRouter()
const selfCheckStore = useSelfCheckStore()
const { hasError, loading } = storeToRefs(selfCheckStore)
const { hasError, loading, data } = storeToRefs(selfCheckStore)
const alertEl = useTemplateRef('alertEl')
const { width: alertWidth } = useElementSize(alertEl)
@@ -26,6 +26,15 @@ const iconRightPosition = computed(() => {
return props.userWrapperWidth ? `${props.userWrapperWidth + 50}px` : '50px'
})
const allFailingAreFixable = computed(() => {
const failing = data.value?.filter(r => r.status === 'error') ?? []
return failing.length > 0 && failing.every(r => r.fixable)
})
const actionLabel = computed(() =>
allFailingAreFixable.value ? $gettext('Fix') : $gettext('Check'),
)
onMounted(() => {
selfCheckStore.check()
})
@@ -37,7 +46,7 @@ onMounted(() => {
<AAlert type="error" show-icon :message="$gettext('Self check failed, Nginx UI may not work properly')">
<template #action>
<AButton class="ml-4" size="small" danger @click="router.push('/system/self_check')">
{{ $gettext('Check') }}
{{ actionLabel }}
</AButton>
</template>
</AAlert>
@@ -61,7 +70,7 @@ onMounted(() => {
</div>
<div>
<AButton size="small" danger @click="router.push('/system/self_check')">
{{ $gettext('Check') }}
{{ actionLabel }}
</AButton>
</div>
</div>
+5
View File
@@ -22,4 +22,9 @@ export default {
40418: () => $gettext('Error log path not exist'),
40419: () => $gettext('Conf.d directory not exists'),
40420: () => $gettext('GeoLite2 database not found at {0}. Log indexing requires GeoLite2 database for geographic IP analysis'),
40421: () => $gettext('Bundled nginx-ui.conf is missing the WebSocket reverse-proxy fix'),
50007: () => $gettext('Failed to read bundled nginx-ui.conf: {0}'),
50008: () => $gettext('Patched nginx-ui.conf is invalid: {0}'),
50009: () => $gettext('Nginx reload after fix failed: {0}'),
50010: () => $gettext('Failed to restore nginx-ui.conf from backup: {0}'),
}
+1
View File
@@ -79,6 +79,7 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
text: 'Appendix',
collapsed: false,
items: [
{ text: 'Docker WebSocket Fix', link: '/guide/docker-websocket-fix' },
{ text: 'Nginx Proxy Example', link: '/guide/nginx-proxy-example' },
{ text: 'Reset Password', link: '/guide/reset-password' },
{ text: 'License', link: '/guide/license' }
+1
View File
@@ -84,6 +84,7 @@ export const zhCNConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
text: '附录',
collapsed: false,
items: [
{ text: 'Docker WebSocket 修复', link: '/zh_CN/guide/docker-websocket-fix' },
{ text: 'Nginx 代理示例', link: '/zh_CN/guide/nginx-proxy-example' },
{ text: '重置密码', link: '/zh_CN/guide/reset-password' },
{ text: '开源协议', link: '/zh_CN/guide/license' }
+1
View File
@@ -84,6 +84,7 @@ export const zhTWConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
text: '附錄',
collapsed: false,
items: [
{ text: 'Docker WebSocket 修復', link: '/zh_TW/guide/docker-websocket-fix' },
{ text: 'Nginx 代理示例', link: '/zh_TW/guide/nginx-proxy-example' },
{ text: '重置密碼', link: '/zh_TW/guide/reset-password' },
{ text: '開源協議', link: '/zh_TW/guide/license' }
+57
View File
@@ -0,0 +1,57 @@
# WebSocket fix for persisted Docker installations
::: tip Applies to
You persisted `/etc/nginx` as a Docker volume from a Nginx UI version older than
the one that introduced this fix, and Nginx UI is fronted by another reverse proxy
that terminates TLS (host nginx, Cloudflare, Traefik, ...).
:::
## Symptoms
WebSocket connections (terminal, log live tail, ...) fail with origin-mismatch errors.
This happens because the container's internal nginx was overwriting `X-Forwarded-Proto`
with its own `$scheme` (`http`), breaking the same-origin check on HTTPS deployments.
## Automatic fix (recommended)
1. Open **System → Self Check**.
2. Locate **Bundled nginx-ui.conf has WebSocket reverse-proxy fix**.
3. Click **Attempt to fix**. A timestamped `.bak` file is written next to the original.
::: warning If the fix fails
The original file is restored from backup automatically. The error message includes
the backup path. See *Manual fix* below.
:::
## Manual fix
::: code-group
```nginx [Additions at top of conf.d/nginx-ui.conf]
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
```
```diff [Replace inside location /]
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-Proto $forwarded_proto;
+ proxy_set_header X-Forwarded-Host $forwarded_host;
```
:::
After saving, run `docker exec <container> nginx -s reload`.
## Opt-out
::: info
Set `NGINX_UI_PRESERVE_BUNDLED_CONF=true` on the container to disable the
startup-time auto-upgrade. The UI-driven fix remains available regardless.
:::
+56
View File
@@ -0,0 +1,56 @@
# 持久化 Docker 部署的 WebSocket 修复
::: tip 适用范围
你以 Docker 卷的形式持久化了 `/etc/nginx`,且镜像版本早于引入本修复的版本;
同时 Nginx UI 前面还有另一层反向代理(host nginx、Cloudflare、Traefik 等)在
终止 TLS。
:::
## 症状
WebSocket 连接(终端、日志实时跟踪等)报同源校验失败。原因是容器内部 nginx
`X-Forwarded-Proto` 覆盖为自己的 `$scheme``http`),导致 HTTPS 部署下
同源校验失效。
## 自动修复(推荐)
1. 打开 **系统 → 自检**
2. 找到 **Bundled nginx-ui.conf 已包含 WebSocket 反代修复**
3. 点击 **尝试修复**。原文件旁会生成带时间戳的 `.bak` 备份。
::: warning 修复失败时
原文件会自动从备份恢复。错误信息中包含备份路径。请参考下文「手动修复」。
:::
## 手动修复
::: code-group
```nginx [conf.d/nginx-ui.conf 顶部新增]
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
```
```diff [location / 内部替换]
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-Proto $forwarded_proto;
+ proxy_set_header X-Forwarded-Host $forwarded_host;
```
:::
保存后执行 `docker exec <container> nginx -s reload`。
## 禁用自动升级
::: info
在容器上设置 `NGINX_UI_PRESERVE_BUNDLED_CONF=true` 可关闭启动期的自动升级;
UI 内的修复入口仍然可用。
:::
+56
View File
@@ -0,0 +1,56 @@
# 持久化 Docker 部署的 WebSocket 修復
::: tip 適用範圍
你以 Docker 卷的形式持久化了 `/etc/nginx`,且鏡像版本早於引入本修復的版本;
同時 Nginx UI 前面還有另一層反向代理(host nginx、Cloudflare、Traefik 等)在
終止 TLS。
:::
## 症狀
WebSocket 連線(終端、日誌即時追蹤等)報同源驗證失敗。原因是容器內部 nginx
`X-Forwarded-Proto` 覆寫為自己的 `$scheme``http`),導致 HTTPS 部署下
同源驗證失效。
## 自動修復(推薦)
1. 開啟 **系統 → 自檢**
2. 找到 **Bundled nginx-ui.conf 已包含 WebSocket 反代修復**
3. 點擊 **嘗試修復**。原檔案旁會產生帶時間戳的 `.bak` 備份。
::: warning 修復失敗時
原檔案會自動從備份還原。錯誤訊息中包含備份路徑。請參考下文「手動修復」。
:::
## 手動修復
::: code-group
```nginx [conf.d/nginx-ui.conf 頂部新增]
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
```
```diff [location / 內部替換]
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-Proto $forwarded_proto;
+ proxy_set_header X-Forwarded-Host $forwarded_host;
```
:::
儲存後執行 `docker exec <container> nginx -s reload`。
## 停用自動升級
::: info
在容器上設定 `NGINX_UI_PRESERVE_BUNDLED_CONF=true` 即可關閉啟動期自動升級;
UI 內的修復入口仍可使用。
:::
+17
View File
@@ -6,6 +6,23 @@ import (
"github.com/spf13/cast"
)
// IsOfficialDockerImage returns true when running inside the official Nginx UI
// docker image. Unlike InNginxUIOfficialDocker, this does NOT honour
// NGINX_UI_IGNORE_DOCKER_SOCKET — that opt-out is specific to features that
// require the docker socket (OTA upgrade), and should not suppress checks
// that simply happen to be docker-only (e.g. bundled config sync).
func IsOfficialDockerImage() bool {
return cast.ToBool(os.Getenv("NGINX_UI_OFFICIAL_DOCKER"))
}
// ShouldManageBundledNginx returns true when the official Docker image owns
// the bundled Nginx configuration. Host mode disables that ownership even when
// the process is still running inside the official image.
func ShouldManageBundledNginx() bool {
return IsOfficialDockerImage() &&
!cast.ToBool(os.Getenv("NGINX_UI_DISABLE_BUNDLED_NGINX"))
}
func InNginxUIOfficialDocker() bool {
return cast.ToBool(os.Getenv("NGINX_UI_OFFICIAL_DOCKER")) &&
!cast.ToBool(os.Getenv("NGINX_UI_IGNORE_DOCKER_SOCKET"))
@@ -0,0 +1,192 @@
package self_check
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/uozi-tech/cosy"
)
// bundledNginxUIConfPath is a var so tests can redirect it.
var bundledNginxUIConfPath = "/etc/nginx/conf.d/nginx-ui.conf"
// Markers indicating the WS reverse-proxy fix is present.
var (
reMapForwardedProto = regexp.MustCompile(`(?m)^\s*map\s+\$http_x_forwarded_proto\s+\$forwarded_proto\b`)
reMapForwardedHost = regexp.MustCompile(`(?m)^\s*map\s+\$http_x_forwarded_host\s+\$forwarded_host\b`)
reHeaderForwardedProto = regexp.MustCompile(`(?m)^\s*proxy_set_header\s+X-Forwarded-Proto\s+\$forwarded_proto\b`)
reHeaderForwardedHost = regexp.MustCompile(`(?m)^\s*proxy_set_header\s+X-Forwarded-Host\s+\$forwarded_host\b`)
)
// CheckBundledNginxUIConf returns nil if the bundled conf has all expected fix markers,
// or ErrBundledNginxUIConfOutdated if any marker is missing.
// Outside of managed bundled Nginx Docker mode, returns nil unconditionally.
func CheckBundledNginxUIConf() error {
if !helper.ShouldManageBundledNginx() {
return nil
}
data, err := os.ReadFile(bundledNginxUIConfPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil // delegated to other tasks
}
return cosy.WrapErrorWithParams(ErrFailedToReadBundledNginxUIConf, err.Error())
}
if !hasBundledConfWebSocketFix(data) {
return ErrBundledNginxUIConfOutdated
}
return nil
}
func hasBundledConfWebSocketFix(data []byte) bool {
return reMapForwardedProto.Match(data) &&
reMapForwardedHost.Match(data) &&
reHeaderForwardedProto.Match(data) &&
reHeaderForwardedHost.Match(data)
}
// Patterns to rewrite when an old default is still in place.
var (
reHeaderForwardedProtoLegacy = regexp.MustCompile(`(?m)^(\s*proxy_set_header\s+X-Forwarded-Proto\s+)\$scheme(\s*;)`)
reHeaderForwardedHostLegacy = regexp.MustCompile(`(?m)^(\s*proxy_set_header\s+X-Forwarded-Host\s+)\$http_host(\s*;)`)
)
const mapForwardedProtoBlock = `# Preserve X-Forwarded-Proto from an outer reverse proxy (e.g. host nginx
# terminating TLS in front of this container). Only fall back to $scheme
# when the inbound request did not carry the header.
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
`
const mapForwardedHostBlock = `# Same for X-Forwarded-Host: keep what the outer proxy stamped, otherwise
# use the inbound Host header.
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
`
// applyBundledConfPatch returns a patched copy of in. Idempotent.
func applyBundledConfPatch(in []byte) []byte {
out := in
out = reHeaderForwardedProtoLegacy.ReplaceAll(out, []byte("${1}$$forwarded_proto${2}"))
out = reHeaderForwardedHostLegacy.ReplaceAll(out, []byte("${1}$$forwarded_host${2}"))
var injection strings.Builder
if !reMapForwardedProto.Match(out) {
injection.WriteString(mapForwardedProtoBlock)
}
if !reMapForwardedHost.Match(out) {
injection.WriteString(mapForwardedHostBlock)
}
if injection.Len() > 0 {
out = injectBeforeFirstServer(out, injection.String())
}
return out
}
// reFirstServer matches the first top-level `server {` for map injection.
var reFirstServer = regexp.MustCompile(`(?m)^server\s*\{`)
// injectBeforeFirstServer inserts s before the first top-level `server {`.
// Falls back to prepend if no server block is found.
func injectBeforeFirstServer(in []byte, s string) []byte {
idx := reFirstServer.FindIndex(in)
if idx == nil {
return append([]byte(s), in...)
}
out := make([]byte, 0, len(in)+len(s))
out = append(out, in[:idx[0]]...)
out = append(out, s...)
out = append(out, in[idx[0]:]...)
return out
}
// patchOnDiskWithBackup atomically writes the patched contents to bundledNginxUIConfPath.
// On any failure after the backup file exists, the target is restored from the backup
// and an error wrapping the backup path is returned.
//
// Restore errors are folded into the returned error message; ErrCriticalRecoveryFailed
// is reserved for the verify/reload layer (Phase 3.7), where a failed restore after
// nginx -t rejection is a distinct fault class operators need to triage specifically.
func patchOnDiskWithBackup(orig []byte, bak string) error {
patched := applyBundledConfPatch(orig)
if !hasBundledConfWebSocketFix(patched) {
return cosy.WrapErrorWithParams(ErrFixedConfigInvalid,
"unable to apply all required WebSocket reverse-proxy markers; backup at "+bak)
}
tmp := bundledNginxUIConfPath + ".tmp"
if err := os.WriteFile(tmp, patched, 0o644); err != nil {
_ = restoreFromBackup(bundledNginxUIConfPath, bak)
return cosy.WrapErrorWithParams(ErrFixedConfigInvalid,
"write failed: "+err.Error()+"; restored from "+bak)
}
if err := os.Rename(tmp, bundledNginxUIConfPath); err != nil {
_ = os.Remove(tmp)
_ = restoreFromBackup(bundledNginxUIConfPath, bak)
return cosy.WrapErrorWithParams(ErrFixedConfigInvalid,
"rename failed: "+err.Error()+"; restored from "+bak)
}
return nil
}
// restoreFromBackup copies the contents of bak over target.
func restoreFromBackup(target, bak string) error {
data, err := os.ReadFile(bak)
if err != nil {
return err
}
return os.WriteFile(target, data, 0o644)
}
// FixBundledNginxUIConf is the FixFunc for the bundled nginx-ui.conf upgrade
// self_check task (registered in tasks.go).
// Flow: read -> backup -> patch -> atomic write -> nginx -t -> reload.
// On any failure between backup and verify the file is rolled back; the error
// always includes the backup path.
func FixBundledNginxUIConf() error {
orig, err := os.ReadFile(bundledNginxUIConfPath)
if err != nil {
return cosy.WrapErrorWithParams(ErrFailedToReadBundledNginxUIConf, err.Error())
}
bak := fmt.Sprintf("%s.bak.%s", bundledNginxUIConfPath, time.Now().Format("20060102150405"))
if err := os.WriteFile(bak, orig, 0o644); err != nil {
return cosy.WrapErrorWithParams(ErrFailedToCreateBackup, err.Error())
}
if err := patchOnDiskWithBackup(orig, bak); err != nil {
return err
}
return verifyAndReload(bak)
}
// verifyAndReload runs `nginx -t` and rolls back on failure, then reloads.
// Reload failures do NOT trigger rollback because the file on disk is already valid.
func verifyAndReload(bak string) error {
if out, err := nginx.TestConfig(); err != nil {
if rerr := restoreFromBackup(bundledNginxUIConfPath, bak); rerr != nil {
return cosy.WrapErrorWithParams(ErrCriticalRecoveryFailed,
"validate failed: "+strings.TrimSpace(out)+
"; restore failed: "+rerr.Error()+
"; backup at "+bak)
}
return cosy.WrapErrorWithParams(ErrFixedConfigInvalid,
strings.TrimSpace(out)+"; restored from "+bak)
}
if out, err := nginx.Reload(); err != nil {
return cosy.WrapErrorWithParams(ErrReloadFailed,
strings.TrimSpace(out)+"; backup at "+bak)
}
return nil
}
@@ -0,0 +1,217 @@
package self_check
import (
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uozi-tech/cosy"
)
// withFixture copies the named fixture from test_cases/bundled/ into a tempdir
// and redirects bundledNginxUIConfPath to it for the duration of the test.
// Also forces InNginxUIOfficialDocker() to true via env var.
func withFixture(t *testing.T, name string) string {
t.Helper()
src := filepath.Join("test_cases", "bundled", name)
data, err := os.ReadFile(src)
require.NoError(t, err, "fixture %s", name)
dir := t.TempDir()
target := filepath.Join(dir, "nginx-ui.conf")
require.NoError(t, os.WriteFile(target, data, 0o644))
orig := bundledNginxUIConfPath
bundledNginxUIConfPath = target
t.Cleanup(func() { bundledNginxUIConfPath = orig })
// Force the docker guard on.
t.Setenv("NGINX_UI_OFFICIAL_DOCKER", "true")
t.Setenv("NGINX_UI_IGNORE_DOCKER_SOCKET", "")
return target
}
func TestCheckBundledNginxUIConf(t *testing.T) {
cases := []struct {
name string
fixture string
wantOK bool
wantErr int32 // cosy error code; ignored if wantOK
}{
{"unfixed default", "unfixed-default.conf", false, 40421},
{"fixed default", "fixed-default.conf", true, 0},
{"customized unfixed", "customized-unfixed.conf", false, 40421},
{"customized fixed", "customized-fixed.conf", true, 0},
{"half-fixed (one map missing)", "maps-only-half.conf", false, 40421},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
withFixture(t, tc.fixture)
err := CheckBundledNginxUIConf()
if tc.wantOK {
assert.NoError(t, err)
return
}
var cErr *cosy.Error
require.True(t, errors.As(err, &cErr), "want cosy.Error, got %T", err)
assert.Equal(t, tc.wantErr, cErr.Code)
})
}
}
func TestCheckBundledNginxUIConf_MissingFile(t *testing.T) {
dir := t.TempDir()
orig := bundledNginxUIConfPath
bundledNginxUIConfPath = filepath.Join(dir, "does-not-exist.conf")
t.Cleanup(func() { bundledNginxUIConfPath = orig })
t.Setenv("NGINX_UI_OFFICIAL_DOCKER", "true")
t.Setenv("NGINX_UI_IGNORE_DOCKER_SOCKET", "")
// Missing file is delegated to other tasks; CheckFunc returns nil.
assert.NoError(t, CheckBundledNginxUIConf())
}
func TestCheckBundledNginxUIConf_NotInDocker(t *testing.T) {
t.Setenv("NGINX_UI_OFFICIAL_DOCKER", "")
// Even with a missing path, no error when not in docker.
orig := bundledNginxUIConfPath
bundledNginxUIConfPath = "/nonexistent/path"
t.Cleanup(func() { bundledNginxUIConfPath = orig })
assert.NoError(t, CheckBundledNginxUIConf())
}
func TestCheckBundledNginxUIConf_SkipsWhenBundledNginxDisabled(t *testing.T) {
t.Setenv("NGINX_UI_OFFICIAL_DOCKER", "true")
t.Setenv("NGINX_UI_DISABLE_BUNDLED_NGINX", "true")
dir := t.TempDir()
target := filepath.Join(dir, "nginx-ui.conf")
src := filepath.Join("test_cases", "bundled", "unfixed-default.conf")
data, err := os.ReadFile(src)
require.NoError(t, err)
require.NoError(t, os.WriteFile(target, data, 0o644))
orig := bundledNginxUIConfPath
bundledNginxUIConfPath = target
t.Cleanup(func() { bundledNginxUIConfPath = orig })
assert.NoError(t, CheckBundledNginxUIConf())
}
func TestCheckBundledNginxUIConf_RunsEvenWithDockerSocketIgnored(t *testing.T) {
// IGNORE_DOCKER_SOCKET should NOT suppress this check — it's only meant
// to opt out of the docker-socket feature, not all docker-only checks.
t.Setenv("NGINX_UI_OFFICIAL_DOCKER", "true")
t.Setenv("NGINX_UI_IGNORE_DOCKER_SOCKET", "true")
dir := t.TempDir()
target := filepath.Join(dir, "nginx-ui.conf")
src := filepath.Join("test_cases", "bundled", "unfixed-default.conf")
data, err := os.ReadFile(src)
require.NoError(t, err)
require.NoError(t, os.WriteFile(target, data, 0o644))
orig := bundledNginxUIConfPath
bundledNginxUIConfPath = target
t.Cleanup(func() { bundledNginxUIConfPath = orig })
err = CheckBundledNginxUIConf()
var cErr *cosy.Error
require.True(t, errors.As(err, &cErr))
assert.Equal(t, int32(40421), cErr.Code)
}
func TestApplyBundledConfPatch_Idempotent(t *testing.T) {
fixed, err := os.ReadFile(filepath.Join("test_cases", "bundled", "fixed-default.conf"))
require.NoError(t, err)
assert.Equal(t, fixed, applyBundledConfPatch(fixed),
"already-fixed input must be byte-equal output")
}
func TestApplyBundledConfPatch_UpgradesUnfixed(t *testing.T) {
in, err := os.ReadFile(filepath.Join("test_cases", "bundled", "unfixed-default.conf"))
require.NoError(t, err)
out := applyBundledConfPatch(in)
assert.True(t, reMapForwardedProto.Match(out), "must inject forwarded_proto map")
assert.True(t, reMapForwardedHost.Match(out), "must inject forwarded_host map")
assert.True(t, reHeaderForwardedProto.Match(out), "must rewrite X-Forwarded-Proto to $forwarded_proto")
assert.True(t, reHeaderForwardedHost.Match(out), "must rewrite X-Forwarded-Host to $forwarded_host")
}
func TestApplyBundledConfPatch_PreservesCustomization(t *testing.T) {
in, err := os.ReadFile(filepath.Join("test_cases", "bundled", "customized-unfixed.conf"))
require.NoError(t, err)
out := applyBundledConfPatch(in)
assert.Contains(t, string(out), "client_max_body_size 256M",
"user customization must survive")
assert.Contains(t, string(out), "server_name nginx-ui.example.com",
"user customization must survive")
assert.True(t, reHeaderForwardedProto.Match(out))
assert.True(t, reHeaderForwardedHost.Match(out))
}
func TestApplyBundledConfPatch_HalfFixedFillsOnlyMissing(t *testing.T) {
in, err := os.ReadFile(filepath.Join("test_cases", "bundled", "maps-only-half.conf"))
require.NoError(t, err)
out := applyBundledConfPatch(in)
// Both maps now present; should not duplicate the existing one.
assert.Equal(t, 1, len(reMapForwardedProto.FindAll(out, -1)),
"forwarded_proto map must appear exactly once")
assert.Equal(t, 1, len(reMapForwardedHost.FindAll(out, -1)))
}
func TestInjectBeforeFirstServer_FallbackToPrepend(t *testing.T) {
in := []byte("# only comments, no server block\n")
out := injectBeforeFirstServer(in, "INJECTED\n")
assert.Equal(t, "INJECTED\n# only comments, no server block\n", string(out))
}
func TestPatchOnDiskWithBackup_RewritesAndBacksUp(t *testing.T) {
target := withFixture(t, "customized-unfixed.conf")
orig, _ := os.ReadFile(target)
bak := target + ".bak.test"
require.NoError(t, os.WriteFile(bak, orig, 0o644))
require.NoError(t, patchOnDiskWithBackup(orig, bak))
got, _ := os.ReadFile(target)
assert.True(t, reHeaderForwardedProto.Match(got), "target must be patched")
assert.True(t, reHeaderForwardedHost.Match(got), "target must be patched")
assert.Contains(t, string(got), "client_max_body_size 256M",
"customization must survive")
bakData, _ := os.ReadFile(bak)
assert.Equal(t, orig, bakData, "backup must contain pre-patch bytes")
}
func TestPatchOnDiskWithBackup_DoesNotMutateTargetOnWriteError(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX-only test: relies on chmod 0o555 to make a directory read-only")
}
target := withFixture(t, "customized-unfixed.conf")
orig, _ := os.ReadFile(target)
bak := target + ".bak.test"
require.NoError(t, os.WriteFile(bak, orig, 0o644))
// Make the parent dir read-only to force os.WriteFile(.tmp) to fail.
dir := filepath.Dir(target)
require.NoError(t, os.Chmod(dir, 0o555))
t.Cleanup(func() { _ = os.Chmod(dir, 0o755) })
err := patchOnDiskWithBackup(orig, bak)
require.Error(t, err)
// Target must be untouched (restore would also fail under the same chmod,
// but since the .tmp write failed first the target file was never modified).
got, _ := os.ReadFile(target)
assert.Equal(t, orig, got, "failed patch must leave target byte-identical to its pre-patch state")
}
+5
View File
@@ -27,4 +27,9 @@ var (
ErrErrorLogPathNotExist = e.New(40418, "Error log path not exist")
ErrConfdNotExists = e.New(40419, "Conf.d directory not exists")
ErrGeoLiteDBNotFound = e.New(40420, "GeoLite2 database not found at {0}. Log indexing requires GeoLite2 database for geographic IP analysis")
ErrFailedToReadBundledNginxUIConf = e.New(50007, "Failed to read bundled nginx-ui.conf: {0}")
ErrBundledNginxUIConfOutdated = e.New(40421, "Bundled nginx-ui.conf is missing the WebSocket reverse-proxy fix")
ErrFixedConfigInvalid = e.New(50008, "Patched nginx-ui.conf is invalid: {0}")
ErrReloadFailed = e.New(50009, "Nginx reload after fix failed: {0}")
ErrCriticalRecoveryFailed = e.New(50010, "Failed to restore nginx-ui.conf from backup: {0}")
)
+14
View File
@@ -153,6 +153,20 @@ func Init() {
})
}
if helper.ShouldManageBundledNginx() {
selfCheckTasks = append(selfCheckTasks, &Task{
Key: "Docker-BundledNginxUIConf-WS",
Name: translation.C("Bundled nginx-ui.conf has WebSocket reverse-proxy fix"),
Description: translation.C(
"When the container is behind an outer reverse proxy that terminates TLS " +
"(e.g. host nginx, Cloudflare), the bundled conf.d/nginx-ui.conf must trust " +
"the inbound X-Forwarded-Proto/Host headers; otherwise WebSocket origin checks fail. " +
"Older deployments that persisted /etc/nginx may still have the unfixed version."),
CheckFunc: CheckBundledNginxUIConf,
FixFunc: FixBundledNginxUIConf,
})
}
if settings.NginxLogSettings.IndexingEnabled {
selfCheckTasks = append(selfCheckTasks, &Task{
Key: "GeoLite-DB",
@@ -0,0 +1,32 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
server {
listen 80;
server_name nginx-ui.example.com;
client_max_body_size 256M;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $forwarded_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
@@ -0,0 +1,22 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name nginx-ui.example.com;
client_max_body_size 256M;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
@@ -0,0 +1,37 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Preserve X-Forwarded-Proto from an outer reverse proxy (e.g. host nginx
# terminating TLS in front of this container). Only fall back to $scheme
# when the inbound request did not carry the header.
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
# Same for X-Forwarded-Host: keep what the outer proxy stamped, otherwise
# use the inbound Host header.
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
server {
listen 80;
server_name localhost; # your domain here
client_max_body_size 128M; # maximum upload size
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $forwarded_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
@@ -0,0 +1,27 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 80;
server_name localhost;
client_max_body_size 128M;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
@@ -0,0 +1,22 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name localhost; # your domain here
client_max_body_size 128M; # maximum upload size
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
+95 -4
View File
@@ -1,6 +1,97 @@
#!/bin/bash
# Initialize /etc/nginx on first boot, and upgrade bundled project files
# (e.g. conf.d/nginx-ui.conf) when they are still byte-equal to a known
# historical official default. Customized files are left alone; the UI
# self_check task surfaces them as a one-click fix.
#
# Override paths via env vars (used by bats tests).
# Sourcing the script with `--testing` defines functions and returns without
# running init_config_main; any other invocation runs main against the caller's
# environment.
set -u # explicit failure handling; -e would skip our recovery paths
if [ "$(ls -A /etc/nginx)" = "" ]; then
cp -rp /usr/local/etc/nginx/* /etc/nginx/
echo "[INFO] Nginx configurations directory initialized"
fi
: "${ETC_NGINX:=/etc/nginx}"
: "${TEMPLATE_DIR:=/usr/local/etc/nginx}"
: "${HASH_FILE:=/usr/local/share/nginx-ui/nginx-ui.conf.known-hashes}"
log() { echo "[$1] init-config: $2"; }
# sync_bundled_file <template> <target> <known_hashes_file>
sync_bundled_file() {
local template="$1" target="$2" hashes="$3"
[ -f "$template" ] || { log WARN "template missing: $template"; return 0; }
[ -f "$hashes" ] || { log WARN "hash list missing: $hashes"; return 0; }
if [ ! -f "$target" ]; then
log INFO "target absent; copying template to $target"
cp -p "$template" "$target"
return $?
fi
if ! command -v sha256sum >/dev/null 2>&1; then
log WARN "sha256sum unavailable; skipping sync of $target"
return 0
fi
local cur_hash tpl_hash
cur_hash="$(sha256sum "$target" | awk '{print $1}')"
tpl_hash="$(sha256sum "$template" | awk '{print $1}')"
if [ "$cur_hash" = "$tpl_hash" ]; then
return 0 # already up-to-date
fi
if grep -vE '^[[:space:]]*(#|$)' "$hashes" | awk '{print $1}' \
| grep -Fxq "$cur_hash"; then
local bak="${target}.bak.$(date +%Y%m%d%H%M%S)"
if ! cp -p "$target" "$bak"; then
log ERROR "backup failed ($target -> $bak); leaving file untouched"
return 1
fi
local tmp="${target}.tmp.$$"
if ! cp -p "$template" "$tmp" || ! mv -f "$tmp" "$target"; then
log ERROR "write failed; restoring from $bak"
cp -p "$bak" "$target" 2>/dev/null
rm -f "$tmp" 2>/dev/null
return 1
fi
log INFO "Synced $target from bundled template (old saved as $bak)"
else
log INFO "Skipping $target: customized (hash $cur_hash). See UI self-check."
fi
}
init_config_main() {
# Early exit: host_via_ssh mode (must come first; see spec §11).
if [ "${NGINX_UI_DISABLE_BUNDLED_NGINX:-}" = "true" ]; then
log INFO "host mode: skipping bundled nginx config initialization"
return 0
fi
# Fresh-install seed path (preserves prior behaviour).
if [ "$(ls -A "$ETC_NGINX" 2>/dev/null)" = "" ]; then
cp -rp "$TEMPLATE_DIR"/* "$ETC_NGINX/"
log INFO "Nginx configurations directory initialized"
return 0
fi
# User opt-out for the upgrade-existing path.
if [ "${NGINX_UI_PRESERVE_BUNDLED_CONF:-}" = "true" ]; then
log INFO "NGINX_UI_PRESERVE_BUNDLED_CONF=true; skipping bundled-conf sync"
return 0
fi
# Whitelist of bundled files we own and may upgrade.
if ! sync_bundled_file \
"$TEMPLATE_DIR/conf.d/nginx-ui.conf" \
"$ETC_NGINX/conf.d/nginx-ui.conf" \
"$HASH_FILE"; then
log WARN "bundled-conf sync failed; continuing without blocking bundled nginx startup"
fi
}
# Only run main when executed directly; --testing lets bats source us.
case "${1:-}" in
--testing) return 0 ;;
*) init_config_main ;;
esac
@@ -0,0 +1,9 @@
# Historical SHA256 hashes of the official default conf.d/nginx-ui.conf.
# DO NOT delete entries: each line represents a past official default;
# removing it would prevent that version's users from getting auto-upgraded.
# Append a new line each time resources/docker/nginx-ui.conf changes.
# CI enforces this list contains the current template hash on its last line.
#
# Maintenance: bash resources/docker/scripts/update-nginx-ui-conf-hash.sh
6c3f1f06e242facad58e7d2cb3fd10ab1de18a70bf801c01d1b7294226b251b4 # pre-fix template (up to commit 054295ad's parent)
93d8357f1fc76297a11855c654cd2aaa7cc517f31afa17c7aa1041d0f5cb2882 # current template (commit 054295ad onward)
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Append current SHA256 of resources/docker/nginx-ui.conf to the
# known-hashes file. Run this after editing the template, then commit
# both files together.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
template=resources/docker/nginx-ui.conf
list=resources/docker/nginx-ui.conf.known-hashes
[ -f "$template" ] || { echo "template not found: $template" >&2; exit 1; }
[ -f "$list" ] || { echo "hash list not found: $list" >&2; exit 1; }
hash=$(sha256sum "$template" | awk '{print $1}')
last=$(grep -vE '^[[:space:]]*(#|$)' "$list" | awk '{print $1}' | tail -n1)
if [ "$hash" = "$last" ]; then
echo "No change: hash $hash already at tail of $list"
exit 0
fi
ver=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")
printf '%s # %s (%s)\n' "$hash" "$ver" "$(date +%Y-%m-%d)" >> "$list"
echo "Appended: $hash # $ver"
+37
View File
@@ -0,0 +1,37 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Preserve X-Forwarded-Proto from an outer reverse proxy (e.g. host nginx
# terminating TLS in front of this container). Only fall back to $scheme
# when the inbound request did not carry the header.
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
# Same for X-Forwarded-Host: keep what the outer proxy stamped, otherwise
# use the inbound Host header.
map $http_x_forwarded_host $forwarded_host {
default $http_x_forwarded_host;
'' $http_host;
}
server {
listen 80;
server_name localhost; # your domain here
client_max_body_size 128M; # maximum upload size
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-Host $forwarded_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
+22
View File
@@ -0,0 +1,22 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name localhost; # your domain here
client_max_body_size 128M; # maximum upload size
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
}
}
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bats
setup() {
TMP="$(mktemp -d)"
export ETC_NGINX="$TMP/etc-nginx"
export TEMPLATE_DIR="$TMP/usr-local/etc/nginx"
export HASH_FILE="$TMP/known-hashes"
mkdir -p "$ETC_NGINX/conf.d" "$TEMPLATE_DIR/conf.d"
cp "$BATS_TEST_DIRNAME/fixtures/fixed-default.conf" \
"$TEMPLATE_DIR/conf.d/nginx-ui.conf"
# Hash list = current template hash + historical (unfixed) hash.
sha256sum "$TEMPLATE_DIR/conf.d/nginx-ui.conf" | awk '{print $1}' > "$HASH_FILE"
sha256sum "$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf" | awk '{print $1}' >> "$HASH_FILE"
unset NGINX_UI_DISABLE_BUNDLED_NGINX NGINX_UI_PRESERVE_BUNDLED_CONF
# Source the script in test mode so functions are defined but main is not run.
# shellcheck disable=SC1091
source "$BATS_TEST_DIRNAME/../init-config.sh" --testing
}
teardown() { rm -rf "$TMP"; }
@test "fresh empty dir copies entire template" {
rm -rf "$ETC_NGINX"/*
run init_config_main
[ "$status" -eq 0 ]
[ -f "$ETC_NGINX/conf.d/nginx-ui.conf" ]
}
@test "current-template hash is no-op (no backup created)" {
cp "$TEMPLATE_DIR/conf.d/nginx-ui.conf" "$ETC_NGINX/conf.d/nginx-ui.conf"
run init_config_main
[ "$status" -eq 0 ]
bak_count=$(ls "$ETC_NGINX/conf.d/"*.bak.* 2>/dev/null | wc -l)
[ "$bak_count" -eq 0 ]
}
@test "historical hash upgrades the file with a timestamped backup" {
cp "$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf" \
"$ETC_NGINX/conf.d/nginx-ui.conf"
run init_config_main
[ "$status" -eq 0 ]
bak=$(ls "$ETC_NGINX/conf.d/"nginx-ui.conf.bak.* 2>/dev/null | head -n1)
[ -n "$bak" ]
diff -q "$ETC_NGINX/conf.d/nginx-ui.conf" "$TEMPLATE_DIR/conf.d/nginx-ui.conf"
diff -q "$bak" "$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf"
}
@test "unknown hash (customized) skips file and logs a message" {
printf '# user-customized stub\n' > "$ETC_NGINX/conf.d/nginx-ui.conf"
before=$(cat "$ETC_NGINX/conf.d/nginx-ui.conf")
run init_config_main
[ "$status" -eq 0 ]
[ "$(cat "$ETC_NGINX/conf.d/nginx-ui.conf")" = "$before" ]
[[ "$output" == *"customized"* ]]
bak_count=$(ls "$ETC_NGINX/conf.d/"*.bak.* 2>/dev/null | wc -l)
[ "$bak_count" -eq 0 ]
}
@test "NGINX_UI_PRESERVE_BUNDLED_CONF=true never syncs" {
export NGINX_UI_PRESERVE_BUNDLED_CONF=true
cp "$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf" \
"$ETC_NGINX/conf.d/nginx-ui.conf"
run init_config_main
[ "$status" -eq 0 ]
bak_count=$(ls "$ETC_NGINX/conf.d/"*.bak.* 2>/dev/null | wc -l)
[ "$bak_count" -eq 0 ]
diff -q "$ETC_NGINX/conf.d/nginx-ui.conf" \
"$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf"
}
@test "NGINX_UI_DISABLE_BUNDLED_NGINX=true wins over PRESERVE" {
export NGINX_UI_DISABLE_BUNDLED_NGINX=true
export NGINX_UI_PRESERVE_BUNDLED_CONF=true
run init_config_main
[ "$status" -eq 0 ]
[[ "$output" == *"host mode"* ]]
}
@test "backup failure does not overwrite target" {
[ "$(id -u)" -ne 0 ] || skip "chmod-based perm test is no-op as root"
cp "$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf" \
"$ETC_NGINX/conf.d/nginx-ui.conf"
chmod 555 "$ETC_NGINX/conf.d"
run init_config_main
chmod 755 "$ETC_NGINX/conf.d"
[ "$status" -eq 0 ]
[[ "$output" == *"backup failed"* ]]
diff -q "$ETC_NGINX/conf.d/nginx-ui.conf" \
"$BATS_TEST_DIRNAME/fixtures/unfixed-default.conf"
}
+1
View File
@@ -178,6 +178,7 @@ Nuestra imagen dpcker [uozi/nginx-ui:latest](https://hub.docker.com/r/uozi/nginx
##### Nota
1. Cuando utilice este contenedor por primera vez, asegúrese de que el volumen mapeado a /etc/nginx esté vacío.
2. Si desea incluir archivos estáticos, puede mapear directorios al contenedor.
3. If you are upgrading from an older image, see the [Docker WebSocket fix guide](https://nginxui.com/guide/docker-websocket-fix.html) for required `conf.d/nginx-ui.conf` updates.
**Ejemplo de desplegado Docker**
+1
View File
@@ -205,6 +205,7 @@ systemctl restart nginx-ui
##### 注意
1. 初回利用時は `/etc/nginx` にマッピングするボリュームが空であることを確認してください。
2. 静的ファイルを配信する場合は、適切なディレクトリをマッピングしてください。
3. If you are upgrading from an older image, see the [Docker WebSocket fix guide](https://nginxui.com/guide/docker-websocket-fix.html) for required `conf.d/nginx-ui.conf` updates.
<details>
<summary><b>Dockerでデプロイ</b></summary>
+1
View File
@@ -192,6 +192,7 @@ Docker image của chúng tôi [uozi/nginx-ui:latest](https://hub.docker.com/r/u
##### Ghi chú
1. Khi khởi chạy container lần đầu tiên, hãy chắc chắn thư mục /etc/nginx trên máy host là rỗng.
2. Nếu bạn muốn lưu trữ các tệp tĩnh, bạn có thể mount các thư mục vào container.
3. If you are upgrading from an older image, see the [Docker WebSocket fix guide](https://nginxui.com/guide/docker-websocket-fix.html) for required `conf.d/nginx-ui.conf` updates.
<details>
<summary><b>Triển khai với Docker</b></summary>
+1
View File
@@ -179,6 +179,7 @@ systemctl restart nginx-ui
#### 注意
1. 首次使用时,映射到 `/etc/nginx` 的目录必须为空文件夹。
2. 如果你想要托管静态文件,可以直接将文件夹映射入容器中。
3. 如果你从旧镜像升级,请参阅 [Docker WebSocket 修复指南](https://nginxui.com/zh_CN/guide/docker-websocket-fix.html) 以更新 `conf.d/nginx-ui.conf`
**Docker 部署示例**
+2
View File
@@ -184,6 +184,8 @@ systemctl restart nginx-ui
注意:對映到 `/etc/nginx` 的資料夾應是一個空資料夾。
如果你從舊鏡像升級,請參閱 [Docker WebSocket 修復指南](https://nginxui.com/zh_TW/guide/docker-websocket-fix.html) 以更新 `conf.d/nginx-ui.conf`
**Docker 範例**
```bash