First commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# Documentation only (network_mode: host reads the listen port from nginx.conf).
|
||||||
|
ERROR_PAGE_BIND=127.0.0.1
|
||||||
|
ERROR_PAGE_PORT=4098
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# error-page
|
||||||
|
|
||||||
|
`*.takits.me`가 컨테이너 다운으로 502/503/504가 날 때, DSM 기본 에러창 대신 쓰는 공통 nginx.
|
||||||
|
|
||||||
|
DSM 리버스 프록시가 **앱 포트에 직접** 붙으면, 그 포트가 닫혀 있을 때 나스가 에러 페이지를 그립니다. 이 컨테이너는 항상 `127.0.0.1:4098`에서 떠 있고, `Host`로 실제 앱에 넘긴 뒤 업스트림이 죽었을 때만 공통 페이지를 보여 줍니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
브라우저 → DSM RP (HTTPS) → 127.0.0.1:4098 (error-page)
|
||||||
|
└─ Host가 takits.me 이면 127.0.0.1:4889
|
||||||
|
```
|
||||||
|
|
||||||
|
앱의 404/500은 가로채지 않습니다.
|
||||||
|
|
||||||
|
## 배포
|
||||||
|
|
||||||
|
NAS 예: `/volume1/09_container_manager/@error-page`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
확인:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sI http://127.0.0.1:4098/healthz
|
||||||
|
curl -sI -H "Host: unknown.takits.me" http://127.0.0.1:4098/
|
||||||
|
```
|
||||||
|
|
||||||
|
두 번째가 `502`와 커스텀 HTML이면 정상입니다.
|
||||||
|
|
||||||
|
## DSM 리버스 프록시
|
||||||
|
|
||||||
|
1. `upstreams.map`에 호스트 → **지금 DSM에 적어 둔 목적지 포트**를 넣습니다.
|
||||||
|
2. 해당 호스트의 리버스 프록시 목적지를 `127.0.0.1:4098`로 바꿉니다. (앱 포트로 두면 나스 에러창이 그대로입니다.)
|
||||||
|
3. WebSocket이 필요한 호스트는 DSM에서 WS를 그대로 켭니다.
|
||||||
|
4. SSL은 지금처럼 DSM이 종료합니다.
|
||||||
|
|
||||||
|
한 줄 추가 후:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec error-page nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
와일드카드 `*.takits.me` → `127.0.0.1:4098` 한 규칙으로 모아도 됩니다. 맵에 없는 호스트는 공통 에러 페이지가 뜹니다.
|
||||||
|
|
||||||
|
## 파일
|
||||||
|
|
||||||
|
| 파일 | 역할 |
|
||||||
|
|------|------|
|
||||||
|
| `upstreams.map` | 호스트별 앱 포트 |
|
||||||
|
| `html/index.html` | 502/503/504 페이지 |
|
||||||
|
| `nginx.conf` | 프록시 + 에러 가로채기 |
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: error-page
|
||||||
|
|
||||||
|
services:
|
||||||
|
error-page:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: error-page
|
||||||
|
restart: unless-stopped
|
||||||
|
# Reach 127.0.0.1-bound app ports on the NAS (DSM reverse proxy style).
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Seoul
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./upstreams.map:/etc/nginx/upstreams.map:ro
|
||||||
|
- ./html:/usr/share/nginx/html:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4098/healthz"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="color-scheme" content="light" />
|
||||||
|
<title>__STATUS__</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--ink-strong: #555;
|
||||||
|
--ink: #666;
|
||||||
|
--ink-soft: #888;
|
||||||
|
--ink-muted: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px 24px;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 42% 38% at 96% 92%, rgba(93, 220, 232, 0.28), transparent 72%),
|
||||||
|
radial-gradient(ellipse 34% 25% at 72% 103%, rgba(176, 183, 244, 0.24), transparent 72%),
|
||||||
|
radial-gradient(ellipse 14% 22% at 100% 54%, rgba(92, 214, 231, 0.13), transparent 74%),
|
||||||
|
radial-gradient(ellipse 18% 14% at 55% 94%, rgba(202, 181, 241, 0.12), transparent 75%),
|
||||||
|
linear-gradient(145deg, #f8f9fc 0%, var(--bg) 48%, #f2f6fb 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: min(100%, 560px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 28px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: clamp(3rem, 10vw, 4.5rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
color: var(--ink-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: clamp(1.25rem, 3vw, 1.75rem);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
bottom: max(24px, env(safe-area-inset-bottom));
|
||||||
|
left: 24px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<svg class="icon" viewBox="0 0 120 120" aria-hidden="true">
|
||||||
|
<circle cx="60" cy="60" r="54" fill="none" stroke="currentColor" stroke-width="5" />
|
||||||
|
<circle cx="42" cy="48" r="5" fill="currentColor" />
|
||||||
|
<circle cx="78" cy="48" r="5" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M38 78c8-10 36-10 44 0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<p class="code">__STATUS__</p>
|
||||||
|
<h1 class="title">__ERROR_TITLE__</h1>
|
||||||
|
<p class="message">
|
||||||
|
We're working to resolve the issue.<br />
|
||||||
|
Please try again shortly.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
<footer>© 2026 takitsme. All rights reserved.</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type text/html;
|
||||||
|
sendfile on;
|
||||||
|
server_tokens off;
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
"" close;
|
||||||
|
}
|
||||||
|
|
||||||
|
map $status $error_title {
|
||||||
|
default "Service Unavailable";
|
||||||
|
502 "Bad Gateway";
|
||||||
|
503 "Service Unavailable";
|
||||||
|
504 "Gateway Timeout";
|
||||||
|
}
|
||||||
|
|
||||||
|
include /etc/nginx/upstreams.map;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:4098;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
default_type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 502 503 504 /__error/index.html;
|
||||||
|
|
||||||
|
location = /__error/index.html {
|
||||||
|
internal;
|
||||||
|
sub_filter_once off;
|
||||||
|
sub_filter_types text/html;
|
||||||
|
sub_filter "__STATUS__" $status;
|
||||||
|
sub_filter "__ERROR_TITLE__" $error_title;
|
||||||
|
sub_filter "__HOST__" $host;
|
||||||
|
add_header Cache-Control "no-store" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
if ($app_port = 0) {
|
||||||
|
return 502;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass http://127.0.0.1:$app_port;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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 Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
|
proxy_connect_timeout 3s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
|
||||||
|
# Only replace DSM-style "upstream down" pages. App 404/500 stay as-is.
|
||||||
|
proxy_intercept_errors on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Host → published port on the NAS (the destination already used in DSM reverse proxy).
|
||||||
|
# 0 = unknown host → custom error page.
|
||||||
|
#
|
||||||
|
# After adding a line, reload: docker exec error-page nginx -s reload
|
||||||
|
# Then point that host's DSM reverse proxy to 127.0.0.1:4098 (not the app port).
|
||||||
|
|
||||||
|
map $host $app_port {
|
||||||
|
default 0;
|
||||||
|
|
||||||
|
takits.me 4889;
|
||||||
|
www.takits.me 4889;
|
||||||
|
|
||||||
|
fas.takits.me 4885;
|
||||||
|
ops.fas.takits.me 4887;
|
||||||
|
|
||||||
|
# dev-fas.takits.me 80xx;
|
||||||
|
# android-fas.takits.me 6080;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user