First commit

This commit is contained in:
2026-08-15 19:50:54 +09:00
commit 5ff20429d6
6 changed files with 287 additions and 0 deletions
+3
View File
@@ -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
+52
View File
@@ -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` | 프록시 + 에러 가로채기 |
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
}
+18
View File
@@ -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;
}